├── .dialyzer.exs ├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── config └── config.exs ├── doc ├── README.md ├── img_dialyzer_clean.png ├── img_dialyzer_info.png ├── img_dialyzer_messages_long.png ├── img_dialyzer_messages_short.png ├── img_dialyzer_warning_stats.png └── img_dialyzer_warning_unmatched.png ├── lib ├── application.ex ├── dialyzer │ ├── command_line_config.ex │ ├── config │ │ ├── config.ex │ │ ├── ignore_warning.ex │ │ └── ignore_warning_mapping.ex │ ├── dialyzer.ex │ ├── logger.ex │ ├── plt │ │ ├── app.ex │ │ ├── builder.ex │ │ ├── command.ex │ │ ├── manifest.ex │ │ ├── path.ex │ │ ├── plt.ex │ │ └── updater.ex │ ├── project.ex │ └── warnings │ │ ├── formatter │ │ ├── formatter.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_spec_argument_type_mismatch.ex │ │ │ ├── callback_spec_type_mismatch.ex │ │ │ ├── callback_type_mismatch.ex │ │ │ ├── contract_diff.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 │ │ │ ├── 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 │ │ │ ├── race_condition.ex │ │ │ ├── record_construction.ex │ │ │ ├── record_matching.ex │ │ │ ├── unknown_behaviour.ex │ │ │ ├── unknown_function.ex │ │ │ ├── unknown_type.ex │ │ │ ├── unmatched_return.ex │ │ │ └── unused_function.ex │ │ ├── warning.ex │ │ └── warnings.ex └── mix │ └── tasks │ ├── clean.ex │ ├── dialyzer.ex │ └── info.ex ├── mix.exs ├── mix.lock ├── test.setup.exs └── test ├── fixtures ├── base_project │ ├── .dialyzer.exs │ ├── lib │ │ └── mod.ex │ ├── mix.exs │ └── mix.lock ├── complex_project │ ├── .dialyzer.exs │ ├── lib │ │ └── mod.ex │ ├── mix.exs │ └── mix.lock └── missing_config │ └── mix.exs ├── mix_dialyzer ├── app_test.exs ├── builder_test.exs ├── command_line_test.exs ├── config_test.exs ├── dialyzer_test.exs ├── manifest_test.exs ├── project_test.exs └── warnings_test.exs ├── test_helper.exs └── util.ex /.dialyzer.exs: -------------------------------------------------------------------------------- 1 | [ 2 | apps: [ 3 | remove: [], 4 | include: [] 5 | ], 6 | warnings: [ 7 | ignore: [ 8 | {"non_existing", :*, :unknown_type}, 9 | {"/usr/local/Cellar/erlang/20.3.6/lib/erlang/lib/dialyzer-3.2.4/ebin/dialyzer.beam",:*, :unknown_function}, 10 | {"/usr/local/Cellar/erlang/20.3.6/lib/erlang/lib/hipe-3.17.1/ebin/erl_types.beam", :*, :unknown_function}, 11 | {"/usr/local/Cellar/erlang/20.3.6/lib/erlang/lib/hipe-3.17.1/ebin/erl_types.beam", :*, :unknown_function}, 12 | {"lib/dialyzer/config/config.ex", 73, :invalid_contract}, 13 | {"lib/dialyzer/config/config.ex", 129, :invalid_contract}, 14 | 15 | {"lib/dialyzer/warnings/formatter/warnings/app_call.ex", :*, :no_return}, 16 | {"lib/dialyzer/warnings/formatter/warnings/apply.ex", :*, :no_return}, 17 | {"lib/dialyzer/warnings/formatter/warnings/binary_construction.ex", :*, :no_return}, 18 | {"lib/dialyzer/warnings/formatter/warnings/call.ex", :*, :no_return}, 19 | {"lib/dialyzer/warnings/formatter/warnings/callback_argument_type_mismatch.ex", :*, :no_return}, 20 | {"lib/dialyzer/warnings/formatter/warnings/callback_spec_argument_type_mismatch.ex", :*, :no_return}, 21 | {"lib/dialyzer/warnings/formatter/warnings/callback_spec_type_mismatch.ex", :*, :no_return}, 22 | {"lib/dialyzer/warnings/formatter/warnings/callback_type_mismatch.ex", :*, :no_return}, 23 | {"lib/dialyzer/warnings/formatter/warnings/contract_diff.ex", :*, :no_return}, 24 | {"lib/dialyzer/warnings/formatter/warnings/contract_with_opaque.ex", :*, :no_return}, 25 | {"lib/dialyzer/warnings/formatter/warnings/exact_equality.ex", :*, :no_return}, 26 | {"lib/dialyzer/warnings/formatter/warnings/extra_range.ex", :*, :no_return}, 27 | {"lib/dialyzer/warnings/formatter/warnings/function_application_arguments.ex", :*, :no_return}, 28 | {"lib/dialyzer/warnings/formatter/warnings/function_application_no_function.ex", :*, :no_return}, 29 | {"lib/dialyzer/warnings/formatter/warnings/guard_fail_pattern.ex", :*, :no_return}, 30 | {"lib/dialyzer/warnings/formatter/warnings/improper_list_construction.ex", :*, :no_return}, 31 | {"lib/dialyzer/warnings/formatter/warnings/pattern_match.ex", :*, :no_return}, 32 | {"lib/dialyzer/warnings/formatter/warnings/pattern_match_covered.ex", :*, :no_return}, 33 | {"lib/dialyzer/warnings/formatter/warnings/race_condition.ex", :*, :no_return}, 34 | {"lib/dialyzer/warnings/formatter/warnings/unmatched_return.ex", :*, :no_return}, 35 | ], 36 | active: [ 37 | :unmatched_returns, 38 | :error_handling, 39 | :unknown 40 | ] 41 | ], 42 | extra_build_dir: [] 43 | ] 44 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.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 | # Ignore .fetch files in case you like to edit your project deps locally. 11 | /.fetch 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 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | dialyzer-*.tar 21 | 22 | # Ignore generated files by erlang lexer and parser 23 | /src/dialyzer_lexer.erl 24 | /src/dialyzer_parser.erl 25 | 26 | # Ignore project level files 27 | /.elixir_ls 28 | /.vscode 29 | 30 | .DS_Store 31 | notes.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Gabriel Gatu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dialyzer for Elixir 2 | 3 | Do you want to read more about how everything is implemented? Go here then: [docs](doc/README.md) 4 | 5 | --- 6 | 7 | Elixir has matured a lot in the last years! And in the newer versions we are continually seeing an increasing focus on 8 | project productivity tools, like error diffs, mix xref, exception blame and code formatter. 9 | 10 | Although all of this is already amazing, and it allows us to write nice and correct (mosty) programs, elixir is still 11 | a dynamic programming language, and even though it let's you quickly iterate from an idea to a prototype, 12 | it doesn't make you feel so confident when working on a large scale project. 13 | 14 | This is why we need some form of static typing, and altough we cannot change elixir, we can leverage a statical analysis tool 15 | like dialyzer (but you probably already know this, since you are here! :D). 16 | 17 | # Goals 18 | 19 | The problem of using dialyzer directly is that we cannot leverage its power to offer to the developer a great experience. 20 | Let me explain briefly over that: dialyzer is awesome, but it has some major flaws: 21 | - Incredibly slow (order of minutes, no good!) 22 | - Cryptic error messages 23 | - Somewhat hard to configure correctly (not as straightforward as one would like it to be) 24 | 25 | The proposed solution here is to develop a mix task that should resolve all the flaws showed above: 26 | - Incremental analysis (we want seconds, at worst) 27 | - First class error messages (ansii colored, long/short variant, elm/credo style) 28 | - Dead easy to run (`mix dialyzer` should be used in the 99% of the cases) 29 | 30 | I know this will not be easy and I'll encounter a lot of problems on the road, so any help and/or 31 | suggestion is really welcomed! 32 | 33 | The final goal is to merge this project into elixir itself! :D 34 | (For some more infos, go here: https://summerofcode.withgoogle.com/projects/#4978058864361472) 35 | 36 | # Difference from other projects 37 | 38 | **First of all, I would like to thank the contributors of all the existing projects that already exists! Without you this would be even more difficult than what already is.** 39 | 40 | The goal of this project is to learn how the other existing solutions tried to implement dialyzer for elixir, 41 | take the good parts and implement them, then take the bad parts and try to improve over them. 42 | 43 | There are already a few libraries for interfacing with dialyzer, the problem is that some of them are 44 | not maintained anymore, even though they had some good ideas at their core: 45 | 46 | - https://github.com/jeremyjh/dialyxir: 47 | Most popular project, has been developed constantly over the years and now has some nice features. 48 | 49 | **pro**: 50 | - Most of the features implemented 51 | - Good implementation 52 | 53 | **cons**: 54 | - Some technical debt over the past versions/years 55 | - Missing incremental compilation(*1) 56 | 57 | - (*1) https://github.com/JakeBecker/elixir-ls: 58 | Really nice language server developed by Jake, used mainly with the companion plugin for VSCode. He implemented an incrementally compiled version of dialyzer into elixirLS. 59 | 60 | **pro**: 61 | - Incremental compilation 62 | 63 | **cons**: 64 | - Using some internal components of dialyzer 65 | 66 | - Other notable projects: 67 | - https://github.com/Comcast/dialyzex: Good project level, but still missing some major features 68 | - https://github.com/fishcakez/dialyze: Seems to integrate some type of incremental compilation, needs more investigation from my side 69 | 70 | # Why 71 | 72 | Soooo... Here are some general points to answer to your nice questions: 73 | - **Why not contribute to another project**: 74 | 75 | Since the goal of this project is to be merged into core elixir, it's better to start from scratch and develop everything 76 | with this goal in mind. We do not want technical debt and need pretty much a lot of features that currently are divided into 77 | separated projects. 78 | 79 | On top of this, I can act freely and break things when I want, the dream of any developer :D 80 | 81 | - **Don't you have a social life? Why are you doing this to yourself!**: 82 | 83 | First of all: I know it sounds strange, but I really enjoy creating stuff! And it gets even better when you are paid to do so! 84 | For those who don't know, this is going to be a project under the Google Summer of Code (https://summerofcode.withgoogle.com/). 85 | 86 | Second: I'll be working on something I've really wanted for a long time to be present in elixir! 87 | 88 | Third: This summer I'm gonna experiment with being a digital nomad while working: 89 | I would like to travel to Istanbul, Bali and Hanoi (we'll see about other places, but if you are from one of those, write me! 90 | We can always meet for a beer and chat about how awesome elixir is - and else! :D) 91 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | level: 5 | (case Mix.env() do 6 | :test -> :error 7 | _ -> :info 8 | end) 9 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Project structure 2 | 3 | - /lib 4 | - /command_line: parsing of command line arguments for mix.dialyzer 5 | - /dialyzer 6 | - /plt 7 | - app.ex: get informations about single applications 8 | - builder.ex: module for building plts 9 | - command.ex: module for sending commands to :dialyzer 10 | - manifest.ex: handling manifest file for caching purposes 11 | - path.ex: utilities for generating plts paths 12 | - plt.ex: main module for interfacing with the plt folder 13 | - updater.ex: module for updating plts 14 | 15 | - config.ex: main module for handling *.dialyzer.exs* 16 | - dialyzer.ex: actual module that runs dialyzer 17 | - project.ex: utilities for accessing project level informations 18 | - /mix 19 | - /tasks: all mix tasks supported, one per file 20 | application.ex 21 | 22 | # Preface 23 | 24 | This project born as an imitation of dialyxir. I've started by looking at all the source code, and copying the bits I needed. 25 | 26 | After I had a prototype working, I've started by cleaning and splitting functionalities into different modules. 27 | 28 | The third step has been building on top of the resulting project I had, by implementing new functionalities. 29 | 30 | This is why you can find some of the ideas implemented here also in dialyxir. 31 | So, I would like to thank Jeremy and all the contributors of dialyxir, without you, 32 | this would have been much harder! 33 | 34 | # Starting point: **mix dialyzer** 35 | 36 | The `mix dialyzer` task doesn't require any argument, and takes care of different 37 | things: 38 | 39 | - Creating a configuration file in case it doesn't exist (.dialyzer.exs) 40 | - Creating all the necessary plts 41 | - Caching the plts in the proper folders and building a manifest file 42 | - Updating the project plt with the latest changes 43 | - Displaying to the user the result of the analysis 44 | 45 | # Configuration file 46 | 47 | First of all, when `mix dialyzer` is executed, it searches for a file called 48 | **.dialyzer.exs** inside the project root. 49 | 50 | This configuration file is similar in scope to what the *.credo.exs* or the *.formatter.exs* do: it is used to configure the dialyzer application, and allows the user to control the applications analyzed/excluded, the active warnings and external build directories. 51 | 52 | In case this file is missing, mix_dialyzer creates a new one, with some default settings: 53 | 54 | ```elixir 55 | [ 56 | warnings: [:unmatched_returns, :error_handling, :underspecs, :unknown], 57 | apps: [remove: [], include: []], 58 | extra_build_dir: [] 59 | ] 60 | ``` 61 | 62 | Every time the configuration file is updated (for instance, an application is removed from analysis), the next `mix dialyzer` execution will automatically detect the change and update the plts accordingly. 63 | 64 | # Plt creation 65 | 66 | The plt building is divided into 3 phases: 67 | 68 | - All the direct and transitive dependencies of the project are found and analyzed 69 | - It generates a definition of 3 plts to be created: erlang, elixir and project 70 | - It builds each plt by caching common resources and reusing the previous plt 71 | 72 | The first point is pretty straightforward, it gets all the dependencies of the project (by handling also umbrella projects) and transitive dependencies (deps of deps), and for each one, it tries to get informations like declared modules, filepaths and version, caching each visited application and reusing them in case of multiple requests. 73 | 74 | In the second point, it uses the previous found applications to define 3 plts to be built in order: the erlang, elixir and project level plts. 75 | 76 | The erlang and elixir plts contain constant defined applications: 77 | 78 | - erlang: `[:erts, :kernel, :stdlib, :crypto]` 79 | - elixir `[:elixir] ++ erlang` 80 | 81 | and are going to be located in the home folder of the user, to allow multiple projects to take advantage of them. 82 | 83 | The project plt contains all the applications defined by the elixir plt, plus the applications found at runtime with the analysis (1st point), and is going to be located inside the *_build* directory. 84 | 85 | In the third point, it builds the plts one by one, starting from the erlang one. 86 | When building a plt that is not the first one, it reuses the previously built plt, by cloning it and removing/adding the applications accordingly. 87 | 88 | # Manifest file 89 | 90 | After each build/update of the plts, a manifest file is created inside the *_build* directory, living alongside the project level plt. 91 | 92 | The manifest file contains a snapshot of the project in that precise moment. When a new **mix dialyzer** invocation happens, the manifest file is the first one checked, by comparing the previously cached snapshot with the current enviroment, and resulting in one of these 3 states: 93 | 94 | - `:missing` - plt or manifest file is missing 95 | - `:outdated` - plts and manifest exists, but something is outdated 96 | - `:up_to_date` - everything seems good 97 | 98 | When all plts and manifest are present, to check the status of the project, the manifest is performing different checks: 99 | 100 | - Snapshot applications are the same as current applications 101 | - No applications has been included/removed by the `.dialyzer.exs` config file 102 | - They have the same version 103 | 104 | # Plt updating 105 | 106 | When the manifest file returns an `:outdated` status, a plt updating occours. 107 | In this case, the manifest file returns a list of all the applications that has been changed, by specifying if they have been removed, added or changed. 108 | 109 | The updating process takes these applications, and creates 2 lists of modules: 110 | 111 | - 1: Modules to remove 112 | - 2: Modules to add 113 | 114 | In the end, it performs the task by removing and then adding the modules to the project level plt. Finally it checks again the plt for consistency. 115 | 116 | # Getting project level informations 117 | 118 | A new task: `mix dialyzer.info` has been introduced, to get project level informations about the current enviroment setup. 119 | 120 | It divides all informations into x main categories: 121 | 122 | - **Current application name** 123 | 124 | - **Applications included into analysis**: divided into 3 blocks, for erlang, elixir and project 125 | 126 | - **Applications removed from analysis** 127 | 128 | - **Warnings currently active**: inferred from the `.dialyzer.exs` file, shows also a quick description of the warning 129 | 130 | - **Warnings ignored**: a list of all the excluded warnings with a quick description. Handy when the user wants to know all the options available and add them quickly. 131 | 132 | - **Build directories** 133 | 134 | ![](./img_dialyzer_info.png "mix dialyzer.info") 135 | 136 | # Cleaning dialyzer artifacts 137 | 138 | Another task, runnable with `mix dialyzer.clean`, is defined. 139 | This allows to clean partially/totally the artifacts generated by the *mix_dialyzer* integration: plt files, manifest file, etc.. 140 | 141 | There are 2 options available: 142 | 143 | - `--info` - used to display success/failure informations as the files are being removed 144 | 145 | - `--all` - used to delete also the erlang and elixir plts 146 | 147 | By default, when executed, `mix dialyzer.clean` runs without logging anything, 148 | and removes only the project level plt and the manifest file. 149 | 150 | ![](./img_dialyzer_clean.png "mix dialyzer.clean") 151 | 152 | # Warning formatting 153 | 154 | Thanks to the **AMAZING** work from Andrew (@asummers) on dialyxir, I've managed to integrate the formatting ability also into mix-dialyzer. 155 | 156 | Currently, the integration is still pretty much a porting of his amazing work, with a few modifications: 157 | 158 | - Limited the formatting option to 2 types: long and short messages 159 | - Added a new callback in warnings to retrieve a simple name of a warning (for user context) 160 | - Improved description and formatting of all warnings 161 | - Removed explanation function from warnings 162 | - Added ansi coloring 163 | - General refactoring across all his modules 164 | 165 | The display style of the warnings is inspired by Elm, but further improvements are expected since some messages are not as readable as I whish they were. 166 | 167 | --- 168 | 169 | For the user, this feature is automatically enabled, and he can choose between 2 options (warnings with formatting of version 0.1.0): 170 | 171 | ```elixir 172 | mix dialyzer # automatic fallback to short messages 173 | ``` 174 | ![](./img_dialyzer_messages_short.png "short error messages") 175 | 176 | ```elixir 177 | mix dialyzer --long 178 | ``` 179 | 180 | ![](./img_dialyzer_messages_long.png "long error messages") 181 | 182 | Finally, from version `0.2.0` we are printing also some stats regarding the warnings emitted: 183 | 184 | ![](./img_dialyzer_warning_stats.png "warnings stats") 185 | 186 | # Ignoring warnings 187 | 188 | From version `0.2.0`, it's also possibile to ignore specific warnings. 189 | Before implementing this, me and @seancribbs evaluated some alternatives, like: 190 | 191 | 1 - Ignoring warnings based on file and line number, directly specified by the user, like: **/lib/dialyzer/config.ex:48**. 192 | 193 | - Cons: The problem with this approach is that now we have a constraint about the line number, because if it changes we are not referring anymore to the right warning, and it will be emitted again. 194 | 195 | - Pros: It's very easy to ignore a specific warning, it's just a matter of copy paste the line which generated the error inside `.dialyzer.exs`. Another advantage is that this allows to be specific about the warning, although we could have a line with 2 warnings and it could not be possible to suppress only one of them. (We could make it a tuple with the format of {filename, warning}). 196 | 197 | 2 - Toggle warnings at function level, by specifying a tuple like **{ModuleName, Function, arity}** or by adding an annotation on top of the function, like **@dialyzer_ignore**. 198 | 199 | - Cons: A lot of them! First of all, poor control over warnings you want to ignore. Also, in case of the second one, leaking dialyzer control into the source code (then, if the user wants to remove dialyzer, it has unnecessary annotations it still needs to remove). 200 | 201 | - Pros: Easy to ignore a warning. Easy to understand from where that warning came. Pretty extensible (for example, a 4 elem tuple to specify also the warning type or the annotatin could take args). 202 | 203 | 3 - Specify directly the error message as a string, and if it matches, it's going to be ignored. 204 | 205 | - Cons: Very limited extension. Dependant on the error formatting. Generally a bad idea. 206 | 207 | - Pros: Well.... Very easy to ignore a very specific warning. 208 | 209 | 4 - Tuple with dialyzer warning and file/line, like what dialyxir currently supports: **{":0:unknown_function Function :erl_types.t_is_opaque/1/1 does not exist."}** 210 | 211 | - Cons: Very hard to write and dependant on the dialyzer warning formatting. Difficult for a beginner to write and to read. (The writing part could be made easier by "leaking" the dialyzer warning to the user, but then we cannot take full advantage of the fact that we have formatted warnings). 212 | 213 | - Pros: Very easy to ignore specific warning. Easy to add ignored warnings if we are leaking the dialyzer warning. 214 | 215 | 5 - Tagged warnings that can be referred by an hash and ignored this way. For example: **[nwhaetf] lib/mod.ex:1 warning...**. 216 | 217 | - Cons: Hard to understand which warnings have been ignored by looking at the `.dialyzer.exs` file. (This could be improved by adding a mix task that shows for each tag which warning has been ignored, and we could also show some more informations, like documentation and so on...) Somehow difficult to make the ignored warnings portable to other enviroments without sharing mappings between hash <> warnings. 218 | 219 | - Pros: Easy to add ignored warnings. Easy to ignore a very specific warning. 220 | 221 | In the end, after some discussion (with the contribution of @asummers, thank you!), we ended up with the solition number 4 (initial adopted solution was nr. 5, but quickly we realized it had some deep flaws). 222 | 223 | To ignore a specific warning, a tuple with this format is required: 224 | 225 | ```elixir 226 | {filepath, line, warning} 227 | ``` 228 | 229 | If the developer wants to ignore more than one warning, based on the same pattern, a wildcard is available: `:*` 230 | 231 | ```elixir 232 | {filepath, :*, :*} # Ignore all warnings from this file 233 | ``` 234 | 235 | Finally, when analyzing with the option `--long`, a tuple to ignore the warning will be printed for each emitted warning. 236 | 237 | All this work is still in refinement process, for example we still need to support warnings that have not a file and a line number! 238 | 239 | 240 | # Unmatched warnings 241 | 242 | If the user has some warnings inside `.dialyzer.exs` that are not valid (ie: they are not matching any emitted warning), dialyzer will alert the user about them when running it, and also it will check if there are some warnings that are very similar, based on one of this 2 conditions: 243 | 244 | - They refer to the same file and warning 245 | - They refer to the same file and the warning is mispelled but similar to one emitted (using String.jaro_distance to check). 246 | 247 | ![](./img_dialyzer_warning_unmatched.png "unmatched warnings") -------------------------------------------------------------------------------- /doc/img_dialyzer_clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielgatu/mix-dialyzer/5fce13022e44f5ca4fca5d4b4f064b5588449b7d/doc/img_dialyzer_clean.png -------------------------------------------------------------------------------- /doc/img_dialyzer_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielgatu/mix-dialyzer/5fce13022e44f5ca4fca5d4b4f064b5588449b7d/doc/img_dialyzer_info.png -------------------------------------------------------------------------------- /doc/img_dialyzer_messages_long.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielgatu/mix-dialyzer/5fce13022e44f5ca4fca5d4b4f064b5588449b7d/doc/img_dialyzer_messages_long.png -------------------------------------------------------------------------------- /doc/img_dialyzer_messages_short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielgatu/mix-dialyzer/5fce13022e44f5ca4fca5d4b4f064b5588449b7d/doc/img_dialyzer_messages_short.png -------------------------------------------------------------------------------- /doc/img_dialyzer_warning_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielgatu/mix-dialyzer/5fce13022e44f5ca4fca5d4b4f064b5588449b7d/doc/img_dialyzer_warning_stats.png -------------------------------------------------------------------------------- /doc/img_dialyzer_warning_unmatched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielgatu/mix-dialyzer/5fce13022e44f5ca4fca5d4b4f064b5588449b7d/doc/img_dialyzer_warning_unmatched.png -------------------------------------------------------------------------------- /lib/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | import Supervisor.Spec 6 | 7 | children = [ 8 | worker(Dialyzer.Plt.App.Cache, []) 9 | ] 10 | 11 | Supervisor.start_link(children, strategy: :one_for_one) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/dialyzer/command_line_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.CommandLine.Config do 2 | defstruct msg_type: :short, halt_on_error: false 3 | 4 | alias __MODULE__ 5 | 6 | @type t :: %__MODULE__{} 7 | @command_options [ 8 | long: :boolean, 9 | ci: :boolean 10 | ] 11 | 12 | def parse(args) do 13 | {opts, _, _} = OptionParser.parse(args, strict: @command_options) 14 | 15 | halt_on_error = Keyword.get(opts, :ci, false) 16 | msg_type = 17 | case Keyword.fetch(opts, :long) do 18 | {:ok, true} -> :long 19 | :error -> :short 20 | end 21 | 22 | %Config{msg_type: msg_type, halt_on_error: halt_on_error} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/dialyzer/config/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Config do 2 | defstruct [ 3 | :init_plt, 4 | build_dir: [], 5 | warnings: [active: [], ignore: []], 6 | apps: [remove: [], include: []], 7 | cmd: nil 8 | ] 9 | 10 | import Dialyzer.Logger 11 | alias Dialyzer.{CommandLine, Project, Plt} 12 | alias __MODULE__ 13 | 14 | @type t :: %__MODULE__{} 15 | 16 | @doc """ 17 | It loads and (if not already present), creates the 18 | configuration file with the default options inferred 19 | from the project enviroment. 20 | """ 21 | @spec load() :: Config.t() 22 | def load() do 23 | config = load_config_file() 24 | %Config{config | cmd: %CommandLine.Config{}} 25 | end 26 | 27 | @spec load(CommandLine.Config.t()) :: Config.t() 28 | def load(cmd_config) do 29 | config = load_config_file() 30 | %Config{config | cmd: cmd_config} 31 | end 32 | 33 | @doc """ 34 | It returns the absolute path of the configuration file. 35 | """ 36 | @spec path :: binary 37 | def path do 38 | File.cwd!() |> Path.join("/.dialyzer.exs") 39 | end 40 | 41 | @doc """ 42 | It returns the default warnings used for an analysis. 43 | """ 44 | @spec default_warnings :: [atom] 45 | def default_warnings do 46 | [ 47 | :unmatched_returns, 48 | :error_handling, 49 | :unknown 50 | ] 51 | end 52 | 53 | @spec load_config_file() :: t 54 | defp load_config_file do 55 | path() 56 | |> File.read() 57 | |> case do 58 | {:ok, content} -> 59 | content 60 | |> Code.eval_string() 61 | |> elem(0) 62 | |> read_config_file() 63 | 64 | {:error, _} -> 65 | info("Dialyzer: configuration file not found. Creating it right now: .dialyzer.exs") 66 | 67 | content = create_base_config() 68 | create_config_file(content) 69 | read_config_file(content) 70 | end 71 | end 72 | 73 | @spec read_config_file(binary) :: t 74 | defp read_config_file(content) do 75 | init_plt = Plt.Path.project_plt() 76 | build_dir = content[:extra_build_dir] ++ Project.build_paths() 77 | 78 | active_warnings = content[:warnings][:active] 79 | ignored_warnings = content[:warnings][:ignore] 80 | 81 | remove_apps = content[:apps][:remove] 82 | included_apps = content[:apps][:include] 83 | 84 | %Config{ 85 | init_plt: init_plt, 86 | build_dir: build_dir, 87 | warnings: [ 88 | active: active_warnings, 89 | ignore: ignored_warnings 90 | ], 91 | apps: [ 92 | remove: remove_apps, 93 | include: included_apps 94 | ] 95 | } 96 | end 97 | 98 | @spec create_base_config() :: Keyword.t() 99 | defp create_base_config do 100 | warnings = default_warnings() 101 | 102 | [ 103 | apps: [ 104 | remove: [], 105 | include: [] 106 | ], 107 | warnings: [ 108 | ignore: [], 109 | active: warnings 110 | ], 111 | extra_build_dir: [] 112 | ] 113 | end 114 | 115 | @spec create_config_file(Keyword.t()) :: :ok 116 | defp create_config_file(content) do 117 | path() 118 | |> File.write!( 119 | inspect( 120 | content, 121 | limit: :infinity, 122 | printable_limit: :infinity, 123 | pretty: true, 124 | width: 0 125 | ) 126 | ) 127 | end 128 | 129 | @spec to_erlang_format(Config.t()) :: map 130 | def to_erlang_format(config) do 131 | [ 132 | init_plt: config.init_plt |> String.to_charlist(), 133 | files_rec: config.build_dir |> Enum.map(&String.to_charlist/1), 134 | warnings: config.warnings[:active] 135 | ] 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/dialyzer/config/ignore_warning.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Config.IgnoreWarning do 2 | defstruct [:file, :line, :warning] 3 | 4 | alias __MODULE__ 5 | 6 | @type t :: %__MODULE__{} 7 | 8 | @spec new({String.t(), integer, atom}) :: t 9 | def new({file, line, warning}) do 10 | %IgnoreWarning{file: file, line: line, warning: warning} 11 | end 12 | 13 | @spec get_field_with_default(t, atom, any) :: any 14 | def get_field_with_default(ignore_warning, field, default) do 15 | ignore_warning 16 | |> Map.get(field) 17 | |> case do 18 | nil -> default 19 | :* -> default 20 | val -> val 21 | end 22 | end 23 | 24 | @spec associate_with_emitted_warnings([t], [Warning.t()]) :: [IgnoreWarning.Mapping.t()] 25 | def associate_with_emitted_warnings(ignored_warnings, emitted_warnings) do 26 | Enum.map(ignored_warnings, fn ignored_warning -> 27 | filtered_warns = 28 | Enum.filter(emitted_warnings, fn emitted_warning -> 29 | ignored_file = get_field_with_default(ignored_warning, :file, emitted_warning.file) 30 | ignored_line = get_field_with_default(ignored_warning, :line, emitted_warning.line) 31 | ignored_warn = get_field_with_default(ignored_warning, :warning, emitted_warning.name) 32 | 33 | cond do 34 | emitted_warning.file != ignored_file -> false 35 | emitted_warning.line != ignored_line -> false 36 | emitted_warning.name != ignored_warn -> false 37 | true -> true 38 | end 39 | end) 40 | 41 | IgnoreWarning.Mapping.new(ignored_warning, filtered_warns) 42 | end) 43 | end 44 | 45 | @spec find_suggestions_for_unmatched_warns([Warning.t()], t) :: [ 46 | Warning.t() 47 | ] 48 | def find_suggestions_for_unmatched_warns(emitted_warnings, ignored_warning) do 49 | Enum.filter(emitted_warnings, fn emitted_warning -> 50 | ignored_file = get_field_with_default(ignored_warning, :file, emitted_warning.file) 51 | ignored_warn = get_field_with_default(ignored_warning, :warning, emitted_warning.name) 52 | 53 | cond1 = emitted_warning.file == ignored_file and emitted_warning.name == ignored_warn 54 | 55 | cond2 = 56 | (fn -> 57 | warn1 = Atom.to_string(emitted_warning.name) 58 | warn2 = Atom.to_string(ignored_warn) 59 | emitted_warning.file == ignored_file and String.jaro_distance(warn1, warn2) > 0.8 60 | end).() 61 | 62 | cond1 or cond2 63 | end) 64 | end 65 | 66 | @spec to_ignore_format(t) :: {String.t(), integer, atom} 67 | def to_ignore_format(ignore_warning) do 68 | file = get_field_with_default(ignore_warning, :file, :*) 69 | line = get_field_with_default(ignore_warning, :line, :*) 70 | warning = get_field_with_default(ignore_warning, :warning, :*) 71 | 72 | {file, line, warning} 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/dialyzer/config/ignore_warning_mapping.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Config.IgnoreWarning.Mapping do 2 | defstruct [:ignore_warning, :warnings] 3 | 4 | alias __MODULE__ 5 | alias Dialyzer.{Config.IgnoreWarning, Warning} 6 | 7 | @type t :: %__MODULE__{} 8 | 9 | @spec new(IgnoreWarning.t(), [Warning.t()]) :: t 10 | def new(ignore_warning, warnings) do 11 | %Mapping{ignore_warning: ignore_warning, warnings: warnings} 12 | end 13 | 14 | @spec filter_warnings_to_emit([Warning.t()], [t]) :: [Warning.t()] 15 | def filter_warnings_to_emit(warnings, mappings) do 16 | Enum.filter(warnings, fn warning -> 17 | not Enum.any?(mappings, fn mapping -> 18 | warning in mapping.warnings 19 | end) 20 | end) 21 | end 22 | 23 | @spec filter_unmatched_warnings([t]) :: [IgnoreWarning.t()] 24 | def filter_unmatched_warnings(warnings_to_ignore) do 25 | warnings_to_ignore 26 | |> Enum.filter(fn mapping -> 27 | mapping.warnings == [] 28 | end) 29 | |> Enum.map(fn mapping -> mapping.ignore_warning end) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/dialyzer/dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer do 2 | import Dialyzer.Logger 3 | alias Dialyzer.Config 4 | 5 | @doc """ 6 | It takes the project configuration and runs dialyzer according to it, 7 | by leaving removed applications/warnings off the analysis, and so on. 8 | 9 | It returns `:ok` if the analysis succeded without any warning from dialyzer, 10 | otherwise it returns `{:error, String.t()}`, with the second argument being 11 | the formatted output of the analysis. 12 | """ 13 | @spec run(Config.t()) :: :ok | {:error, String.t()} 14 | def run(config) do 15 | Dialyzer.Plt.ensure_loaded(config) 16 | 17 | config 18 | |> Config.to_erlang_format() 19 | |> Dialyzer.Plt.Command.run() 20 | |> case do 21 | {:ok, []} -> 22 | :ok 23 | {:ok, warnings} -> 24 | {:error, Dialyzer.Warnings.format(warnings, config)} 25 | {:error, msg} -> 26 | {:error, error(":dialyzer.run error: #{msg}")} 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/dialyzer/logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Logger do 2 | require Logger 3 | 4 | def info(arg) do 5 | IO.puts(color(:green, "* - #{arg}")) 6 | end 7 | 8 | def error(arg) do 9 | IO.puts(color(:red, "* - #{arg}")) 10 | end 11 | 12 | def color(:cyan, arg), do: IO.ANSI.format([:cyan, arg], true) 13 | def color(:yellow, arg), do: IO.ANSI.format([:yellow, arg], true) 14 | def color(:green, arg), do: IO.ANSI.format([:green, arg], true) 15 | def color(:red, arg), do: IO.ANSI.format([:red, arg], true) 16 | end 17 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/app.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.App do 2 | @moduledoc """ 3 | It represents an OTP application. This is used as a "wrapper", 4 | to group some informations used internally by `mix_dialyzer`. 5 | 6 | Internally, it implements a caching system, this is because some 7 | projects depend on the same applications, and we want to speed things up. 8 | """ 9 | 10 | defstruct [:app, :mods, :vsn] 11 | 12 | alias __MODULE__ 13 | 14 | @type t :: %App{} 15 | 16 | @doc """ 17 | It returns informations, like modules defined, 18 | files location and current version, of an application. 19 | 20 | Returns nil if the application doesn't exist. 21 | """ 22 | @spec get_info(atom, boolean) :: t | nil 23 | def info(app), do: get_info(app, true) 24 | def info(app, use_cached_version), do: get_info(app, use_cached_version) 25 | 26 | defp get_info(app, true) do 27 | App.Cache.get_or_insert(app) 28 | end 29 | 30 | defp get_info(app, false) do 31 | case can_load_app?(app) do 32 | false -> 33 | nil 34 | 35 | true -> 36 | app 37 | |> read_app_info() 38 | end 39 | end 40 | 41 | @spec can_load_app?(atom) :: boolean 42 | defp can_load_app?(app) do 43 | case Application.load(app) do 44 | :ok -> true 45 | {:error, {:already_loaded, _}} -> true 46 | {:error, _} -> false 47 | end 48 | end 49 | 50 | @spec read_app_info(atom) :: t 51 | defp read_app_info(app) do 52 | info = Application.spec(app) 53 | mods = Enum.map(info[:modules], &App.Module.new/1) 54 | vsn = info[:vsn] 55 | 56 | %App{app: app, mods: mods, vsn: vsn} 57 | end 58 | end 59 | 60 | defmodule Dialyzer.Plt.App.Module do 61 | @moduledoc """ 62 | It represents a BEAM module. This is used as a "wrapper", 63 | to group some informations used internally by `mix_dialyzer`. 64 | """ 65 | 66 | defstruct [:module, :filepath, :md5] 67 | alias __MODULE__ 68 | 69 | @type t :: %__MODULE__{} 70 | 71 | @spec new(atom) :: t 72 | def new(mod) when is_atom(mod) do 73 | filepath = :code.which(mod) 74 | md5 = apply(mod, :module_info, [:md5]) 75 | 76 | %Module{module: mod, filepath: filepath, md5: md5} 77 | end 78 | end 79 | 80 | defmodule Dialyzer.Plt.App.Cache do 81 | use Agent 82 | 83 | def start_link do 84 | Agent.start_link(fn -> %{} end, name: __MODULE__) 85 | end 86 | 87 | def get_or_insert(app) do 88 | Agent.get_and_update(__MODULE__, fn state -> 89 | {info, new_state} = 90 | case Map.fetch(state, app) do 91 | :error -> 92 | info = Dialyzer.Plt.App.info(app, false) 93 | {info, Map.put(state, app, info)} 94 | 95 | {:ok, info} -> 96 | {info, state} 97 | end 98 | 99 | {info, new_state} 100 | end) 101 | end 102 | 103 | def in_cache?(app) do 104 | Agent.get(__MODULE__, fn state -> 105 | Map.has_key?(state, app) 106 | end) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/builder.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.Builder do 2 | @moduledoc """ 3 | The builder is responsable for building a plt from zero, 4 | by including all the necessary modules, and using pre-existing 5 | plt to build over them and speed things up. 6 | """ 7 | 8 | alias Dialyzer.{Config, Project, Plt} 9 | require Logger 10 | 11 | @doc """ 12 | It builds incrementally all the plts. 13 | """ 14 | @spec build(Config.t()) :: :ok 15 | def build(config) do 16 | Enum.each(Plt.missing_plts(), fn plt -> build_plt(plt, config) end) 17 | end 18 | 19 | @doc """ 20 | It returns the apps used to build the erlang plt. 21 | """ 22 | @spec erlang_apps :: [atom] 23 | def erlang_apps, do: [:erts, :kernel, :stdlib, :crypto] 24 | 25 | @doc """ 26 | It returns the apps used to build the elixir plt. 27 | """ 28 | @spec elixir_apps :: [atom] 29 | def elixir_apps, do: erlang_apps() ++ [:elixir, :mix] 30 | 31 | @doc """ 32 | It returns the apps used to build the project plt. 33 | """ 34 | @spec project_apps(Config.t()) :: [atom] 35 | def project_apps(config) do 36 | removed_apps = config.apps[:remove] 37 | included_apps = config.apps[:include] 38 | 39 | Project.dependencies() 40 | |> Kernel.++(elixir_apps()) 41 | |> Enum.uniq() 42 | |> Kernel.++(included_apps) 43 | |> Kernel.--(removed_apps) 44 | |> Enum.uniq() 45 | end 46 | 47 | @spec build_plt(atom, Config.t()) :: {:ok, list} | {:error, any} 48 | defp build_plt(:erlang, _config) do 49 | path = Plt.Path.erlang_plt() 50 | apps = erlang_apps() |> Enum.map(&Plt.App.info/1) 51 | prev_plt_apps = [] 52 | 53 | ensure_dir_accessible!(path) 54 | _ = Plt.Command.new(path) 55 | build_plt(path, apps, prev_plt_apps) 56 | end 57 | 58 | defp build_plt(:elixir, _config) do 59 | path = Plt.Path.elixir_plt() 60 | apps = elixir_apps() |> Enum.map(&Plt.App.info/1) 61 | prev_plt_apps = erlang_apps() |> Enum.map(&Plt.App.info/1) 62 | 63 | Plt.Command.copy(Plt.Path.erlang_plt(), path) 64 | build_plt(path, apps, prev_plt_apps) 65 | end 66 | 67 | defp build_plt(:project, config) do 68 | path = Plt.Path.project_plt() 69 | apps = project_apps(config) |> Enum.map(&Plt.App.info/1) |> Enum.filter(&(&1 != nil)) 70 | prev_plt_apps = elixir_apps() |> Enum.map(&Plt.App.info/1) 71 | 72 | Plt.Command.copy(Plt.Path.elixir_plt(), path) 73 | build_plt(path, apps, prev_plt_apps) 74 | end 75 | 76 | defp build_plt(path, apps, prev_plt_apps) do 77 | ensure_dir_accessible!(path) 78 | 79 | plt_files = collect_files_from_apps(apps) 80 | prev_plt_files = collect_files_from_apps(prev_plt_apps) 81 | 82 | remove = MapSet.difference(prev_plt_files, plt_files) 83 | add = MapSet.difference(plt_files, prev_plt_files) 84 | 85 | _ = Plt.Command.remove(path, Enum.to_list(remove)) 86 | _ = Plt.Command.add(path, Enum.to_list(add)) 87 | end 88 | 89 | @spec collect_files_from_apps([Plt.App.t()]) :: MapSet.t() 90 | defp collect_files_from_apps(apps) do 91 | Enum.flat_map(apps, fn app -> 92 | app 93 | |> Map.fetch!(:mods) 94 | |> Enum.map(& &1.filepath) 95 | |> Enum.filter(&(not is_atom(&1))) 96 | end) 97 | |> MapSet.new() 98 | end 99 | 100 | @spec ensure_dir_accessible!(String.t()) :: :ok | :error 101 | defp ensure_dir_accessible!(dir) do 102 | case :filelib.ensure_dir(dir) do 103 | :ok -> 104 | :ok 105 | 106 | {:error, error} -> 107 | raise "Could not write: #{dir}. Error: #{to_string(error)}" 108 | :error 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/command.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.Command do 2 | @moduledoc """ 3 | Utility module containing all the necessary functions to 4 | interact with a plt. 5 | 6 | It takes care of logging to the user the operations 7 | running, and converting to erlang types the parameters 8 | (for example, erlang expects charlists instead of elixir strings). 9 | """ 10 | 11 | import Dialyzer.Logger 12 | 13 | @doc """ 14 | It creates a new plt basic plt inside the path passed. 15 | """ 16 | @spec new(binary) :: {:ok, list} | {:error, any} 17 | def new(plt_path) do 18 | info("Creating #{Path.basename(plt_path)}") 19 | 20 | plt_path = to_charlist(plt_path) 21 | run(analysis_type: :plt_build, output_plt: plt_path, apps: [:erts]) 22 | end 23 | 24 | @doc """ 25 | It duplicates a plt by copying it from one path to another one. 26 | """ 27 | @spec copy(binary, binary) :: :ok 28 | def copy(plt_path, new_plt_path) do 29 | info("Copying #{Path.basename(plt_path)} to #{Path.basename(new_plt_path)}") 30 | File.cp!(plt_path, new_plt_path) 31 | end 32 | 33 | @doc """ 34 | It adds the files to the plt, without checking. 35 | """ 36 | @spec add(binary, [binary]) :: {:ok, list} | {:error, any} 37 | def add(plt_path, files) do 38 | if Enum.count(files) > 0 do 39 | info("Adding modules to #{Path.basename(plt_path)}") 40 | 41 | plt_path = to_charlist(plt_path) 42 | files = Enum.map(files, &to_charlist/1) 43 | run(analysis_type: :plt_add, init_plt: plt_path, output_plt: plt_path, files: files) 44 | end 45 | end 46 | 47 | @doc """ 48 | It removes the files from the plt, without checking. 49 | """ 50 | @spec remove(binary, [binary]) :: {:ok, list} | {:error, any} 51 | def remove(plt_path, files) do 52 | if Enum.count(files) > 0 do 53 | info("Removing modules from #{Path.basename(plt_path)}") 54 | 55 | plt_path = to_charlist(plt_path) 56 | files = Enum.map(files, &to_charlist/1) 57 | run(analysis_type: :plt_remove, init_plt: plt_path, output_plt: plt_path, files: files) 58 | end 59 | end 60 | 61 | @doc """ 62 | It used dialyzer to check the plt. 63 | """ 64 | @spec check(binary) :: {:ok, list} | {:error, any} 65 | def check(plt_path) do 66 | info("Checking modules in #{Path.basename(plt_path)}") 67 | 68 | plt_path = to_charlist(plt_path) 69 | run(analysis_type: :plt_check, init_plt: plt_path) 70 | end 71 | 72 | @doc """ 73 | It runs dialyzer with the arguments passed. 74 | """ 75 | @spec run(Keyword.t()) :: {:ok, list} | {:error, any} 76 | def run(opts) do 77 | try do 78 | res = :dialyzer.run([check_plt: false] ++ opts) 79 | {:ok, res} 80 | catch 81 | {:dialyzer_error, msg} -> 82 | # TODO: when creating a plt without an app, this logs an error. 83 | # suppress for now, but remember to handle this case. 84 | error(":dialyzer.run error: #{msg}") 85 | {:error, msg} 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/manifest.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.Manifest do 2 | @moduledoc """ 3 | The manifest is responsable for keeping track of all 4 | the applications and modules included in the project plt. 5 | 6 | The manifest is created the first time the `mix dialyzer` command is run. 7 | Subsequently, after every other call, the manifest will be updated with the 8 | latest changes, so that it is always in sync with the project. 9 | 10 | When an application or a single module is added/removed, 11 | the manifest is used to incrementally analyse the project, by 12 | including/removing only the changed modules in the plt. 13 | """ 14 | 15 | alias Dialyzer.{Config, Plt, Project} 16 | 17 | @type manifest :: Keyword.t() 18 | @type status :: :up_to_date | :outdated | :missing 19 | 20 | @doc """ 21 | It returns the plt status of the project: 22 | * :missing - when plt / manifest file is missing 23 | * :outdated - when an application / module has been changed 24 | * :up_to_date - when nothing needs to be updated 25 | """ 26 | @spec status(Config.t()) :: status 27 | def status(config) do 28 | cond do 29 | not File.exists?(path()) -> :missing 30 | Plt.missing_plts() != [] -> :missing 31 | not files_changed?(config) -> :up_to_date 32 | true -> :outdated 33 | end 34 | end 35 | 36 | @doc """ 37 | It returns a keyword list with all the changes detected inside 38 | the project. 39 | """ 40 | @spec changes(Config.t()) :: manifest 41 | def changes(config) do 42 | manifest = read_manifest!() 43 | 44 | apps = 45 | all_applications() 46 | |> Kernel.++(Enum.map(config.apps[:include], &Plt.App.info/1)) 47 | |> Kernel.--(Enum.map(config.apps[:remove], &Plt.App.info/1)) 48 | 49 | files_added = files_added(apps, manifest) 50 | files_removed = files_removed(apps, manifest) 51 | files_changed = files_changed(apps, manifest) 52 | 53 | [ 54 | files: [ 55 | added: files_added, 56 | removed: files_removed, 57 | changed: files_changed 58 | ] 59 | ] 60 | end 61 | 62 | @doc """ 63 | It updates the manifest file by saving the current enviroment. 64 | """ 65 | @spec update() :: :ok 66 | def update do 67 | apps = all_applications() 68 | content = [apps: apps] 69 | 70 | write_content = inspect(content, limit: :infinity, printable_limit: :infinity) 71 | File.write!(path(), write_content) 72 | 73 | :ok 74 | end 75 | 76 | @doc """ 77 | It returns the path for the manifest file. 78 | """ 79 | @spec path() :: binary 80 | def path(), do: Plt.Path.project_plt() <> ".manifest" 81 | 82 | @spec files_added([Plt.App.t()], manifest) :: [atom] 83 | defp files_added(apps, manifest) do 84 | apps 85 | |> Stream.map(fn app -> 86 | manifest_app = Enum.find(manifest[:apps], &(&1.app == app.app)) 87 | {app, manifest_app} 88 | end) 89 | |> Stream.filter(fn {_app, manifest_app} -> manifest_app == nil end) 90 | |> Stream.map(fn {app, _manifest_app} -> app end) 91 | |> Stream.flat_map(& &1.mods) 92 | |> Stream.map(& &1.filepath) 93 | |> Enum.to_list() 94 | end 95 | 96 | @spec files_changed([Plt.App.t()], manifest) :: [atom] 97 | defp files_changed(apps, manifest) do 98 | apps 99 | |> Stream.map(fn app -> 100 | manifest_app = Enum.find(manifest[:apps], &(&1.app == app.app)) 101 | {app, manifest_app} 102 | end) 103 | |> Stream.filter(fn {_app, manifest_app} -> manifest_app != nil end) 104 | |> Stream.transform([], fn {app, manifest_app}, acc -> 105 | case app.vsn == manifest_app.vsn do 106 | true -> {filter_modules_changed(app) ++ acc, acc} 107 | false -> {app.mods ++ acc, acc} 108 | end 109 | end) 110 | |> Stream.map(& &1.filepath) 111 | |> Enum.to_list() 112 | end 113 | 114 | @spec files_removed([Plt.App.t()], manifest) :: [atom] 115 | defp files_removed(apps, manifest) do 116 | manifest[:apps] 117 | |> Stream.map(fn manifest_app -> 118 | app = Enum.find(apps, &(&1.app == manifest_app.app)) 119 | {app, manifest_app} 120 | end) 121 | |> Stream.transform([], fn {app, manifest_app}, acc -> 122 | case {app, manifest_app} do 123 | {nil, manifest_app} -> 124 | {manifest_app.mods ++ acc, acc} 125 | 126 | {app, manifest_app} -> 127 | removed_mods = manifest_app.mods -- app.mods 128 | {removed_mods ++ acc, acc} 129 | end 130 | end) 131 | |> Stream.map(& &1.filepath) 132 | |> Enum.to_list() 133 | end 134 | 135 | @spec read_manifest!() :: manifest 136 | defp read_manifest! do 137 | path() 138 | |> Code.eval_file() 139 | |> elem(0) 140 | end 141 | 142 | @spec all_applications() :: [Plt.App.t()] 143 | defp all_applications do 144 | Project.dependencies() 145 | |> Enum.map(&Plt.App.info/1) 146 | |> Enum.filter(&(not is_nil(&1))) 147 | end 148 | 149 | @spec filter_modules_changed(Plt.App.t()) :: [atom] 150 | defp filter_modules_changed(app) do 151 | compiled_mods_with_md5 = 152 | Mix.Utils.extract_files(Project.build_paths(), [:beam]) 153 | |> Enum.reduce(%{}, fn filepath, acc -> 154 | mod = filepath |> to_charlist() |> :beam_lib.info() |> Keyword.fetch!(:module) 155 | md5 = apply(mod, :module_info, [:md5]) 156 | 157 | Map.put(acc, mod, md5) 158 | end) 159 | 160 | Enum.filter(app.mods, fn mod -> 161 | case Map.fetch(compiled_mods_with_md5, mod) do 162 | :error -> 163 | false 164 | 165 | {:ok, hash} -> 166 | mod.md5 != hash 167 | end 168 | end) 169 | end 170 | 171 | @spec files_changed?(Config.t()) :: boolean 172 | defp files_changed?(config) do 173 | changes = changes(config) 174 | changes[:files][:added] != [] or changes[:files][:removed] != [] 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/path.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.Path do 2 | @moduledoc """ 3 | Utility module defining the path of the files used in 4 | the analysis. 5 | 6 | This files are: erlang/elixir/project plts and manifest file. 7 | """ 8 | 9 | @doc """ 10 | Generates an absolute path for the project plt (going to include the elixir version, 11 | erlang, and deps). The path is referring to a location inside the build dir of the project, 12 | since the plt is going to be saved there. 13 | """ 14 | @spec project_plt() :: binary 15 | def project_plt() do 16 | in_build_dir("erlang-#{get_otp_version()}_elixir-#{System.version()}_deps") 17 | end 18 | 19 | @doc """ 20 | It returns the path for the elixir plt. 21 | """ 22 | @spec elixir_plt() :: binary 23 | def elixir_plt() do 24 | in_home_dir("erlang-#{get_otp_version()}_elixir-#{System.version()}") 25 | end 26 | 27 | @doc """ 28 | It returns the path for the erlang plt. 29 | """ 30 | @spec erlang_plt() :: binary 31 | def erlang_plt() do 32 | in_home_dir("erlang-#{get_otp_version()}") 33 | end 34 | 35 | @doc """ 36 | It returns the path for the cache directory stored inside user's home. 37 | """ 38 | @spec home_dir() :: binary 39 | def home_dir do 40 | Path.join([System.user_home(), ".cache", "dialyzer", "plt"]) 41 | end 42 | 43 | @spec get_otp_version() :: String.t() 44 | defp get_otp_version do 45 | "#{System.otp_release()}-erts-#{:erlang.system_info(:version)}" 46 | end 47 | 48 | @spec in_build_dir(String.t()) :: binary 49 | defp in_build_dir(name) do 50 | build_path = Mix.Project.build_path() 51 | plt_name = "dialyzer_#{name}.plt" 52 | Path.join(build_path, plt_name) 53 | end 54 | 55 | @spec in_home_dir(String.t()) :: binary 56 | defp in_home_dir(name) do 57 | home_path = home_dir() 58 | plt_name = "dialyzer_#{name}.plt" 59 | Path.join(home_path, plt_name) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/plt.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt do 2 | @moduledoc """ 3 | This is the main entry point when running an analysis, 4 | it handles the dispatch to the correct function based on system 5 | and plt status. 6 | """ 7 | 8 | alias Dialyzer.{Config, Plt} 9 | import Dialyzer.Logger 10 | 11 | defstruct [:name, :path, :apps] 12 | 13 | @type t :: %Plt{} 14 | 15 | @doc """ 16 | Ensures the plts used by this project are already built and 17 | up to date. It builds them and updates them in case this is not true. 18 | """ 19 | @spec ensure_loaded(Config.t()) :: :ok 20 | def ensure_loaded(config) do 21 | case Plt.Manifest.status(config) do 22 | :up_to_date -> 23 | info("Plt's are all up to date.") 24 | :ok 25 | 26 | :outdated -> 27 | info("Updating outdated plts.") 28 | Plt.Updater.update(config) 29 | :ok 30 | 31 | :missing -> 32 | info("Creating one or more missing plt.") 33 | Plt.Builder.build(config) 34 | Plt.Manifest.update() 35 | :ok 36 | end 37 | end 38 | 39 | @doc """ 40 | It returns which plts are missing, that need to be built. 41 | """ 42 | @spec missing_plts() :: [atom] 43 | def missing_plts do 44 | cond do 45 | not File.exists?(Plt.Path.erlang_plt()) -> [:erlang, :elixir, :project] 46 | not File.exists?(Plt.Path.elixir_plt()) -> [:elixir, :project] 47 | not File.exists?(Plt.Path.project_plt()) -> [:project] 48 | true -> [] 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/dialyzer/plt/updater.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.Updater do 2 | @moduledoc """ 3 | This module is responsable for updating a plt, based on the changes 4 | found by the manifest file. 5 | """ 6 | 7 | alias Dialyzer.{Config, Plt} 8 | 9 | @doc """ 10 | It updates the project plt by analyzing the manifest file, 11 | and updating only the applications that really changed. 12 | """ 13 | @spec update(Config.t()) :: :ok 14 | def update(config) do 15 | changes = Plt.Manifest.changes(config) 16 | plt = Plt.Path.project_plt() 17 | 18 | _ = Plt.Command.remove(plt, changes[:files][:removed]) 19 | _ = Plt.Command.add(plt, changes[:files][:added]) 20 | 21 | Plt.Manifest.update() 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dialyzer/project.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Project do 2 | @doc """ 3 | Get the name of the applications defined at the root. 4 | 5 | In case of umbrella projects, this is a list of all 6 | the applications defined inside `/apps`. 7 | """ 8 | @spec applications() :: [atom] 9 | def applications do 10 | if Mix.Project.umbrella?() do 11 | Mix.Dep.Umbrella.loaded() |> Enum.map(& &1.app) 12 | else 13 | [Mix.Project.get().project()[:app]] 14 | end 15 | end 16 | 17 | @doc """ 18 | Return all the dependencies (direct or indirect) of this project. 19 | """ 20 | @spec dependencies() :: [atom] 21 | def dependencies do 22 | # compile & load all deps paths 23 | Mix.Tasks.Deps.Loadpaths.run([]) 24 | # compile & load current project paths 25 | Mix.Project.compile([]) 26 | 27 | project_deps() 28 | |> Enum.sort() 29 | |> Enum.uniq() 30 | |> Kernel.--(applications()) 31 | end 32 | 33 | @doc """ 34 | Return an array with the absolute paths for all the build paths of this project. 35 | Normally a single element will be returned, since most project just have the _build dir. 36 | """ 37 | @spec build_paths() :: [String.t()] 38 | def build_paths, do: build_paths([]) 39 | 40 | @spec build_paths([String.t()]) :: [String.t()] 41 | defp build_paths(acc) do 42 | if Mix.Project.umbrella?() do 43 | children = Mix.Dep.Umbrella.loaded() 44 | 45 | Enum.reduce(children, acc, fn child, acc -> 46 | Mix.Project.in_project(child.app, child.opts[:path], fn _ -> 47 | build_paths(acc) 48 | end) 49 | end) 50 | else 51 | [Mix.Project.compile_path() | acc] 52 | end 53 | end 54 | 55 | # Works by recursively analyzing all the deps of this project, as well 56 | # as all the deps of deps, returning them as a list of application names. 57 | @spec project_deps() :: [atom] 58 | defp project_deps, do: project_deps([]) 59 | 60 | @spec project_deps([atom]) :: [atom] 61 | defp project_deps(acc) do 62 | if Mix.Project.umbrella?() do 63 | children = Mix.Dep.Umbrella.loaded() 64 | 65 | Enum.reduce(children, acc, fn child, acc -> 66 | Mix.Project.in_project(child.app, child.opts[:path], fn _ -> 67 | project_deps(acc) 68 | end) 69 | end) 70 | else 71 | acc ++ project_core_deps() ++ project_transitive_deps() 72 | end 73 | end 74 | 75 | # Retrieve all the deps to the core elixir. Be careful: this will not return 76 | # project imported dependencies, but just the deps required from the core elixir 77 | # system, such as :kernel, :stdlib and so forth. 78 | @spec project_core_deps() :: [atom] 79 | defp project_core_deps() do 80 | Mix.Project.config() 81 | |> Keyword.get(:app) 82 | |> project_core_deps() 83 | end 84 | 85 | # Load each app and recursively build a list of all the core application deps 86 | # For each app, log also to the user an error in case of incomplete dep list. 87 | @spec project_core_deps(atom()) :: [atom] 88 | defp project_core_deps(app) do 89 | Application.load(app) |> log_app_load_issues(app) 90 | Application.spec(app, :applications) |> find_project_core_deps() 91 | end 92 | 93 | # In case of error during the loading of an app, sometimes the error is due an incomplete 94 | # dependency list, then log it to the user. 95 | # TODO: Improve this doc. 96 | defp log_app_load_issues(:ok, _), do: nil 97 | defp log_app_load_issues({:error, {:already_loaded, _}}, _), do: nil 98 | 99 | defp log_app_load_issues({:error, err}, app), 100 | do: IO.puts("Error loading #{app}, dependency list may be incomplete.\n #{err}") 101 | 102 | # Recursively reduce all applications found 103 | # TODO: Improve this doc. 104 | defp find_project_core_deps([]), do: [] 105 | defp find_project_core_deps(nil), do: [] 106 | 107 | defp find_project_core_deps(apps) do 108 | apps 109 | |> Enum.map(&project_core_deps/1) 110 | |> List.flatten() 111 | |> Enum.concat(apps) 112 | end 113 | 114 | # Return a list of all the transitive dependencies this project has. 115 | # This means direct and indirect dependencies (deps of deps). 116 | @spec project_transitive_deps() :: [atom] 117 | defp project_transitive_deps do 118 | Mix.Project.deps_paths() |> Map.keys() 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/formatter.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter do 6 | import Dialyzer.Logger 7 | alias Dialyzer.Warning 8 | 9 | @doc """ 10 | It gets a list of all the warnings generated by dialyzer and 11 | formats them into nice error messages, with ansi color support. 12 | """ 13 | @spec format(list(), :short | :long) :: [String.t()] 14 | def format(warnings, type) do 15 | Enum.map(warnings, &format_warning(&1, type)) 16 | end 17 | 18 | defp format_warning(warning, type) do 19 | relative_filepath = Path.relative_to_cwd(warning.file) 20 | 21 | try do 22 | warning_formatter = fetch_warning(warning.name) 23 | 24 | case type do 25 | :short -> 26 | header = 27 | case warning.line do 28 | 0 -> relative_filepath 29 | _ -> "#{relative_filepath}:#{warning.line}" 30 | end 31 | 32 | message = warning_formatter.format_short(warning.args) 33 | 34 | "#{color(:cyan, header)} - #{message}" 35 | 36 | :long -> 37 | header = 38 | generate_warning_header(warning_formatter.name(), relative_filepath, warning.line) 39 | 40 | ignore_warning_tuple = Warning.to_ignore_format(warning) 41 | 42 | message = 43 | warning.args 44 | |> warning_formatter.format_long() 45 | |> String.split("\n") 46 | |> Enum.map(&(" " <> &1)) 47 | |> Enum.join("\n") 48 | |> String.trim() 49 | 50 | """ 51 | #{color(:cyan, header)} 52 | 53 | #{message} 54 | 55 | Ignore warning: #{ 56 | inspect(ignore_warning_tuple, limit: :infinity, printable_limit: :infinity) 57 | } 58 | """ 59 | end 60 | rescue 61 | e -> 62 | error("Unknown error occurred: #{inspect(e)}") 63 | catch 64 | {:error, :unknown_warning, warning_name} -> 65 | error("Unknown warning: #{inspect(warning_name)}") 66 | 67 | {:error, :lexing, warning} -> 68 | error("Failed to lex warning: #{inspect(warning)}") 69 | 70 | {:error, :parsing, failing_string} -> 71 | error("Failed to parse warning: #{inspect(failing_string)}") 72 | 73 | {:error, :pretty_printing, failing_string} -> 74 | error("Failed to pretty print warning: #{inspect(failing_string)}") 75 | 76 | {:error, :formatting, code} -> 77 | error("Failed to format warning: #{inspect(code)}") 78 | end 79 | end 80 | 81 | @spec fetch_warning(atom) :: module 82 | defp fetch_warning(warning_name) do 83 | warnings = Dialyzer.Formatter.Warnings.warnings() 84 | 85 | if Map.has_key?(warnings, warning_name) do 86 | Map.get(warnings, warning_name) 87 | else 88 | throw({:error, :unknown_warning, warning_name}) 89 | end 90 | end 91 | 92 | defp generate_warning_header(warning_name, filepath, line_nr) do 93 | warning_fragment = if warning_name != "", do: " #{String.upcase(warning_name)} ", else: "" 94 | 95 | filepath_fragment = 96 | case line_nr do 97 | 0 -> filepath 98 | _ -> " #{filepath}:#{line_nr} " 99 | end 100 | 101 | len = 80 - (String.length(warning_fragment) + String.length(filepath_fragment)) 102 | separators = for _ <- 0..len, into: "", do: "-" 103 | 104 | String.slice(separators, 0..1) 105 | |> Kernel.<>(warning_fragment) 106 | |> Kernel.<>(String.slice(separators, 2..-1)) 107 | |> Kernel.<>(filepath_fragment) 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warning.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warning do 6 | @moduledoc """ 7 | Behaviour for defining warning semantings. 8 | 9 | Contains callbacks for various warnings 10 | """ 11 | 12 | @doc """ 13 | By expressing the warning that is to be matched on, error handlong 14 | and dispatching can be avoided in format functions. 15 | """ 16 | @callback warning() :: atom 17 | 18 | @doc """ 19 | The name of the warning, used to quickly give the user a context. 20 | """ 21 | @callback name() :: String.t() 22 | 23 | @doc """ 24 | A short message, often missing things like success types and expected types for space. 25 | """ 26 | @callback format_short([String.t()] | {String.t(), String.t(), String.t()} | String.t()) :: 27 | String.t() 28 | 29 | @doc """ 30 | The default documentation when seeing an error wihout the user 31 | otherwise overriding the format. 32 | """ 33 | @callback format_long([String.t()] | {String.t(), String.t(), String.t()} | String.t()) :: 34 | String.t() 35 | end 36 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warning_helpers.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.WarningHelpers do 6 | @spec ordinal(non_neg_integer) :: String.t() 7 | 8 | def ordinal(1), do: "1st" 9 | def ordinal(2), do: "2nd" 10 | def ordinal(3), do: "3rd" 11 | def ordinal(n) when is_integer(n), do: "#{n}th" 12 | 13 | def call_or_apply_to_string( 14 | arg_positions, 15 | :only_sig, 16 | signature_args, 17 | _signature_return, 18 | {_overloaded?, _contract} 19 | ) do 20 | pretty_signature_args = Erlex.pretty_print_args(signature_args) 21 | 22 | if Enum.empty?(arg_positions) do 23 | # We do not know which argument(s) caused the failure 24 | """ 25 | will never return since the success typing arguments are 26 | #{pretty_signature_args} 27 | """ 28 | else 29 | positions = form_position_string(arg_positions) 30 | 31 | """ 32 | will never return since it differs in arguments with 33 | positions #{positions} from the success typing arguments: 34 | 35 | #{pretty_signature_args} 36 | """ 37 | end 38 | end 39 | 40 | def call_or_apply_to_string( 41 | arg_positions, 42 | :only_contract, 43 | _signature_args, 44 | _signature_return, 45 | {overloaded?, contract} 46 | ) do 47 | pretty_contract = Erlex.pretty_print_contract(contract) 48 | 49 | if Enum.empty?(arg_positions) or overloaded? do 50 | # We do not know which arguments caused the failure 51 | """ 52 | breaks the contract 53 | #{pretty_contract} 54 | """ 55 | else 56 | position_string = form_position_string(arg_positions) 57 | 58 | """ 59 | breaks the contract 60 | #{pretty_contract} 61 | 62 | in argument 63 | #{position_string} 64 | """ 65 | end 66 | end 67 | 68 | def call_or_apply_to_string( 69 | _arg_positions, 70 | :both, 71 | signature_args, 72 | signature_return, 73 | {_overloaded?, contract} 74 | ) do 75 | pretty_contract = Erlex.pretty_print_contract(contract) 76 | 77 | pretty_print_signature = 78 | Erlex.pretty_print_contract("#{signature_args} -> #{signature_return}") 79 | 80 | """ 81 | will never return since the success typing is: 82 | #{pretty_print_signature} 83 | 84 | and the contract is 85 | #{pretty_contract} 86 | """ 87 | end 88 | 89 | def form_position_string(arg_positions) do 90 | Enum.join(arg_positions, " and ") 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings do 6 | @warnings Enum.into( 7 | [ 8 | Dialyzer.Formatter.Warnings.AppCall, 9 | Dialyzer.Formatter.Warnings.Apply, 10 | Dialyzer.Formatter.Warnings.BinaryConstruction, 11 | Dialyzer.Formatter.Warnings.Call, 12 | Dialyzer.Formatter.Warnings.CallToMissingFunction, 13 | Dialyzer.Formatter.Warnings.CallWithOpaque, 14 | Dialyzer.Formatter.Warnings.CallWithoutOpaque, 15 | Dialyzer.Formatter.Warnings.CallbackArgumentTypeMismatch, 16 | Dialyzer.Formatter.Warnings.CallbackInfoMissing, 17 | Dialyzer.Formatter.Warnings.CallbackMissing, 18 | Dialyzer.Formatter.Warnings.CallbackSpecArgumentTypeMismatch, 19 | Dialyzer.Formatter.Warnings.CallbackSpecTypeMismatch, 20 | Dialyzer.Formatter.Warnings.CallbackTypeMismatch, 21 | Dialyzer.Formatter.Warnings.ContractDiff, 22 | Dialyzer.Formatter.Warnings.ContractSubtype, 23 | Dialyzer.Formatter.Warnings.ContractSupertype, 24 | Dialyzer.Formatter.Warnings.ContractWithOpaque, 25 | Dialyzer.Formatter.Warnings.ExactEquality, 26 | Dialyzer.Formatter.Warnings.ExtraRange, 27 | Dialyzer.Formatter.Warnings.FuncionApplicationArguments, 28 | Dialyzer.Formatter.Warnings.FunctionApplicationNoFunction, 29 | Dialyzer.Formatter.Warnings.GuardFail, 30 | Dialyzer.Formatter.Warnings.GuardFailPattern, 31 | Dialyzer.Formatter.Warnings.ImproperListConstruction, 32 | Dialyzer.Formatter.Warnings.InvalidContract, 33 | Dialyzer.Formatter.Warnings.NegativeGuardFail, 34 | Dialyzer.Formatter.Warnings.NoReturn, 35 | Dialyzer.Formatter.Warnings.OpaqueGuard, 36 | Dialyzer.Formatter.Warnings.OpaqueEquality, 37 | Dialyzer.Formatter.Warnings.OpaqueMatch, 38 | Dialyzer.Formatter.Warnings.OpaqueNonequality, 39 | Dialyzer.Formatter.Warnings.OpaqueTypeTest, 40 | Dialyzer.Formatter.Warnings.OverlappingContract, 41 | Dialyzer.Formatter.Warnings.PatternMatch, 42 | Dialyzer.Formatter.Warnings.PatternMatchCovered, 43 | Dialyzer.Formatter.Warnings.RaceCondition, 44 | Dialyzer.Formatter.Warnings.RecordConstruction, 45 | Dialyzer.Formatter.Warnings.RecordMatching, 46 | Dialyzer.Formatter.Warnings.UnknownBehaviour, 47 | Dialyzer.Formatter.Warnings.UnknownFunction, 48 | Dialyzer.Formatter.Warnings.UnknownType, 49 | Dialyzer.Formatter.Warnings.UnmatchedReturn, 50 | Dialyzer.Formatter.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/dialyzer/warnings/formatter/warnings/app_call.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.AppCall do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :app_call 10 | def warning(), do: :app_call 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short([module, function, arity, _culprit, _expected_type, _actual_type]) do 19 | pretty_module = Erlex.pretty_print(module) 20 | 21 | "The call #{pretty_module}.#{function}/#{arity} has a type mismatch." 22 | end 23 | 24 | @impl Dialyzer.Formatter.Warning 25 | @spec format_long([String.t()]) :: String.t() 26 | def format_long([module, function, arity, culprit, expected_type, actual_type]) do 27 | pretty_module = Erlex.pretty_print(module) 28 | pretty_expected_type = Erlex.pretty_print_type(expected_type) 29 | pretty_actual_type = Erlex.pretty_print_type(actual_type) 30 | 31 | """ 32 | The call #{pretty_module}.#{function}/#{arity} requires that 33 | #{culprit} is of type #{pretty_expected_type} not #{pretty_actual_type} 34 | """ 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/apply.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.Apply do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :apply 10 | def warning(), do: :apply 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined function call" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Function call will not succeed." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([args, arg_positions, fail_reason, signature_args, signature_return, contract]) do 25 | pretty_args = Erlex.pretty_print_args(args) 26 | 27 | call_string = 28 | Dialyzer.Formatter.WarningHelpers.call_or_apply_to_string( 29 | arg_positions, 30 | fail_reason, 31 | signature_args, 32 | signature_return, 33 | contract 34 | ) 35 | 36 | """ 37 | The function being invoked exists, and has the correct arity, but 38 | will not succeed, because there is a mismatch with the arguments. 39 | 40 | #{pretty_args} #{call_string} 41 | """ 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/binary_construction.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.BinaryConstruction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :bin_construction 10 | def warning(), do: :bin_construction 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Binary construction will fail." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([culprit, size, segment, type]) do 25 | pretty_type = Erlex.pretty_print_type(type) 26 | 27 | """ 28 | Binary construction will fail since the #{culprit} field #{size} in 29 | segment #{segment} has type #{pretty_type}. 30 | """ 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/call.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.Call do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :call 10 | def warning(), do: :call 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined function call" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "The function call will fail." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([ 25 | module, 26 | function, 27 | args, 28 | arg_positions, 29 | fail_reason, 30 | signature_args, 31 | signature_return, 32 | contract 33 | ]) do 34 | pretty_args = Erlex.pretty_print_args(args) 35 | pretty_module = Erlex.pretty_print(module) 36 | 37 | call_string = 38 | Dialyzer.Formatter.WarningHelpers.call_or_apply_to_string( 39 | arg_positions, 40 | fail_reason, 41 | signature_args, 42 | signature_return, 43 | contract 44 | ) 45 | 46 | """ 47 | The function being invoked exists, and has the correct arity, but 48 | will not succeed, because there is a mismatch with the arguments. 49 | 50 | #{pretty_module}.#{function}#{pretty_args} 51 | 52 | #{String.trim_trailing(call_string)} 53 | """ 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/call_to_missing_function.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallToMissingFunction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :call_to_missing 10 | def warning(), do: :call_to_missing 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined function call" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short([module, function, arity]) do 19 | pretty_module = Erlex.pretty_print(module) 20 | 21 | "Call to missing or private function #{pretty_module}.#{function}/#{arity}." 22 | end 23 | 24 | @impl Dialyzer.Formatter.Warning 25 | @spec format_long([String.t()]) :: String.t() 26 | def format_long([module, function, arity]) do 27 | pretty_module = Erlex.pretty_print(module) 28 | 29 | """ 30 | Call to missing or private function. May be a typo, or 31 | incorrect arity. 32 | 33 | #{pretty_module}.#{function}/#{arity}. 34 | """ 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/call_with_opaque.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallWithOpaque do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :call_with_opaque 10 | def warning(), do: :call_with_opaque 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Call with opaqueness type mismatch." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([module, function, args, arg_positions, expected_args]) do 25 | pretty_module = Erlex.pretty_print(module) 26 | 27 | """ 28 | The call #{pretty_module}.#{function}#{args} contains #{form_positions(arg_positions)} 29 | when #{form_expected(expected_args)}}. 30 | """ 31 | end 32 | 33 | defp form_positions(arg_positions = [_]) do 34 | form_position_string = Dialyzer.Formatter.WarningHelpers.form_position_string(arg_positions) 35 | "an opaque term in #{form_position_string} argument" 36 | end 37 | 38 | defp form_positions(arg_positions) do 39 | form_position_string = Dialyzer.Formatter.WarningHelpers.form_position_string(arg_positions) 40 | "opaque terms in #{form_position_string} arguments" 41 | end 42 | 43 | defp form_expected([type]) do 44 | type_string = :erl_types.t_to_string(type) 45 | 46 | if :erl_types.t_is_opaque(type) do 47 | "an opaque term of type #{type_string} is expected" 48 | else 49 | "a structured term of type #{type_string} is expected" 50 | end 51 | end 52 | 53 | defp form_expected(_expected_args) do 54 | "terms of different types are expected in these positions" 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/call_without_opaque.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallWithoutOpaque do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :call_without_opaque 10 | def warning(), do: :call_without_opaque 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Call without opaqueness type mismatch." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([module, function, args, expected_triples]) do 25 | pretty_module = Erlex.pretty_print(module) 26 | 27 | "The call #{pretty_module}.#{function}#{args} does not have #{ 28 | form_expected_without_opaque(expected_triples) 29 | }." 30 | end 31 | 32 | # We know which positions N are to blame; 33 | # the list of triples will never be empty. 34 | defp form_expected_without_opaque([{position, type, type_string}]) do 35 | form_position_string = Dialyzer.Formatter.WarningHelpers.form_position_string([position]) 36 | 37 | message = 38 | if :erl_types.t_is_opaque(type) do 39 | "an opaque term of type #{type_string} in " 40 | else 41 | "a term of type #{type_string} (with opaque subterms) in " 42 | end 43 | 44 | message <> form_position_string 45 | end 46 | 47 | # TODO: can do much better here 48 | defp form_expected_without_opaque(expected_triples) do 49 | {arg_positions, _typess, _type_strings} = :lists.unzip3(expected_triples) 50 | form_position_string = Dialyzer.Formatter.WarningHelpers.form_position_string(arg_positions) 51 | "opaque terms in #{form_position_string}" 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/callback_argument_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallbackArgumentTypeMismatch do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :callback_arg_type_mismatch 11 | def warning(), do: :callback_arg_type_mismatch 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([behaviour, function, arity, position, _success_type, _callback_type]) do 20 | pretty_behaviour = Erlex.pretty_print(behaviour) 21 | ordinal_position = Dialyzer.Formatter.WarningHelpers.ordinal(position) 22 | 23 | "Type mismatch in #{ordinal_position} argument for #{function}/#{arity}" <> 24 | " callback in #{pretty_behaviour} behaviour." 25 | end 26 | 27 | @impl Dialyzer.Formatter.Warning 28 | @spec format_long([String.t()]) :: String.t() 29 | def format_long([behaviour, function, arity, position, success_type, callback_type]) do 30 | pretty_behaviour = Erlex.pretty_print(behaviour) 31 | pretty_success_type = Erlex.pretty_print_type(success_type) 32 | pretty_callback_type = Erlex.pretty_print_type(callback_type) 33 | ordinal_position = Dialyzer.Formatter.WarningHelpers.ordinal(position) 34 | 35 | """ 36 | The inferred type for the #{ordinal_position} argument is not a 37 | supertype of the expected type for the #{function}/#{arity} callback 38 | in the #{pretty_behaviour} behaviour. 39 | 40 | #{color(:yellow, "Success type:")} 41 | #{pretty_success_type} 42 | 43 | #{color(:yellow, "Behaviour callback type:")} 44 | #{pretty_callback_type} 45 | """ 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/callback_info_missing.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallbackInfoMissing do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :callback_info_missing 10 | def warning(), do: :callback_info_missing 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined behaviour" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short([behaviour]) do 19 | pretty_behaviour = Erlex.pretty_print(behaviour) 20 | 21 | "Callback info about the #{pretty_behaviour} behaviour is not available." 22 | end 23 | 24 | @impl Dialyzer.Formatter.Warning 25 | @spec format_long([String.t()]) :: String.t() 26 | def format_long([behaviour]) do 27 | pretty_behaviour = Erlex.pretty_print(behaviour) 28 | 29 | """ 30 | The module is using a behaviour that does not exist or is not a 31 | behaviour. 32 | 33 | Undefined: #{pretty_behaviour} 34 | """ 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/callback_missing.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallbackMissing do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :callback_missing 10 | def warning(), do: :callback_missing 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined callback" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short([behaviour, function, arity]) do 19 | pretty_behaviour = Erlex.pretty_print(behaviour) 20 | 21 | "Undefined callback function #{function}/#{arity} (behaviour #{pretty_behaviour})." 22 | end 23 | 24 | @impl Dialyzer.Formatter.Warning 25 | @spec format_long([String.t()]) :: String.t() 26 | def format_long([behaviour, function, arity]) do 27 | pretty_behaviour = Erlex.pretty_print(behaviour) 28 | 29 | """ 30 | Module implements a behaviour but does not have all of its 31 | callbacks. 32 | 33 | #{function}/#{arity} (behaviour #{pretty_behaviour}) 34 | """ 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/callback_spec_argument_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallbackSpecArgumentTypeMismatch do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :callback_spec_arg_type_mismatch 11 | def warning(), do: :callback_spec_arg_type_mismatch 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "spec mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([behaviour, function, arity, position, _success_type, _callback_type]) do 20 | pretty_behaviour = Erlex.pretty_print(behaviour) 21 | ordinal_position = Dialyzer.Formatter.WarningHelpers.ordinal(position) 22 | 23 | """ 24 | Spec type mismatch in #{ordinal_position} argument for #{function}/#{arity} 25 | callback in #{pretty_behaviour} behaviour. 26 | """ 27 | end 28 | 29 | @impl Dialyzer.Formatter.Warning 30 | @spec format_long([String.t()]) :: String.t() 31 | def format_long([behaviour, function, arity, position, success_type, callback_type]) do 32 | pretty_behaviour = Erlex.pretty_print(behaviour) 33 | pretty_success_type = Erlex.pretty_print_type(success_type) 34 | pretty_callback_type = Erlex.pretty_print_type(callback_type) 35 | ordinal_position = Dialyzer.Formatter.WarningHelpers.ordinal(position) 36 | 37 | """ 38 | The @spec type for the #{ordinal_position} argument is not a 39 | supertype of the expected type for the #{function}/#{arity} callback 40 | in the #{pretty_behaviour} behaviour. 41 | 42 | #{color(:yellow, "Success type:")} 43 | #{pretty_success_type} 44 | 45 | #{color(:yellow, "Behaviour callback type:")} 46 | #{pretty_callback_type} 47 | """ 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/callback_spec_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallbackSpecTypeMismatch do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :callback_spec_type_mismatch 11 | def warning(), do: :callback_spec_type_mismatch 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "spec mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([behaviour, function, arity, _success_type, _callback_type]) do 20 | pretty_behaviour = Erlex.pretty_print(behaviour) 21 | 22 | "The @spec return type for does not match the expected return type" <> 23 | "for #{function}/#{arity} callback in #{pretty_behaviour} behaviour." 24 | end 25 | 26 | @impl Dialyzer.Formatter.Warning 27 | @spec format_long([String.t()]) :: String.t() 28 | def format_long([behaviour, function, arity, success_type, callback_type]) do 29 | pretty_behaviour = Erlex.pretty_print(behaviour) 30 | pretty_success_type = Erlex.pretty_print_type(success_type) 31 | pretty_callback_type = Erlex.pretty_print_type(callback_type) 32 | 33 | """ 34 | The @spec return type for does not match the expected return type 35 | for #{function}/#{arity} callback in #{pretty_behaviour} behaviour. 36 | 37 | #{color(:yellow, "Actual:")} 38 | @spec #{function}(...) :: #{pretty_success_type} 39 | 40 | #{color(:yellow, "Expected:")} 41 | @spec #{function}(...) :: #{pretty_callback_type} 42 | """ 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/callback_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.CallbackTypeMismatch do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :callback_type_mismatch 11 | def warning(), do: :callback_type_mismatch 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "callback mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([behaviour, function, arity, _fail_type, _success_type]) do 20 | pretty_behaviour = Erlex.pretty_print(behaviour) 21 | "Callback mismatch for @callback #{function}/#{arity} in #{pretty_behaviour} behaviour." 22 | end 23 | 24 | @impl Dialyzer.Formatter.Warning 25 | @spec format_long([String.t() | non_neg_integer]) :: String.t() 26 | def format_long([behaviour, function, arity, fail_type, success_type]) do 27 | pretty_behaviour = Erlex.pretty_print(behaviour) 28 | pretty_fail_type = Erlex.pretty_print_type(fail_type) 29 | pretty_success_type = Erlex.pretty_print_type(success_type) 30 | 31 | """ 32 | The success type of the function does not match the callback type 33 | in behaviour: #{function}/#{arity} in #{pretty_behaviour} 34 | 35 | #{color(:yellow, "Actual:")} 36 | #{pretty_fail_type} 37 | 38 | #{color(:yellow, "Expected:")} 39 | #{pretty_success_type} 40 | """ 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/contract_diff.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ContractDiff do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :contract_diff 11 | def warning(), do: :contract_diff 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "Type specification is not equal to the success typing." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([module, function, arity, contract, signature]) do 26 | pretty_module = Erlex.pretty_print(module) 27 | pretty_contract = Erlex.pretty_print_type(contract) 28 | pretty_signature = Erlex.pretty_print_type(signature) 29 | 30 | """ 31 | Type specification is not equal to the success typing. 32 | 33 | #{color(:yellow, "Function:")} 34 | #{pretty_module}.#{function}/#{arity} 35 | 36 | #{color(:yellow, "Type specification:")} 37 | #{pretty_contract} 38 | 39 | #{color(:yellow, "Success typing:")} 40 | #{pretty_signature} 41 | """ 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/contract_subtype.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ContractSubtype do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :contract_subtype 11 | def warning(), do: :contract_subtype 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "Type specification is a subtype of the success typing." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([module, function, arity, contract, signature]) do 26 | pretty_module = Erlex.pretty_print(module) 27 | pretty_signature = Erlex.pretty_print_contract(signature) 28 | 29 | pretty_contract = Erlex.pretty_print_contract(contract, module, function) 30 | 31 | """ 32 | The type in the @spec does not completely cover the types returned 33 | by function. 34 | 35 | #{color(:yellow, "Function:")} 36 | #{pretty_module}.#{function}/#{arity} 37 | 38 | #{color(:yellow, "Type specification:")} 39 | @spec #{function}#{pretty_contract} 40 | 41 | #{color(:yellow, "Success typing:")} 42 | @spec #{function}#{pretty_signature} 43 | """ 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/contract_supertype.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ContractSupertype do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :contract_supertype 11 | def warning(), do: :contract_supertype 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "Type specification is a supertype of the success typing." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([module, function, arity, contract, signature]) do 26 | pretty_module = Erlex.pretty_print(module) 27 | pretty_contract = Erlex.pretty_print_contract(contract) 28 | pretty_signature = Erlex.pretty_print_contract(signature) 29 | 30 | """ 31 | The @spec, while not incorrect, is more general than the type 32 | returned by the function. 33 | 34 | #{color(:yellow, "Function:")} 35 | #{pretty_module}.#{function}/#{arity} 36 | 37 | #{color(:yellow, "Type specification:")} 38 | @spec #{function}#{pretty_contract} 39 | 40 | #{color(:yellow, "Success typing:")} 41 | @spec #{function}#{pretty_signature} 42 | """ 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/contract_with_opaque.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ContractWithOpaque do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :contract_with_opaque 11 | def warning(), do: :contract_with_opaque 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "The @spec has an opaque subtype which is violated by the success typing" 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([module, function, arity, type, signature_type]) do 26 | pretty_module = Erlex.pretty_print(module) 27 | pretty_type = Erlex.pretty_print_type(type) 28 | pretty_success_type = Erlex.pretty_print_type(signature_type) 29 | 30 | """ 31 | The @spec says the function is returning an opaque type but it is 32 | returning a different type. 33 | 34 | #{color(:yellow, "Type specification:")} 35 | #{pretty_module}.#{function}/#{arity} declared this opaque type: #{pretty_type} 36 | 37 | #{color(:yellow, "Success typing:")} 38 | #{pretty_success_type} 39 | """ 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/exact_equality.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ExactEquality do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :exact_eq 10 | def warning(), do: :exact_eq 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "value mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Expression can never evaluate to true." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([type1, op, type2]) do 25 | pretty_type1 = Erlex.pretty_print_type(type1) 26 | pretty_type2 = Erlex.pretty_print_type(type2) 27 | 28 | """ 29 | The expression #{pretty_type1} #{op} #{pretty_type2} can never evaluate to 'true'. 30 | """ 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/extra_range.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ExtraRange do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :extra_range 11 | def warning(), do: :extra_range 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([module, function, arity, _extra_ranges, _signature_range]) do 20 | pretty_module = Erlex.pretty_print(module) 21 | 22 | "@spec for #{pretty_module}.#{function}/#{arity} has more types " <> 23 | "than returned by function." 24 | end 25 | 26 | @impl Dialyzer.Formatter.Warning 27 | @spec format_long([String.t()]) :: String.t() 28 | def format_long([module, function, arity, extra_ranges, signature_range]) do 29 | pretty_module = Erlex.pretty_print(module) 30 | pretty_extra = Erlex.pretty_print_type(extra_ranges) 31 | pretty_signature = Erlex.pretty_print_type(signature_range) 32 | 33 | """ 34 | The @spec says the function returns more types than the function actually returns. 35 | 36 | #{color(:yellow, "Function:")} 37 | #{pretty_module}.#{function}/#{arity} 38 | 39 | #{color(:yellow, "Extra type:")} 40 | #{pretty_extra} 41 | 42 | #{color(:yellow, "Success typing:")} 43 | #{pretty_signature} 44 | """ 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/function_application_arguments.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.FuncionApplicationArguments do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :fun_app_args 10 | def warning(), do: :fun_app_args 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Wrong types for function call." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([args, type]) do 25 | pretty_args = Erlex.pretty_print_args(args) 26 | pretty_type = Erlex.pretty_print(type) 27 | 28 | "Function call with arguments #{pretty_args} will fail " <> 29 | "since the function has type #{pretty_type}." 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/function_application_no_function.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.FunctionApplicationNoFunction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :fun_app_no_fun 10 | def warning(), do: :fun_app_no_fun 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined function call" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Wrong number of arguments for function call." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([op, type, arity]) do 25 | pretty_op = Erlex.pretty_print(op) 26 | pretty_type = Erlex.pretty_print_type(type) 27 | 28 | """ 29 | The function being invoked exists has an arity mismatch. 30 | 31 | #{pretty_op} :: #{pretty_type} does not accept #{arity} arguments. 32 | """ 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/guard_fail.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.GuardFail do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :guard_fail 11 | def warning(), do: :guard_fail 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "pattern mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "Guard test can never succeed because of a pattern mismatch." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([]) do 26 | "Guard test can never succeed because of a pattern mismatch." 27 | end 28 | 29 | def format_long([guard, args]) do 30 | pretty_args = Erlex.pretty_print_args(args) 31 | 32 | """ 33 | #{color(:yellow, "Guard test:")} 34 | #{guard}#{pretty_args} 35 | 36 | can never succeed. 37 | """ 38 | end 39 | 40 | def format_long([arg1, infix, arg2]) do 41 | pretty_arg1 = Erlex.pretty_print_type(arg1) 42 | pretty_arg2 = Erlex.pretty_print_args(arg2) 43 | pretty_infix = Erlex.pretty_print_infix(infix) 44 | 45 | """ 46 | The function guard either presents an impossible guard or the only 47 | calls will never succeed against the guards. 48 | 49 | #{color(:yellow, "Guard test:")} 50 | #{pretty_arg1} 51 | 52 | #{pretty_infix} 53 | 54 | #{pretty_arg2} 55 | 56 | can never succeed. 57 | """ 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/guard_fail_pattern.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.GuardFailPattern do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :guard_fail_pat 10 | def warning(), do: :guard_fail_pat 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "pattern mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Clause guard cannot succeed because of a pattern mismatch." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([pattern, type]) do 25 | pretty_type = Erlex.pretty_print_type(type) 26 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 27 | 28 | """ 29 | The guard describes a condition of literals that fails the pattern 30 | given in function head. 31 | 32 | The pattern #{pretty_pattern} was matched against the type #{pretty_type} 33 | """ 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/improper_list_construction.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.ImproperListConstruction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :improper_list_constr 10 | def warning(), do: :improper_list_constr 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(args), do: format_long(args) 19 | 20 | @impl Dialyzer.Formatter.Warning 21 | @spec format_long([String.t()]) :: String.t() 22 | def format_long([tl_type]) do 23 | pretty_type = Erlex.pretty_print_type(tl_type) 24 | 25 | "Cons will produce an improper list since its 2nd argument is #{pretty_type}." 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/invalid_contract.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.InvalidContract do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :invalid_contract 11 | def warning(), do: :invalid_contract 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "type mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([module, function, arity, _signature]) do 20 | pretty_module = Erlex.pretty_print(module) 21 | 22 | "Invalid type specification for function #{pretty_module}.#{function}/#{arity}." 23 | end 24 | 25 | @impl Dialyzer.Formatter.Warning 26 | @spec format_long([String.t()]) :: String.t() 27 | def format_long([module, function, arity, signature]) do 28 | pretty_module = Erlex.pretty_print(module) 29 | pretty_signature = Erlex.pretty_print_contract(signature) 30 | 31 | """ 32 | The @spec for the function does not match the success typing of 33 | the function. 34 | 35 | #{color(:yellow, "Function:")} 36 | #{pretty_module}.#{function}/#{arity} 37 | 38 | #{color(:yellow, "Success typing:")} 39 | @spec #{function}#{pretty_signature} 40 | """ 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/negative_guard_fail.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.NegativeGuardFail do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :neg_guard_fail 11 | def warning(), do: :neg_guard_fail 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "pattern mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "Guard test can never succeed." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([guard, args]) do 26 | pretty_args = Erlex.pretty_print_args(args) 27 | 28 | """ 29 | The function guard either presents an impossible guard or the only 30 | calls will never succeed against the guards. 31 | 32 | #{color(:yellow, "Guard test:")} 33 | not #{guard}#{pretty_args} 34 | 35 | can never succeed. 36 | """ 37 | end 38 | 39 | def format_long([arg1, infix, arg2]) do 40 | pretty_infix = Erlex.pretty_print_infix(infix) 41 | 42 | """ 43 | The function guard either presents an impossible guard or the only 44 | calls will never succeed against the guards. 45 | 46 | #{color(:yellow, "Guard test:")} 47 | not #{arg1} #{pretty_infix} #{arg2} 48 | 49 | can never succeed. 50 | """ 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/no_return.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.NoReturn do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :no_return 10 | def warning(), do: :no_return 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "no return" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "The function has no return" 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t() | atom]) :: String.t() 24 | def format_long([type | name]) do 25 | name_string = 26 | case name do 27 | [] -> 28 | "The created fun" 29 | 30 | [function, arity] -> 31 | "Function #{function}/#{arity}" 32 | end 33 | 34 | type_string = 35 | case type do 36 | :no_match -> 37 | "has no clauses that will ever match." 38 | 39 | :only_explicit -> 40 | "only terminates with explicit exception." 41 | 42 | :only_normal -> 43 | "has no local return." 44 | 45 | :both -> 46 | "has no local return." 47 | end 48 | 49 | """ 50 | The function has no return. This is usually due to an issue later 51 | on in the call stack causing it to not be recognized as returning 52 | for some reason. It is often helpful to cross reference the 53 | complete list of warnings with the call stack in the function and 54 | fix the deepest part of the call stack, which will usually fix 55 | many of the other no_return errors. 56 | 57 | #{name_string} #{type_string} 58 | """ 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/opaque_equality.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.OpaqueEquality do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :opaque_eq 10 | def warning(), do: :opaque_eq 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Attempt to test for equality with an opaque type." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([type, _op, opaque_type]) do 25 | """ 26 | Attempt to test for equality between a term of type #{type} 27 | and a term of opaque type #{opaque_type}. 28 | """ 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/opaque_guard.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.OpaqueGuard do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :opaque_guard 10 | def warning(), do: :opaque_guard 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Guard test breaks the opaqueness of its argument." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([guard, args]) do 25 | "Guard test #{guard}#{args} breaks the opaqueness of its argument." 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/opaque_match.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.OpaqueMatch do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :opaque_match 10 | def warning(), do: :opaque_match 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Attempted to match against opaque term." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([pattern, opaque_type, opaque_term]) do 25 | term = 26 | if opaque_type == opaque_term do 27 | "the term" 28 | else 29 | opaque_term 30 | end 31 | 32 | pretty_pattern = Erlex.pretty_print(pattern) 33 | 34 | """ 35 | The attempt to match a term of type #{opaque_term} against the #{pretty_pattern} 36 | breaks the opaqueness of #{term}. 37 | """ 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/opaque_nonequality.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.OpaqueNonequality do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :opaque_neq 10 | def warning(), do: :opaque_neq 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Attempted to test for inequality between a type and an opaque type." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([type, _op, opaque_type]) do 25 | """ 26 | Attempt to test for inequality between a term of type #{type} 27 | and a term of opaque type #{opaque_type}. 28 | """ 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/opaque_type_test.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.OpaqueTypeTest do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :opaque_type_test 10 | def warning(), do: :opaque_type_test 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "The type test breaks the opaqueness of the term." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([function, opaque]) do 25 | "The type test #{function}(#{opaque}) breaks the opaqueness of the term #{opaque}." 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/overlapping_contract.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.OverlappingContract do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :overlapping_contract 11 | def warning(), do: :overlapping_contract 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "unreachable block" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short([module, function, arity]) do 20 | pretty_module = Erlex.pretty_print(module) 21 | 22 | "Overloaded contract for #{pretty_module}.#{function}/#{arity}" 23 | end 24 | 25 | @impl Dialyzer.Formatter.Warning 26 | @spec format_long([String.t()]) :: String.t() 27 | def format_long([module, function, arity]) do 28 | pretty_module = Erlex.pretty_print(module) 29 | 30 | """ 31 | The function has an additional @spec that is already covered more 32 | generally by a higher @spec. 33 | 34 | #{color(:yellow, "Actual:")} 35 | #{pretty_module}.#{function}/#{arity} 36 | """ 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/pattern_match.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.PatternMatch do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :pattern_match 11 | def warning(), do: :pattern_match 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "pattern mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "The pattern can never match the type." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([pattern, type]) do 26 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 27 | pretty_type = Erlex.pretty_print_type(type) 28 | 29 | """ 30 | The pattern matching is never given a value that satisfies all of 31 | its clauses. 32 | 33 | #{color(:yellow, "The pattern")} 34 | #{pretty_pattern} 35 | 36 | #{color(:yellow, "can never match the type")} 37 | #{pretty_type} 38 | """ 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/pattern_match_covered.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.PatternMatchCovered do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :pattern_match_cov 11 | def warning(), do: :pattern_match_cov 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "unreachable block" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "The pattern can never match the type since it covered by previous clauses." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([pattern, type]) do 26 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 27 | pretty_type = Erlex.pretty_print_type(type) 28 | 29 | """ 30 | The pattern match has a later clause that will never be executed 31 | because a more general clause is higher in the matching order. 32 | 33 | #{color(:yellow, "Unreachable pattern:")} 34 | #{pretty_pattern} 35 | 36 | #{color(:yellow, "Because covered by:")} 37 | #{pretty_type} 38 | """ 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/race_condition.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.RaceCondition do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :race_condition 10 | def warning(), do: :race_condition 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "race condition" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(args), do: format_long(args) 19 | 20 | @impl Dialyzer.Formatter.Warning 21 | @spec format_long([String.t()]) :: String.t() 22 | def format_long([module, function, args, reason]) do 23 | pretty_args = Erlex.pretty_print_args(args) 24 | pretty_module = Erlex.pretty_print(module) 25 | 26 | "The call #{pretty_module},#{function}#{pretty_args} #{reason}." 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/record_construction.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.RecordConstruction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :record_constr 10 | def warning(), do: :record_constr 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(_) do 19 | "Record construction violates the declared type." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([types, name]) do 25 | "Record construction #{types} violates the declared type for ##{name}{}." 26 | end 27 | 28 | def format_long([name, field, type]) do 29 | """ 30 | Record construction violates the declared type for ##{name}{} since 31 | #{field} cannot be of type #{type}. 32 | """ 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/record_matching.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.RecordMatching do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :record_matching 10 | def warning(), do: :record_matching 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "type mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short(args), do: format_long(args) 19 | 20 | @impl Dialyzer.Formatter.Warning 21 | @spec format_long([String.t()]) :: String.t() 22 | def format_long([string, name]) do 23 | "The #{string} violates the declared type for ##{name}{}." 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/unknown_behaviour.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.UnknownBehaviour do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :unknown_behaviour 10 | def warning(), do: :unknown_behaviour 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined behaviour" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short(String.t()) :: String.t() 18 | def format_short(args), do: format_long(args) 19 | 20 | @impl Dialyzer.Formatter.Warning 21 | @spec format_long(String.t()) :: String.t() 22 | def format_long(behaviour) do 23 | pretty_module = Erlex.pretty_print(behaviour) 24 | 25 | "Unknown behaviour: #{pretty_module}." 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/unknown_function.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.UnknownFunction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :unknown_function 10 | def warning(), do: :unknown_function 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "undefined function" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short({String.t(), String.t(), String.t()}) :: String.t() 18 | def format_short(args), do: format_long(args) 19 | 20 | @impl Dialyzer.Formatter.Warning 21 | @spec format_long({String.t(), String.t(), String.t()}) :: String.t() 22 | def format_long({module, function, arity}) do 23 | pretty_module = Erlex.pretty_print(module) 24 | "Function #{pretty_module}.#{function}/#{arity} does not exist." 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/unknown_type.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.UnknownType do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :unknown_type 10 | def warning(), do: :unknown_type 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "spec mismatch" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short({String.t(), String.t(), String.t()}) :: String.t() 18 | def format_short(args), do: format_long(args) 19 | 20 | @impl Dialyzer.Formatter.Warning 21 | @spec format_long({String.t(), String.t(), String.t()}) :: String.t() 22 | def format_long({module, function, arity}) do 23 | pretty_module = Erlex.pretty_print(module) 24 | 25 | "Spec references a missing @type: #{pretty_module}.#{function}/#{arity}." 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/unmatched_return.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.UnmatchedReturn do 6 | @behaviour Dialyzer.Formatter.Warning 7 | import Dialyzer.Logger, only: [color: 2] 8 | 9 | @impl Dialyzer.Formatter.Warning 10 | @spec warning() :: :unmatched_return 11 | def warning(), do: :unmatched_return 12 | 13 | @impl Dialyzer.Formatter.Warning 14 | @spec name() :: String.t() 15 | def name(), do: "pattern mismatch" 16 | 17 | @impl Dialyzer.Formatter.Warning 18 | @spec format_short([String.t()]) :: String.t() 19 | def format_short(_) do 20 | "Expression produces multiple types but none are matched." 21 | end 22 | 23 | @impl Dialyzer.Formatter.Warning 24 | @spec format_long([String.t()]) :: String.t() 25 | def format_long([type]) do 26 | pretty_type = Erlex.pretty_print_type(type) 27 | 28 | """ 29 | The invoked expression returns a union of types and the call does 30 | not match on its return types using e.g. a case or wildcard. 31 | 32 | #{color(:yellow, "Value produced:")} 33 | #{pretty_type} 34 | 35 | but this value is unmatched. 36 | """ 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/formatter/warnings/unused_function.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyxir` project 2 | # Copyright by Andrew Summers 3 | # https://github.com/jeremyjh/dialyxir 4 | 5 | defmodule Dialyzer.Formatter.Warnings.UnusedFunction do 6 | @behaviour Dialyzer.Formatter.Warning 7 | 8 | @impl Dialyzer.Formatter.Warning 9 | @spec warning() :: :unused_fun 10 | def warning(), do: :unused_fun 11 | 12 | @impl Dialyzer.Formatter.Warning 13 | @spec name() :: String.t() 14 | def name(), do: "unreachable block" 15 | 16 | @impl Dialyzer.Formatter.Warning 17 | @spec format_short([String.t()]) :: String.t() 18 | def format_short([function, arity]) do 19 | "Function #{function}/#{arity} will never be called." 20 | end 21 | 22 | @impl Dialyzer.Formatter.Warning 23 | @spec format_long([String.t()]) :: String.t() 24 | def format_long([function, arity]) do 25 | """ 26 | Due to issues higher in the function or call stack, while the 27 | function is recognized as used by the compiler, it will never be 28 | recognized as having been called until the other error is 29 | resolved. 30 | 31 | Function #{function}/#{arity} will never be called. 32 | """ 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/warning.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Warning do 2 | @moduledoc """ 3 | It represents a dialyzer warning. When taking the original warning, 4 | it takes also care of setting eventually default values. 5 | """ 6 | 7 | defstruct [:type, :file, :line, :name, :args] 8 | alias __MODULE__ 9 | 10 | @type t :: %__MODULE__{} 11 | 12 | @spec new({atom, {binary, integer}, {atom, list}}) :: t 13 | def new({type, {file, line}, {name, args}}) do 14 | file = if file != '', do: file, else: :code.which(elem(args, 0)) 15 | %Warning{type: type, file: to_string(file), line: line, name: name, args: args} 16 | end 17 | 18 | @doc """ 19 | It returns a tuple, from the original dialyzer warning, that can 20 | be used to ignore the specific warning, through `.dialyzer.exs`. 21 | """ 22 | @spec to_ignore_format(t) :: {String.t(), integer, atom} 23 | def to_ignore_format(warning) do 24 | file = if warning.file == "", do: :*, else: warning.file 25 | line = if warning.line == 0, do: :*, else: warning.line 26 | name = warning.name 27 | 28 | {file, line, name} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dialyzer/warnings/warnings.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Warnings do 2 | @moduledoc """ 3 | This is reponsable for formatting all warnings and generating 4 | a nicely formatted output ready to be printed. 5 | 6 | Read &format/1 for more reference on how the output is formatted. 7 | """ 8 | 9 | alias Dialyzer.{Warning, Config.IgnoreWarning} 10 | import Dialyzer.Logger, only: [color: 2] 11 | 12 | @doc """ 13 | It takes a list with all the warnings emitted directly from dialyzer and a 14 | config struct (used to discard ignored warnings, and so on) and it returns 15 | a formatted output ready to be printed out. 16 | 17 | The output is composed by 5 main parts: 18 | 1: A header containing some general stats, like the number of warnings emitted/ignored 19 | 2: A table containing stats about warnings emitted 20 | 3: A footer containing some general informations for the user 21 | 4: The warnings found, formatted accordingly to the format specified by config 22 | 5: A list of all the warnings that have been ignored in `.dialyzer.exs` but 23 | have not been found in the outputted version. 24 | """ 25 | @spec format(list(), Dialyzer.Config.t()) :: String.t() 26 | def format(warnings, config) do 27 | warnings = Enum.map(warnings, &Warning.new/1) 28 | 29 | ignored_tuples = Enum.map(config.warnings[:ignore], &IgnoreWarning.new/1) 30 | 31 | warning_mappings = IgnoreWarning.associate_with_emitted_warnings(ignored_tuples, warnings) 32 | 33 | warnings_to_emit = IgnoreWarning.Mapping.filter_warnings_to_emit(warnings, warning_mappings) 34 | warnings_without_mapping = IgnoreWarning.Mapping.filter_unmatched_warnings(warning_mappings) 35 | 36 | [ 37 | print_header_stats(warnings, warnings_to_emit), 38 | print_stats(warnings, warnings_to_emit), 39 | print_footer(), 40 | print_warnings(warnings_to_emit, config.cmd.msg_type), 41 | print_warnings_without_mapping(warnings_to_emit, warnings_without_mapping) 42 | ] 43 | |> Enum.join("") 44 | end 45 | 46 | @spec print_header_stats([Warning.t()], [Warning.t()]) :: String.t() 47 | defp print_header_stats(warnings, warnings_to_emit) do 48 | """ 49 | 50 | #{color(:yellow, "* STATS")} 51 | 52 | #{color(:cyan, "Number of warnings emitted:")} #{Enum.count(warnings_to_emit)} 53 | #{color(:cyan, "Number of warnings ignored:")} #{ 54 | Enum.count(warnings) - Enum.count(warnings_to_emit) 55 | } 56 | 57 | """ 58 | end 59 | 60 | @spec print_stats([Warning.t()], [Warning.t()]) :: String.t() 61 | defp print_stats(warnings, warnings_to_emit) do 62 | warnings 63 | |> Enum.group_by(fn warning -> warning.name end) 64 | |> Enum.map(fn {warning_name, warnings} -> 65 | num_warnings = Enum.count(warnings) 66 | 67 | num_ignored = 68 | num_warnings - 69 | (fn -> 70 | warnings_to_emit 71 | |> Enum.filter(&(&1.name == warning_name)) 72 | |> Enum.count() 73 | end).() 74 | 75 | %{warning: warning_name, num_ignored: num_ignored, num_emitted: num_warnings} 76 | end) 77 | |> Scribe.format(style: Scribe.Style.Pseudo) 78 | |> case do 79 | :ok -> "" 80 | text -> text 81 | end 82 | end 83 | 84 | @spec print_warnings([Warning.t()], :short | :long) :: String.t() 85 | defp print_warnings(warnings, format) do 86 | if Enum.count(warnings) > 0 do 87 | header = color(:yellow, "* WARNINGS\n") 88 | 89 | body = 90 | warnings 91 | |> Dialyzer.Formatter.format(format) 92 | |> Enum.join("\n") 93 | 94 | "\n#{header}\n#{body}" 95 | end 96 | end 97 | 98 | @spec print_warnings_without_mapping([Warning.t()], [IgnoreWarning.t()]) :: String.t() 99 | defp print_warnings_without_mapping(emitted_warnings, warnings_without_mapping) do 100 | if Enum.count(warnings_without_mapping) > 0 do 101 | message = 102 | Enum.reduce(warnings_without_mapping, "", fn warning, acc -> 103 | header = "\n\n- #{color(:yellow, inspect(IgnoreWarning.to_ignore_format(warning)))}" 104 | 105 | emitted_warnings 106 | |> IgnoreWarning.find_suggestions_for_unmatched_warns(warning) 107 | |> case do 108 | [] -> 109 | acc <> header 110 | 111 | matches -> 112 | formatted_matches = 113 | Enum.reduce(matches, "", fn match, acc -> 114 | ignore_warning_tuple = Warning.to_ignore_format(match) 115 | 116 | acc <> 117 | "\n #{ 118 | color( 119 | :cyan, 120 | inspect( 121 | ignore_warning_tuple, 122 | limit: :infinity, 123 | printable_limit: :infinity 124 | ) 125 | ) 126 | }" 127 | end) 128 | 129 | header 130 | |> Kernel.<>( 131 | "\n\n From the warnings emitted I have found these warnings that could have been the ones you were trying to ignore:\n" 132 | ) 133 | |> Kernel.<>(formatted_matches) 134 | |> Kernel.<>(acc) 135 | end 136 | end) 137 | 138 | """ 139 | 140 | 141 | No match has been found for these ignored warnings you specified in #{ 142 | color(:cyan, ".dialyzer.exs") 143 | }: 144 | 145 | #{message |> String.trim()} 146 | """ 147 | end 148 | end 149 | 150 | @spec print_footer() :: String.t() 151 | defp print_footer do 152 | """ 153 | 154 | #{color(:yellow, "* INFOS")} 155 | 156 | To get more informations about the warnings, as well as your project, 157 | like analyzed applications, avaiable/active/ignored warnings, build paths examined, ... 158 | use the mix command: #{color(:cyan, "`mix dialyzer.info`")} 159 | 160 | To ignore a set of warnings (ie: :underspecs warnings), just remove the 161 | warning atom from the active warnings in #{color(:cyan, "`.dialyzer.exs`")} 162 | 163 | To ignore a specific warning, add a tuple with the format 164 | #{color(:cyan, "{filepath, line, warning}")} to the ignored warnings in #{ 165 | color(:cyan, "`.dialyzer.exs`") 166 | }. 167 | 168 | To match more than one warning, use a placeholder (#{color(:cyan, ":*")}) instead of a specific value: 169 | #{color(:cyan, "{filepath, :*, warning}")} 170 | 171 | When printing with the *long* format, the tuple to ignore a specific warning will be 172 | automatically printed for each warning! 173 | """ 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/mix/tasks/clean.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Dialyzer.Clean do 2 | @shortdoc "Cleans all the artifacts built by dialyzer (plts, caches, manifest files)" 3 | @moduledoc """ 4 | Cleans all the artifacts built by dialyzer. 5 | 6 | ## Command line options 7 | 8 | - --info - logs informations about files deleted, warnings and errors 9 | - --all - removes also the erlang and elixir plts 10 | 11 | ## Usage 12 | 13 | By default, when dialyzer.clean is executed, only the project level artifacts 14 | used by dialyzer will be deleted. 15 | 16 | If you want to delete all the dialyzer artifacts, use the `--all` option. 17 | 18 | `mix dialyzer.clean --all` 19 | 20 | ## .dialyzer.exs 21 | 22 | The configuration file for running dialyzer will never be touched. 23 | If you want to remove completely all the dialyzer related files 24 | from your project, run: 25 | `mix dialyzer.clean --all` 26 | 27 | and then remove manually the .dialyzer.exs file. 28 | """ 29 | 30 | use Mix.Task 31 | import Dialyzer.Logger 32 | alias Dialyzer.Plt 33 | 34 | @command_options [ 35 | info: :boolean, 36 | all: :boolean 37 | ] 38 | 39 | def run(args) do 40 | {opts, _, _} = OptionParser.parse(args, strict: @command_options) 41 | info_active = Keyword.get(opts, :info, false) 42 | all_active = Keyword.get(opts, :all, false) 43 | 44 | deletions = [ 45 | delete_file(Plt.Path.project_plt()), 46 | delete_file(Plt.Manifest.path()), 47 | if(all_active, do: delete_folder(Plt.Path.home_dir()), else: "") 48 | ] 49 | 50 | logs = 51 | deletions 52 | |> Enum.filter(&(&1 != "")) 53 | |> Enum.join("\n") 54 | 55 | if info_active do 56 | if logs != "" do 57 | Mix.shell().info(""" 58 | 59 | #{color(:yellow, "## Files deleted")} 60 | """) 61 | 62 | Mix.shell().info(logs) 63 | else 64 | Mix.shell().info(""" 65 | 66 | #{color(:yellow, "## No files to delete")} 67 | """) 68 | end 69 | end 70 | end 71 | 72 | @spec delete_file(binary) :: String.t() 73 | defp delete_file(filepath) do 74 | case File.rm(filepath) do 75 | :ok -> 76 | """ 77 | #{color(:cyan, "* success")} - #{filepath} 78 | """ 79 | 80 | {:error, :enoent} -> 81 | "" 82 | 83 | {:error, reason} -> 84 | """ 85 | #{color(:cyan, "* failure")} - error during deletion of #{filepath}: #{ 86 | color(:yellow, inspect(reason)) 87 | } 88 | """ 89 | end 90 | end 91 | 92 | @spec delete_folder(binary) :: String.t() 93 | defp delete_folder(path) do 94 | case File.rm_rf(path) do 95 | {:ok, files} -> 96 | files 97 | |> Enum.map(fn filepath -> 98 | """ 99 | #{color(:cyan, "* success")} - #{filepath} 100 | """ 101 | end) 102 | |> Enum.join("\n") 103 | 104 | {:error, :enoent, _filepath} -> 105 | "" 106 | 107 | {:error, reason, filepath} -> 108 | """ 109 | #{color(:cyan, "* failure")} - error during deletion of #{filepath}: #{ 110 | color(:yellow, inspect(reason)) 111 | } 112 | """ 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/mix/tasks/dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Dialyzer do 2 | @shortdoc "Runs static analysis via dialyzer" 3 | @moduledoc """ 4 | Runs a discrepancy analysis through dialyzer and logs 5 | the warnings found. 6 | 7 | The analysis can be configured via the `.dialyzer.exs` file, 8 | in your project root. 9 | 10 | ## Command line options 11 | 12 | - --short - uses the "short" version to format the outputted warnings 13 | - --long - uses the "long" version to format the outputted warnings. 14 | 15 | ## Short formatting 16 | 17 | The short version is a single line containing the file/line and a brief 18 | description of the warning. 19 | 20 | ## Long formatting 21 | 22 | The long version is a multiline containing the file/line and a detailed 23 | description of the warning, as well as the expected type from dialyzer 24 | and a tuple to ignore the file, to add to the `.dialyzer.exs` file. 25 | 26 | ## Usage 27 | 28 | By default, when mix.dialyzer is executed, the short formatting 29 | is used. 30 | 31 | If you want to format with the long format, run: 32 | 33 | `mix dialyzer --long` 34 | """ 35 | 36 | use Mix.Task 37 | 38 | def run(args) do 39 | Mix.Project.compile([]) 40 | _ = Application.ensure_all_started(:mix_dialyzer) 41 | 42 | config = 43 | args 44 | |> Dialyzer.CommandLine.Config.parse() 45 | |> Dialyzer.Config.load() 46 | 47 | config 48 | |> Dialyzer.run() 49 | |> case do 50 | :ok -> 51 | System.stop(0) 52 | 53 | {:error, resp} -> 54 | IO.puts(resp) 55 | if config.cmd.halt_on_error, do: System.stop(1) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/mix/tasks/info.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Dialyzer.Info do 2 | @shortdoc "Gets informations about the dialyzer enviroment" 3 | @moduledoc """ 4 | Displays some informations about your dialyzer enviroment, 5 | like analyzed applications and dependencies, active and ignored 6 | warnings, build paths analyzed and so on. 7 | """ 8 | 9 | use Mix.Task 10 | import Dialyzer.Logger 11 | 12 | @warning_info [ 13 | error_handling: "Include warnings for functions that only return by an exception.", 14 | no_behaviours: 15 | "Suppress warnings about behavior callbacks that drift from the published recommended interfaces.", 16 | no_contracts: "Suppress warnings about invalid contracts.", 17 | no_fail_call: "Suppress warnings for failing calls.", 18 | no_fun_app: "Suppress warnings for fun applications that will fail.", 19 | no_improper_lists: "Suppress warnings for construction of improper lists.", 20 | no_match: "Suppress warnings for patterns that are unused or cannot match.", 21 | no_missing_calls: "Suppress warnings about calls to missing functions.", 22 | no_opaque: "Suppress warnings for violations of opacity of data types.", 23 | no_return: "Suppress warnings for functions that will never return a value.", 24 | no_undefined_callbacks: 25 | "Suppress warnings about behaviors that have no -callback attributes for their callbacks.", 26 | no_unused: "Suppress warnings for unused functions.", 27 | race_conditions: 28 | "Include warnings for possible race conditions. Notice that the analysis that finds data races performs intra-procedural data flow analysis and can sometimes explode in time. Enable it at your own risk.", 29 | underspecs: 30 | "Warn about underspecified functions (the specification is strictly more allowing than the success typing).", 31 | unknown: 32 | "Let warnings about unknown functions and types affect the exit status of the command-line version. The default is to ignore warnings about unknown functions and types when setting the exit status. When using Dialyzer from Erlang, warnings about unknown functions and types are returned; the default is not to return these warnings.", 33 | unmatched_returns: 34 | "Include warnings for function calls that ignore a structured return value or do not match against one of many possible return value(s).", 35 | overspecs: 36 | "Warn about overspecified functions (the specification is strictly less allowing than the success typing).", 37 | specdiffs: "Warn when the specification is different than the success typing." 38 | ] 39 | 40 | def run(_args) do 41 | Mix.Project.compile([]) 42 | _ = Application.ensure_all_started(:mix_dialyzer) 43 | 44 | config = Dialyzer.Config.load() 45 | 46 | IO.puts(""" 47 | 48 | Welcome to mix dialyzer! A tool for integrating dialyzer into a project and analyzing discrepances. 49 | Here are some infos about your system: 50 | 51 | #{color(:yellow, "## Application name")} 52 | 53 | #{application_name()} 54 | 55 | #{color(:yellow, "## Applications included into analysis")} 56 | 57 | #{applications_analyzed(config)} 58 | 59 | #{color(:yellow, "## Applications removed from analysis")} 60 | 61 | #{applications_not_analyzed(config)} 62 | 63 | #{color(:yellow, "## Warnings currently active")} 64 | 65 | #{warnings_analyzed(config)} 66 | 67 | #{color(:yellow, "## Warnings ignored")} 68 | 69 | #{warnings_not_analyzed(config)} 70 | 71 | #{color(:yellow, "## Build directories found")} 72 | 73 | #{build_directories(config)} 74 | 75 | #{color(:yellow, "## More infos")} 76 | 77 | If you want to read more about dialyzer itself, here you can find some nice infos: 78 | - http://learnyousomeerlang.com/dialyzer A general guide to what dialyzer is and how it works 79 | - http://erlang.org/doc/man/dialyzer.html Official documentation 80 | """) 81 | end 82 | 83 | defp application_name do 84 | Dialyzer.Project.applications() 85 | |> Enum.reduce("", fn app, acc -> 86 | acc <> 87 | """ 88 | #{color(:cyan, "* #{app}")} 89 | """ 90 | end) 91 | |> String.trim() 92 | end 93 | 94 | defp applications_analyzed(config) do 95 | erlang_apps = Dialyzer.Plt.Builder.erlang_apps() 96 | elixir_apps = Dialyzer.Plt.Builder.elixir_apps() -- erlang_apps 97 | 98 | project_apps = 99 | config 100 | |> Dialyzer.Plt.Builder.project_apps() 101 | |> Kernel.--(elixir_apps) 102 | |> Kernel.--(Dialyzer.Project.applications()) 103 | 104 | str = format_applications_analyzed_section("Erlang applications", erlang_apps) 105 | str = str <> format_applications_analyzed_section("Elixir applications", elixir_apps) 106 | str = str <> format_applications_analyzed_section("Project applications", project_apps) 107 | str 108 | end 109 | 110 | defp format_applications_analyzed_section(section_name, apps) do 111 | Enum.reduce(apps, "", fn app, acc -> 112 | acc <> 113 | """ 114 | #{color(:cyan, "* #{app}")} 115 | """ 116 | end) 117 | |> case do 118 | "" -> 119 | "[]" 120 | 121 | str -> 122 | """ 123 | 124 | #{color(:yellow, "### #{section_name}")} 125 | 126 | """ <> str 127 | end 128 | end 129 | 130 | defp applications_not_analyzed(config) do 131 | config.apps[:remove] 132 | |> Enum.reduce("", fn app, acc -> 133 | acc <> 134 | """ 135 | #{color(:cyan, "* #{app.app}")} 136 | """ 137 | end) 138 | |> case do 139 | "" -> "[]" 140 | str -> str 141 | end 142 | end 143 | 144 | defp warnings_analyzed(config) do 145 | Enum.reduce(config.warnings[:active], "", fn warning, acc -> 146 | acc <> 147 | """ 148 | #{color(:cyan, "* #{warning}")} - #{@warning_info[warning]} 149 | 150 | """ 151 | end) 152 | # To eliminate blank line at the end 153 | |> String.trim() 154 | |> case do 155 | "" -> "[]" 156 | str -> str 157 | end 158 | end 159 | 160 | defp warnings_not_analyzed(config) do 161 | warnings = Keyword.keys(@warning_info) -- config.warnings 162 | 163 | Enum.reduce(warnings, "", fn warning, acc -> 164 | acc <> 165 | """ 166 | #{color(:cyan, "* #{warning}")} - #{@warning_info[warning]} 167 | 168 | """ 169 | end) 170 | # To eliminate blank line at the end 171 | |> String.trim() 172 | |> case do 173 | "" -> "[]" 174 | str -> str 175 | end 176 | end 177 | 178 | defp build_directories(config) do 179 | Enum.reduce(config.build_dir, "", fn dir, acc -> 180 | acc <> 181 | """ 182 | #{color(:cyan, "* #{dir}")} 183 | 184 | """ 185 | end) 186 | |> String.trim() 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :mix_dialyzer, 7 | version: "0.2.0", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | aliases: aliases(), 12 | test_coverage: [tool: ExCoveralls], 13 | preferred_cli_env: [ 14 | coveralls: :test, 15 | "coveralls.detail": :test, 16 | "coveralls.post": :test, 17 | "coveralls.html": :test 18 | ] 19 | ] 20 | end 21 | 22 | def application do 23 | [ 24 | mod: {Dialyzer.Application, []}, 25 | extra_applications: [:logger] 26 | ] 27 | end 28 | 29 | defp deps do 30 | [ 31 | {:erlex, "~> 0.1.0"}, 32 | {:scribe, "~> 0.8"}, 33 | {:excoveralls, "~> 0.8", only: [:test]} 34 | ] 35 | end 36 | 37 | defp aliases do 38 | [ 39 | test: [ 40 | "run ./test.setup.exs", 41 | "test" 42 | ] 43 | ] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "erlex": {:hex, :erlex, "0.1.0", "4679cfef156cfb39ffc33e47dca23f848d7ecd7b30a15be93b7239045d29d7b2", [:mix], [], "hexpm"}, 4 | "excoveralls": {:hex, :excoveralls, "0.9.1", "14fd20fac51ab98d8e79615814cc9811888d2d7b28e85aa90ff2e30dcf3191d6", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 9 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 10 | "pane": {:hex, :pane, "0.3.0", "7b028e2aa8f004b62a965de983d824ad20d94f3c9c82b2bb4a9cdcebeb9e7060", [:mix], [], "hexpm"}, 11 | "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, 12 | "scribe": {:hex, :scribe, "0.8.0", "41ead349dc2f89b8f6b4596633f829340e7e5fcbee3e7eb3642fd031dd21142a", [:mix], [{:pane, "~> 0.2", [hex: :pane, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 14 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 15 | } 16 | -------------------------------------------------------------------------------- /test.setup.exs: -------------------------------------------------------------------------------- 1 | File.cd!("test/fixtures/base_project/", fn -> 2 | System.cmd("mix", ["deps.get"]) 3 | System.cmd("mix", ["compile"]) 4 | System.cmd("mix", ["dialyzer"]) 5 | end) 6 | 7 | File.cd!("test/fixtures/complex_project/", fn -> 8 | System.cmd("mix", ["deps.get"]) 9 | System.cmd("mix", ["compile"]) 10 | System.cmd("mix", ["dialyzer"]) 11 | end) 12 | -------------------------------------------------------------------------------- /test/fixtures/base_project/.dialyzer.exs: -------------------------------------------------------------------------------- 1 | [ 2 | apps: [ 3 | remove: [], 4 | include: [] 5 | ], 6 | warnings: [ 7 | ignore: [], 8 | active: [ 9 | :unmatched_returns, 10 | :error_handling, 11 | :unknown 12 | ] 13 | ], 14 | extra_build_dir: [] 15 | ] 16 | -------------------------------------------------------------------------------- /test/fixtures/base_project/lib/mod.ex: -------------------------------------------------------------------------------- 1 | defmodule BaseProject.Mod do 2 | def add(n1, n2), do: n1 + n2 3 | 4 | def call_add, do: add("hello", "world") 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/base_project/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BaseProject.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :base_project, version: "0.1.0", deps: deps()] 6 | end 7 | 8 | def application do 9 | [applications: [:logger]] 10 | end 11 | 12 | defp deps do 13 | [ 14 | {:mix_dialyzer, path: "../../../", only: [:dev]} 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/base_project/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "erlex": {:hex, :erlex, "0.1.0", "4679cfef156cfb39ffc33e47dca23f848d7ecd7b30a15be93b7239045d29d7b2", [:mix], [], "hexpm"}, 3 | "pane": {:hex, :pane, "0.3.0", "7b028e2aa8f004b62a965de983d824ad20d94f3c9c82b2bb4a9cdcebeb9e7060", [:mix], [], "hexpm"}, 4 | "scribe": {:hex, :scribe, "0.8.0", "41ead349dc2f89b8f6b4596633f829340e7e5fcbee3e7eb3642fd031dd21142a", [:mix], [{:pane, "~> 0.2", [hex: :pane, repo: "hexpm", optional: false]}], "hexpm"}, 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/complex_project/.dialyzer.exs: -------------------------------------------------------------------------------- 1 | [ 2 | apps: [ 3 | remove: [], 4 | include: [] 5 | ], 6 | warnings: [ 7 | ignore: [ 8 | {"lib/mod.ex", -1, :no_return}, 9 | {"lib/mod.ex", 6, :*} 10 | ], 11 | active: [ 12 | :unmatched_returns, 13 | :error_handling, 14 | :unknown 15 | ] 16 | ], 17 | extra_build_dir: [] 18 | ] 19 | -------------------------------------------------------------------------------- /test/fixtures/complex_project/lib/mod.ex: -------------------------------------------------------------------------------- 1 | defmodule ComplexProject.Mod do 2 | def add(n1, n2), do: n1 + n2 3 | def mult(n1, n2), do: n1 * n2 4 | 5 | def call_add, do: add("hello", "world") 6 | def call_mult, do: mult("hello", "world") 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/complex_project/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ComplexProject.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :complex_project, version: "0.1.0", deps: deps()] 6 | end 7 | 8 | def application do 9 | [applications: [:logger]] 10 | end 11 | 12 | defp deps do 13 | [ 14 | {:mix_dialyzer, path: "../../../", only: [:dev]} 15 | ] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/fixtures/complex_project/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "erlex": {:hex, :erlex, "0.1.0", "4679cfef156cfb39ffc33e47dca23f848d7ecd7b30a15be93b7239045d29d7b2", [:mix], [], "hexpm"}, 3 | "pane": {:hex, :pane, "0.3.0", "7b028e2aa8f004b62a965de983d824ad20d94f3c9c82b2bb4a9cdcebeb9e7060", [:mix], [], "hexpm"}, 4 | "scribe": {:hex, :scribe, "0.8.0", "41ead349dc2f89b8f6b4596633f829340e7e5fcbee3e7eb3642fd031dd21142a", [:mix], [{:pane, "~> 0.2", [hex: :pane, repo: "hexpm", optional: false]}], "hexpm"}, 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/missing_config/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MissingConfig.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :missing_config, version: "0.1.0", deps: deps()] 6 | end 7 | 8 | def application do 9 | [applications: [:logger]] 10 | end 11 | 12 | defp deps do 13 | [] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/mix_dialyzer/app_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.AppTest do 2 | use ExUnit.Case 3 | alias Dialyzer.Plt.{App} 4 | 5 | setup_all do 6 | Application.ensure_all_started(:mix_dialyzer) 7 | :ok 8 | end 9 | 10 | describe "when getting application info" do 11 | test "it works correctly when the application exists" do 12 | info = App.info(:elixir) 13 | info2 = Application.spec(:elixir) 14 | 15 | assert info.app == elem(info2[:mod], 0) 16 | assert Enum.map(info.mods, & &1.module) == info2[:modules] 17 | assert info.vsn == info2[:vsn] 18 | end 19 | 20 | test "it returns nil when application doesn't exist" do 21 | assert App.info(:not_existing) == nil 22 | end 23 | 24 | test "it gets an app from cache if already present" do 25 | app = :kernel 26 | 27 | if not App.Cache.in_cache?(app) do 28 | App.info(app) 29 | end 30 | 31 | assert App.Cache.in_cache?(app) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/mix_dialyzer/builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.BuilderTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | 5 | alias Dialyzer.Plt.{Builder} 6 | alias Dialyzer.{Project, Config} 7 | 8 | describe "when requesting applications used to build plts" do 9 | test "it returns the correct list for the erlang plt" do 10 | in_project(:base_project, fn -> 11 | assert [:erts, :kernel, :stdlib, :crypto] == Builder.erlang_apps() 12 | end) 13 | end 14 | 15 | test "it returns the correct list for the elixir plt" do 16 | in_project(:base_project, fn -> 17 | expected = Builder.erlang_apps() ++ [:elixir, :mix] 18 | res = Builder.elixir_apps() 19 | 20 | assert expected == res 21 | end) 22 | end 23 | 24 | test "it returns the correct list for the project plt" do 25 | in_project(:base_project, fn -> 26 | expected_list = [Builder.erlang_apps(), Builder.elixir_apps(), Project.dependencies()] 27 | res = Builder.project_apps(Config.load()) 28 | 29 | for expected_apps <- expected_list, 30 | expected_app <- expected_apps do 31 | assert expected_app in res 32 | end 33 | end) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/mix_dialyzer/command_line_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.CommandLineTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | alias Dialyzer.CommandLine.Config 5 | 6 | describe "when passing some arguments to dialyzer" do 7 | test "it uses the default values when an argument is not specified" do 8 | config = Config.parse([]) 9 | assert config.msg_type == :short 10 | assert config.halt_on_error == false 11 | end 12 | 13 | test "it parses correctly the arguments" do 14 | config = Config.parse(["--long"]) 15 | assert config.msg_type == :long 16 | 17 | config = Config.parse(["--ci"]) 18 | assert config.halt_on_error == true 19 | end 20 | 21 | test "it ignores unsupported arguments" do 22 | config = Config.parse(["--new_argument"]) 23 | assert config.msg_type == :short 24 | assert config.halt_on_error == false 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/mix_dialyzer/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.ConfigTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | alias Dialyzer.{Warning, Config, Plt} 5 | 6 | setup do 7 | on_exit(fn -> 8 | in_project(:missing_config, fn -> 9 | Config.path() |> File.rm() 10 | end) 11 | end) 12 | end 13 | 14 | describe "when config file is not present" do 15 | test "it is missing from project root" do 16 | in_project(:missing_config, fn -> 17 | config_path = Config.path() 18 | refute File.exists?(config_path) 19 | end) 20 | end 21 | 22 | test "it creates a new configuration file in project root when trying to load it" do 23 | in_project(:missing_config, fn -> 24 | Config.load() 25 | 26 | config_path = Config.path() 27 | assert File.exists?(config_path) 28 | end) 29 | end 30 | end 31 | 32 | describe "when config file is present" do 33 | test "it is present in project root" do 34 | in_project(:base_project, fn -> 35 | Config.load() 36 | path = Config.path() 37 | assert File.exists?(path) 38 | end) 39 | end 40 | 41 | test "it evaluates correctly the file" do 42 | in_project(:base_project, fn -> 43 | config = Config.load() 44 | assert is_map(config) 45 | end) 46 | end 47 | 48 | test "it has default options setted correctly" do 49 | in_project(:base_project, fn -> 50 | config = Config.load() 51 | assert config.apps == [remove: [], include: []] 52 | assert config.init_plt == Plt.Path.project_plt() 53 | assert config.warnings[:active] == Config.default_warnings() 54 | end) 55 | end 56 | end 57 | 58 | describe "when it is ignoring a warning" do 59 | test "it has a tuple inside the ignored warnings" do 60 | in_project(:complex_project, fn -> 61 | config = Config.load() 62 | 63 | expected = {"lib/mod.ex", -1, :no_return} 64 | assert expected in config.warnings[:ignore] 65 | end) 66 | end 67 | 68 | test "it parses correctly the tuple" do 69 | in_project(:complex_project, fn -> 70 | config = Config.load() 71 | 72 | expected = {"lib/mod.ex", 5, :no_return} 73 | res = Config.IgnoreWarning.new(expected) 74 | 75 | assert res.file == elem(expected, 0) 76 | assert res.line == elem(expected, 1) 77 | assert res.warning == elem(expected, 2) 78 | end) 79 | end 80 | 81 | test "it extracts the value or a default from the tuple" do 82 | in_project(:complex_project, fn -> 83 | config = Config.load() 84 | 85 | expected = {"lib/mod.ex", :*, :no_return} 86 | res = Config.IgnoreWarning.new(expected) 87 | 88 | assert res.file == Config.IgnoreWarning.get_field_with_default(res, :file, nil) 89 | assert :none == Config.IgnoreWarning.get_field_with_default(res, :line, :none) 90 | end) 91 | end 92 | 93 | test "it associates correctly with an emitted warning" do 94 | in_project(:complex_project, fn -> 95 | config = Config.load() 96 | 97 | ignored_warning = {"lib/mod.ex", 5, :no_return} 98 | res_ignored = Config.IgnoreWarning.new(ignored_warning) 99 | 100 | emitted_warning = 101 | {:warn_return_no_exit, {'lib/mod.ex', 5}, {:no_return, [:only_normal, :call_add, 0]}} 102 | 103 | res_emitted = Warning.new(emitted_warning) 104 | 105 | [res] = Config.IgnoreWarning.associate_with_emitted_warnings([res_ignored], [res_emitted]) 106 | assert res.ignore_warning == res_ignored 107 | assert res_emitted in res.warnings 108 | end) 109 | end 110 | 111 | test "it finds suggestions for an unmatched warning" do 112 | in_project(:complex_project, fn -> 113 | config = Config.load() 114 | 115 | ignored_warning = {"lib/mod.ex", 7, :no_return} 116 | res_ignored = Config.IgnoreWarning.new(ignored_warning) 117 | 118 | emitted_warning = 119 | {:warn_return_no_exit, {'lib/mod.ex', 5}, {:no_return, [:only_normal, :call_add, 0]}} 120 | 121 | res_emitted = Warning.new(emitted_warning) 122 | 123 | [res] = 124 | Config.IgnoreWarning.find_suggestions_for_unmatched_warns([res_emitted], res_ignored) 125 | 126 | assert res == res_emitted 127 | end) 128 | end 129 | 130 | test "it formats correctly a warning into the tuple format" do 131 | ignored_warning = {"lib/mod.ex", 7, :no_return} 132 | res_ignored = Config.IgnoreWarning.new(ignored_warning) 133 | 134 | tuple = Config.IgnoreWarning.to_ignore_format(res_ignored) 135 | assert res_ignored.file == elem(tuple, 0) 136 | assert res_ignored.line == elem(tuple, 1) 137 | assert res_ignored.warning == elem(tuple, 2) 138 | end 139 | 140 | test "it filters the warnings to emit excluding the ignored ones" do 141 | in_project(:complex_project, fn -> 142 | config = Config.load() 143 | 144 | ignored_warning = {"lib/mod.ex", 7, :no_return} 145 | res_ignored = Config.IgnoreWarning.new(ignored_warning) 146 | 147 | emitted_warning = 148 | {:warn_return_no_exit, {'lib/mod.ex', 5}, {:no_return, [:only_normal, :call_add, 0]}} 149 | 150 | res_emitted = Warning.new(emitted_warning) 151 | 152 | mappings = 153 | Config.IgnoreWarning.associate_with_emitted_warnings([res_ignored], [res_emitted]) 154 | 155 | [res] = Config.IgnoreWarning.Mapping.filter_warnings_to_emit([res_emitted], mappings) 156 | 157 | assert res == res_emitted 158 | end) 159 | end 160 | 161 | test "it filters the unmatched warnings" do 162 | in_project(:complex_project, fn -> 163 | config = Config.load() 164 | 165 | ignored_warning = {"lib/mod.ex", 7, :no_return} 166 | res_ignored = Config.IgnoreWarning.new(ignored_warning) 167 | 168 | emitted_warning = 169 | {:warn_return_no_exit, {'lib/mod.ex', 5}, {:no_return, [:only_normal, :call_add, 0]}} 170 | 171 | res_emitted = Warning.new(emitted_warning) 172 | 173 | mappings = 174 | Config.IgnoreWarning.associate_with_emitted_warnings([res_ignored], [res_emitted]) 175 | 176 | res = Config.IgnoreWarning.Mapping.filter_unmatched_warnings(mappings) 177 | 178 | assert res == [res_ignored] 179 | end) 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /test/mix_dialyzer/dialyzer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.DialyzerTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | alias Dialyzer.Config 5 | 6 | setup_all do 7 | Application.ensure_all_started(:mix_dialyzer) 8 | :ok 9 | end 10 | 11 | describe "when running dialyzer" do 12 | test "it prints out the warnings when emitted" do 13 | in_project(:complex_project, fn -> 14 | config = Config.load() 15 | {:error, output} = Dialyzer.run(config) 16 | 17 | assert String.contains?(output, "lib/mod.ex:5") 18 | end) 19 | end 20 | 21 | test "it doesn't print a warning when ignored in .dialyzer.exs" do 22 | in_project(:complex_project, fn -> 23 | config = Config.load() 24 | {:error, output} = Dialyzer.run(config) 25 | 26 | refute String.contains?(output, "lib/mod.ex:6") 27 | end) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/mix_dialyzer/manifest_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.ManifestTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | alias Dialyzer.Config 5 | alias Dialyzer.Plt.{Manifest} 6 | 7 | describe "when inside a new project" do 8 | test "it doesn't have a manifest file" do 9 | in_project(:base_project, fn -> 10 | Manifest.path() |> File.rm() 11 | 12 | config = Config.load() 13 | assert Manifest.status(config) == :missing 14 | end) 15 | end 16 | end 17 | 18 | describe "when the manifest file exists" do 19 | test "it doesn't return :missing when requesting the status" do 20 | in_project(:base_project, fn -> 21 | config = Config.load() 22 | Dialyzer.Plt.ensure_loaded(config) 23 | Manifest.update() 24 | 25 | assert Manifest.status(config) != :missing 26 | end) 27 | end 28 | 29 | test "it detects correctly the status" do 30 | in_project(:base_project, fn -> 31 | config = Config.load() 32 | Dialyzer.Plt.ensure_loaded(config) 33 | Manifest.update() 34 | 35 | assert Manifest.status(config) == :up_to_date 36 | 37 | config = %Config{config | apps: [remove: [:kernel], include: []]} 38 | assert Manifest.status(config) == :outdated 39 | end) 40 | end 41 | 42 | test "it detects correctly if the apps changes" do 43 | in_project(:base_project, fn -> 44 | config = Config.load() 45 | Manifest.update() 46 | 47 | config = %Config{config | apps: [remove: [:kernel], include: []]} 48 | assert Manifest.changes(config)[:files][:removed] |> Enum.count() > 0 49 | end) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/mix_dialyzer/project_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.ProjectTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | alias Dialyzer.{Project} 5 | 6 | describe "when inside a project" do 7 | test "it gets correctly the application name" do 8 | in_project(:base_project, fn -> 9 | assert Project.applications() == [:base_project] 10 | end) 11 | end 12 | 13 | test "it gets direct dependencies" do 14 | in_project(:base_project, fn -> 15 | assert :logger in Project.dependencies() 16 | end) 17 | end 18 | 19 | test "it gets transitive dependencies" do 20 | in_project(:base_project, fn -> 21 | assert :elixir in Project.dependencies() 22 | end) 23 | end 24 | 25 | test "it gets all the build paths" do 26 | in_project(:base_project, fn -> 27 | assert Enum.count(Project.build_paths()) == 1 28 | end) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/mix_dialyzer/warnings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Plt.WarningsTest do 2 | use ExUnit.Case 3 | import Dialyzer.Test.Util 4 | 5 | test "it has a corresponding module for each warning emitted by dialyzer" do 6 | dialyzer_warnings = [ 7 | :opaque_neq, 8 | :opaque_guard, 9 | :callback_type_mismatch, 10 | :exact_eq, 11 | :callback_info_missing, 12 | :pattern_match_cov, 13 | :call_with_opaque, 14 | :record_matching, 15 | :race_condition, 16 | :contract_subtype, 17 | :bin_construction, 18 | :opaque_type_test, 19 | :fun_app_args, 20 | :call_to_missing, 21 | :invalid_contract, 22 | :overlapping_contract, 23 | :guard_fail_pat, 24 | :unknown_behaviour, 25 | :no_return, 26 | :extra_range, 27 | :callback_arg_type_mismatch, 28 | :improper_list_constr, 29 | :callback_spec_type_mismatch, 30 | :guard_fail, 31 | :contract_with_opaque, 32 | :record_constr, 33 | :app_call, 34 | :callback_spec_arg_type_mismatch, 35 | :unknown_function, 36 | :callback_missing, 37 | :unknown_type, 38 | :contract_supertype, 39 | :call_without_opaque, 40 | :apply, 41 | :unused_fun, 42 | :opaque_eq, 43 | :contract_diff, 44 | :neg_guard_fail, 45 | :call, 46 | :unmatched_return, 47 | :opaque_match, 48 | :fun_app_no_fun, 49 | :pattern_match 50 | ] 51 | 52 | Dialyzer.Formatter.Warnings.warnings() 53 | |> Enum.any?(fn {warning, _mod} -> 54 | warning in dialyzer_warnings 55 | end) 56 | |> assert 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Code.load_file("test/util.ex") 2 | 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /test/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyzer.Test.Util do 2 | def in_project(app, f) when is_atom(app) do 3 | Mix.Project.in_project(app, "test/fixtures/#{Atom.to_string(app)}", fn _ -> f.() end) 4 | end 5 | end 6 | --------------------------------------------------------------------------------