├── .awconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── new-rule.md │ └── other-issues.md └── workflows │ └── erlang.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── elvis.config ├── nextroll.dict ├── priv └── overview.edoc ├── rebar.config ├── rebar.lock ├── src ├── hank.erl ├── hank_context.erl ├── hank_rule.erl ├── hank_utils.erl ├── rebar3_hank.app.src ├── rebar3_hank.erl ├── rebar3_hank_prv.erl └── rules │ ├── single_use_hrl_attrs.erl │ ├── single_use_hrls.erl │ ├── unnecessary_function_arguments.erl │ ├── unused_callbacks.erl │ ├── unused_configuration_options.erl │ ├── unused_hrls.erl │ ├── unused_macros.erl │ └── unused_record_fields.erl ├── test.dict └── test ├── files ├── hidden │ ├── .hidden.module.erl │ ├── .hidden_folder │ │ └── regular_module.erl │ ├── _hidden_folder │ │ └── regular_module.erl │ └── _hidden_module.erl ├── ignore │ ├── no_ignore.erl │ ├── no_ignore.hrl │ ├── rebar_config_ignore.erl │ ├── specific_ignore.erl │ ├── with_ignore.erl │ └── with_ignore.hrl ├── single_use_hrl_attrs │ └── lib │ │ └── app │ │ ├── include │ │ ├── flow.hrl │ │ ├── header1.hrl │ │ ├── header2.hrl │ │ ├── header3.hrl │ │ └── ignore.hrl │ │ └── src │ │ ├── app.app.src │ │ ├── app_include.erl │ │ ├── app_include_lib.erl │ │ ├── app_other.erl │ │ ├── flow.erl │ │ └── ignore.erl ├── single_use_hrls │ ├── include │ │ ├── ignored.hrl │ │ ├── multi.hrl │ │ ├── single.hrl │ │ └── single_unicode.hrl │ └── src │ │ ├── include_ignored.erl │ │ ├── include_missing.erl │ │ ├── include_multi.erl │ │ ├── include_single.erl │ │ └── include_unicode_åö.erl ├── test_app │ ├── another_sys.config │ ├── elvis.config │ ├── include │ │ ├── a_folder │ │ │ └── a_header.hrl │ │ └── a_header.hrl │ ├── lib │ │ └── a_project │ │ │ └── src │ │ │ └── a_folder │ │ │ └── a_subfolder │ │ │ └── a_module.erl │ ├── one_tuple.config │ ├── rebar.config │ ├── relx.config │ ├── src │ │ ├── a_folder │ │ │ ├── a_module.erl │ │ │ └── a_subfolder │ │ │ │ └── a_module.erl │ │ ├── a_module.erl │ │ └── test_app.app.src │ ├── sys.config │ ├── test │ │ ├── a_folder │ │ │ └── a_module.erl │ │ └── a_module_SUITE.erl │ ├── unconventional.config │ ├── user.config │ └── without_warnings │ │ ├── include │ │ ├── a_folder │ │ │ └── a_header.hrl │ │ └── a_header.hrl │ │ ├── lib │ │ └── a_project │ │ │ └── src │ │ │ └── a_folder │ │ │ └── a_subfolder │ │ │ ├── a_module.erl │ │ │ └── another_module.erl │ │ ├── src │ │ ├── a_folder │ │ │ ├── a_module.erl │ │ │ └── a_subfolder │ │ │ │ └── a_module.erl │ │ └── a_module.erl │ │ └── test │ │ ├── a_folder │ │ └── a_module.erl │ │ └── a_module_SUITE.erl ├── unnecessary_function_arguments │ ├── a_behaviour.erl │ ├── a_behaviour_imp.erl │ ├── clean.erl │ ├── ct_SUITE.erl │ ├── export_all1.erl │ ├── export_all2.erl │ ├── gen_server_imp.erl │ ├── ignore.erl │ ├── ignore_config.erl │ ├── macro_behaviour_imp.erl │ ├── macros.erl │ ├── nifs.erl │ ├── not_included_behaviour.erl │ ├── old_ct_SUITE.erl │ ├── parse_transf.erl │ ├── warnings_A.erl │ ├── warnings_B.erl │ └── weird.erl ├── unused_callbacks │ ├── clean.erl │ ├── ignore.erl │ ├── ignore_config.erl │ ├── macros.erl │ └── warnings.erl ├── unused_hrls │ └── lib │ │ ├── app0-with-other-name │ │ ├── include │ │ │ └── header.hrl │ │ └── src │ │ │ └── app0.app.src │ │ ├── app1 │ │ ├── include │ │ │ └── header.hrl │ │ └── src │ │ │ ├── app1.app.src │ │ │ ├── app1_include.erl │ │ │ ├── app1_include_lib.erl │ │ │ └── app1_not_using_header.erl │ │ └── app2 │ │ └── src │ │ ├── app2.app.src │ │ ├── app2_include.erl │ │ ├── app2_include_lib.erl │ │ └── app2_not_using_header.erl ├── unused_ignores │ ├── ignore_all.erl │ └── unused_ignores.erl ├── unused_macros │ ├── double.erl │ ├── flow.erl │ ├── header.hrl │ ├── ignore.erl │ ├── ignore_config.erl │ ├── unused_macro_sample.erl │ └── used_macro_sample.erl └── unused_record_fields │ ├── header.hrl │ ├── ignore.erl │ ├── ignore_config.erl │ ├── macros.erl │ ├── unused_record_field_sample.erl │ └── used_record_field_sample.erl ├── global_rejector.erl ├── hank_test_utils.erl ├── hidden_SUITE.erl ├── ignore_SUITE.erl ├── single_use_hrl_attrs_SUITE.erl ├── single_use_hrls_SUITE.erl ├── test_app_SUITE.erl ├── unnecessary_function_arguments_SUITE.erl ├── unused_callbacks_SUITE.erl ├── unused_hrls_SUITE.erl ├── unused_ignores_SUITE.erl ├── unused_macros_SUITE.erl └── unused_record_fields_SUITE.erl /.awconfig: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug Description 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior 15 | 16 | ## Expected Behavior 17 | A clear and concise description of what you expected to happen. 18 | 19 | ## `rebar3` Logs 20 | If applicable, run `rebar3` with `DIAGNOSTIC=1` and attach all the logs to your report. 21 | 22 | ## Additional Context 23 | - OS: [e.g. MacOS] 24 | - Erlang version 25 | - rebar3 version 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional Context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-rule.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Rule 3 | about: Add a new way to find dead code and kill it with fire 4 | title: '' 5 | labels: rule 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Name 11 | 12 | 13 | ### Brief Description 14 | 15 | 16 | ### Reasoning 17 | 18 | 19 | ### Refactoring Proposal 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other Issues 3 | about: Something that's not covered by the other categories 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/erlang.yml: -------------------------------------------------------------------------------- 1 | name: Erlang CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | ERL_FLAGS: "-enable-feature all" 7 | 8 | jobs: 9 | 10 | build: 11 | 12 | runs-on: ubuntu-24.04 13 | 14 | strategy: 15 | matrix: 16 | otp: ['25', '26', '27'] 17 | rebar: ['3.24'] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: erlef/setup-beam@v1 22 | id: setup-beam 23 | with: 24 | otp-version: ${{matrix.otp}} 25 | rebar3-version: ${{matrix.rebar}} 26 | - name: Restore _build 27 | uses: actions/cache@v4 28 | with: 29 | path: _build 30 | key: _build-cache-for-os-${{runner.os}}-otp-${{steps.setup-beam.outputs.otp-version}}-rebar3-${{steps.setup-beam.outputs.rebar3-version}}-hash-${{hashFiles('rebar.lock')}} 31 | - name: Restore rebar3's cache 32 | uses: actions/cache@v4 33 | with: 34 | path: ~/.cache/rebar3 35 | key: rebar3-cache-for-os-${{runner.os}}-otp-${{steps.setup-beam.outputs.otp-version}}-rebar3-${{steps.setup-beam.outputs.rebar3-version}}-hash-${{hashFiles('rebar.lock')}} 36 | - name: Compile 37 | run: rebar3 compile 38 | - name: Format check 39 | run: rebar3 format --verify 40 | - name: Run tests and verifications 41 | run: rebar3 test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rebar3.crashdump 2 | .rebar3 3 | _* 4 | .eunit 5 | *.o 6 | *.beam 7 | *.plt 8 | erl_crash.dump 9 | .concrete/DEV_MODE 10 | 11 | .rebar 12 | rel/ 13 | ebin/ 14 | 15 | _build 16 | _checkouts 17 | doc/ 18 | rebar3 19 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 24.3 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See the [Releases](../../releases) page. 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rtb-team@nextroll.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit 4 | helps, and credit will always be given. 5 | 6 | You can contribute in many ways: 7 | 8 | Types of Contributions 9 | ---------------------- 10 | 11 | ### Report Bugs 12 | 13 | Report bugs at https://github.com/AdRoll/rebar3_hank/issues. 14 | 15 | If you are reporting a bug, please include: 16 | 17 | * Your operating system name and version. 18 | * Any details about your local setup that might be helpful in troubleshooting (OTP version, rebar3 version, sample code, etc). 19 | * Detailed steps to reproduce the bug. 20 | 21 | ### Fix Bugs 22 | 23 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 24 | wanted" is open to whoever wants to implement it. 25 | 26 | ### Implement Features 27 | 28 | Look through the GitHub issues for features. Anything tagged with "enhancement" 29 | and "help wanted" is open to whoever wants to implement it. 30 | 31 | ### Write Documentation 32 | 33 | rebar3_hank could always use more documentation, whether as part of the 34 | official rebar3_hank docs, in docstrings, or even on the web in blog posts, 35 | articles, and such. 36 | 37 | ### Submit Feedback 38 | 39 | The best way to send feedback is to start a dicsussion at https://github.com/AdRoll/rebar3_hank/discussions. 40 | 41 | Get Started! 42 | ------------ 43 | 44 | Ready to contribute? Here's how to set up `rebar3_hank` for local development. 45 | 46 | 1. Fork the `rebar3_hank` repo on GitHub. 47 | 48 | 2. Clone your fork locally: 49 | 50 | `$ git clone git@github.com:your_name_here/rebar3_hank.git` 51 | 52 | 3. Compile the project, assuming you rebar3 and OTP 21+ installed, you can run: 53 | 54 | `$ rebar3 compile` 55 | 56 | 4. Create a branch for local development: 57 | 58 | `$ git checkout -b name-of-your-bugfix-or-feature` 59 | 60 | Now you can make your changes locally. 61 | 62 | 5. When you're done making changes, check that your changes pass the tests by running `rebar3 test`. You might have to run it twice, if the `dialyzer` step fails while building the PLT. It's a known annoyance, sorry about that. 63 | 64 | 6. Commit your changes and push your branch to GitHub: 65 | 66 | ```bash 67 | $ git add . 68 | $ git commit -m "Your detailed description of your changes." 69 | $ git push origin name-of-your-bugfix-or-feature 70 | ``` 71 | 72 | 7. Submit a pull request through the GitHub website. 73 | 74 | Pull Request Guidelines 75 | ----------------------- 76 | 77 | Before you submit a pull request, check that it meets these guidelines: 78 | 79 | 1. The pull request should include tests. 80 | 2. If the pull request adds functionality, the docs should be updated. 81 | 3. `rebar3 test` shouldn't fail. 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 NextRoll Inc., Brujo Benavides, Pablo Brudnick, and Diego Calero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rebar3_hank [![Build Status](https://github.com/AdRoll/rebar3_hank/actions/workflows/erlang.yml/badge.svg)](https://github.com/AdRoll/rebar3_hank) [![Hex pm](http://img.shields.io/hexpm/v/rebar3_hank.svg?style=flat)](https://hex.pm/packages/rebar3_hank) 2 | ### The Erlang Dead Code Cleaner 3 | 4 | Kill it with Fire 5 | 6 | > Mr. Scorpio says productivity is up 2%, and it's all because of my motivational techniques, like donuts and the possibility of more donuts to come. 7 | > 8 | > _(Homer Simpson)_ 9 | 10 | ## Sample Output 11 | 12 | If Hank detects issues in your code, it will report them as follows… 13 | 14 | ``` 15 | src/lapp.erl:18: maybe_evaluate/3 doesn't need its #2 argument 16 | src/lapp.erl:15: maybe_evaluate/2 doesn't need its #1 argument 17 | src/lapp.erl:5: ?DEFAULT_SAMPLE_RATE is unused 18 | src/lapp.app.src:0: sample_rate is not used anywhere in the code 19 | src/include/header.hrl:2: ?SOME_MACRO/1 is used only at src/lapp.erl 20 | src/my_behaviour.erl:3: Callback process/1 is not used anywhere in the module 21 | src/include/header.hrl:1: ?APP_HEADER is unused 22 | src/main.erl:8: Field color in record state is unused 23 | ``` 24 | 25 | ## Build 26 | 27 | ```bash 28 | $ rebar3 compile 29 | ``` 30 | 31 | ## Test 32 | 33 | ```bash 34 | $ rebar3 test 35 | ``` 36 | 37 | ## Usage 38 | 39 | Add the plugin to your rebar config: 40 | 41 | ```erlang 42 | {project_plugins, [rebar3_hank]} 43 | ``` 44 | 45 | Then just call the plugin directly in an existing application: 46 | 47 | ```bash 48 | $ rebar3 hank # or… 49 | $ rebar3 kiwf # (Kill It With Fire) 50 | ``` 51 | 52 | This will review your project, analyzing every `*.[he]rl` file in it (optionally skipping some folders/files if you want to - see below). 53 | Note that Hank will **not** consider files from your project dependencies for the analysis. It will only check the source code in your current application (_applications_, if you're working in an umbrella project). 54 | 55 | It will then apply its rules and produce a list of all the dead code (_specially **oxbow** code_) that you can effectively delete and/or refactor. 56 | 57 | ### Command-Line Options 58 | 59 | ``` 60 | $ rebar3 help hank 61 | 62 | Usage: rebar3 hank [-u ] 63 | 64 | -u, --unused_ignores Warn on unused ignores (default: true). 65 | 66 | ``` 67 | By default, Hank will emit warnings such as the following ones if you are ignoring rules that you don't need to ignore (more on that below). But you can turn those warnings off, by using `--unused_ignores=no`. 68 | 69 | It's worth noting that, even when those warnings are printed, that doesn't affect the overall result of the command. That is, if Hank can't find any instances of oxbow code, it will return successfully (i.e. `exit code: 0`) even when it may print these warnings. 70 | 71 | ```markdown 72 | ===> The following ignore specs are no longer needed and can be removed: 73 | * src/ignore_not_empty.erl: unused_hrls 74 | * src/ignore_not_empty.erl: unused_configuration_options 75 | ``` 76 | 77 | ## Certainty 78 | 79 | In principle, Hank should have _Dialyzer_ levels of certainty. That is: if Hank points at some code, you can be **100% sure** it's _dead_. 80 | 81 | That means that if you find some false positives (i.e. Hank pointed at some code that was not really dead / should not be deleted), **please** report it as **a bug** and we'll take care of fixing it. 82 | 83 | ## Configuration 84 | 85 | The plugin supports the following configuration options in the `hank` section of `rebar.config`: 86 | 87 | * `rules` (`[hank_rule:t()]`): 88 | - This is the list of rules to apply to the analyzed code. Each rule is a module that should apply the `hank_rule` behavior. 89 | - If this option is not defined, Hank will apply all [the default rules](src/rules). 90 | * `parsing_style` (`hank:parsing_style()`): 91 | - This parameter determines if Hank should parse files in a parallel (`rpc:pmap/3`) or sequential (`lists:map/2`) fashion. 92 | - The default value is `parallel` since it's faster. 93 | - It's recommended to use `sequential` when reporting bugs since the error descriptions are usually more detailed. 94 | * `ignore` (`[file:filename_all() | {file:filename_all(), hank_rule:t() | [hank_rule:t()]} | {file:filename_all(), hank_rule:t() | [hank_rule:t()], list()}]`): 95 | - List of wildcard patterns representing the files and rules that Hank will ignore when formatting. Tuple format is used to ignore either a specific rule or a set of rules in those files. 96 | ```erlang 97 | % single rule 98 | {hank, 99 | [{ignore, [ 100 | "rel/**/*", 101 | "lib/some_module.erl", 102 | {"test/**/*.erl", unnecessary_function_arguments} 103 | ]}]} 104 | ``` 105 | or 106 | ```erlang 107 | % set of rules (after expansion, the same wildcard and options are used for all rules) 108 | {hank, 109 | [{ignore, [ 110 | {"test/**/*.erl", [unused_macros, unnecessary_function_arguments]} 111 | ]}]} 112 | ``` 113 | - For ignoring options for `unused_configuration_options` rule, set a list of keys from the .config files you want to ignore. 114 | ```erlang 115 | {hank, 116 | [{ignore, [ 117 | {"sys.config", unused_configuration_options, [not_used_but_ignored]} 118 | ]}]} 119 | ``` 120 | - You can also ignore a specific file adding the attribute `-hank ignore.` to it. 121 | - And you can ignore specific rules adding the attribute `-hank [hank_rule:t()].` with the list of rules you want to ignore. 122 | 123 | ### Ignore specific rule items 124 | You can even ignore specific rule items with the `-hank` attribute by giving extra _ignore specifications_ for each rule, example: 125 | ```erlang 126 | -hank([single_use_hrls, %% Will ignore the whole rule within the module 127 | {unused_macros, 128 | ["ALL", %% Will ignore ?ALL, ?ALL() and ?ALL(X) 129 | {"ZERO", 0}, %% Will ignore ?ZERO() but not ?ZERO(X) nor ?ZERO 130 | {"ONE", 1}, %% Will ignore ?ONE(X) but not ?ONE() nor ?ONE 131 | {"NONE", none} %% Will ignore ?NONE but not ?NONE(X) nor ?NONE() 132 | ]}, 133 | {unused_record_fields, 134 | [a_record, %% Will ignore all fields in #a_record 135 | {a_record, a_field} %% Will ignore #a_record.a_field 136 | ]}, 137 | {unused_callbacks, 138 | [all, %% Will ignore all versions of the all callback (i.e. any arity) 139 | {just, 1} %% Will ignore just(term()) but not just() nor just(_, _) callbacks 140 | ]}, 141 | {unused_configuration_options, 142 | [option, %% Will ignore any appearance of option 143 | {"this_file.config", option} %% Will ignore option if it appears in "this_file.config" 144 | ]}, 145 | {single_use_hrl_attrs, 146 | ["ALL", %% Will ignore ?ALL, ?ALL() and ?ALL(X) 147 | {"ZERO", 0}, %% Will ignore ?ZERO() but not ?ZERO(X) nor ?ZERO 148 | {"ONE", 1}, %% Will ignore ?ONE(X) but not ?ONE() nor ?ONE 149 | {"NONE", none}, %% Will ignore ?NONE but not ?NONE(X) nor ?NONE() 150 | record_name %% Will ignore #record_name 151 | ]}, 152 | {unnecessary_function_arguments, 153 | [{ignore_me, 2}, %% Will ignore any unused argument from ignore_me/2 154 | {ignore_me_too, 3, 2}, %% Will ignore the 2nd argument from ignore_me_too/3 155 | ignore_me_again]}]). %% Will ignore any unused argument from any `ignore_me_again/x` within the module (no matter the arity) 156 | ``` 157 | Refer to each rule documentation for further details. 158 | 159 | ## Rules 160 | 161 | Find detailed information about the rules provided by Hank in [hex docs](https://hexdocs.pm/rebar3_hank/). 162 | 163 | ## Full Example 164 | 165 | [**@elbrujohalcon**](https://github.com/elbrujohalcon) presented Hank in a lightning talk at CodeBEAM V SF 2021. Watch his talk where he shows an example of using Hank in an iterative process: 166 | 167 | [![Hank @ CodeBEAM V SF 2021](http://img.youtube.com/vi/JWicgBIoUTM/0.jpg)](http://www.youtube.com/watch?v=JWicgBIoUTM "Hank @ CodeBEAM V SF 2021") 168 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [{elvis, 2 | [{config, 3 | [#{dirs => ["src", "src/**"], 4 | filter => "*.erl", 5 | ruleset => erl_files}, 6 | #{dirs => ["test"], %% Don't analyze test/files 7 | filter => "*.erl", 8 | ruleset => erl_files, 9 | rules => [{elvis_style, no_debug_call, disable}]}]}]}]. 10 | -------------------------------------------------------------------------------- /nextroll.dict: -------------------------------------------------------------------------------- 1 | # 2 | -hank 3 | -include 4 | -record 5 | -define 6 | .beam 7 | .src 8 | arities 9 | arity 10 | asts 11 | config 12 | epp 13 | erlang 14 | github 15 | hrl 16 | hrls 17 | i.e 18 | include_lib 19 | kiwf 20 | macro-skipping 21 | mfa 22 | multi-clause 23 | nif 24 | nif_error 25 | non-exported 26 | non-ignored 27 | non-otp 28 | otp 29 | paresable 30 | plugin 31 | project_apps 32 | rebar.config 33 | rebar3 34 | record_field 35 | record_type 36 | record_type_field 37 | single_use_hrl_attrs 38 | single_use_hrls 39 | todo 40 | typed_record_field 41 | unicode 42 | unnecessary_function_arguments 43 | unparseable 44 | unused_callbacks 45 | unused_macros 46 | unused_record_fields 47 | -------------------------------------------------------------------------------- /priv/overview.edoc: -------------------------------------------------------------------------------- 1 | ** this is the overview.doc file for rebar3_hank ** 2 | 3 | @author Brujo 4 | @author Pablo 5 | @author Diego 6 | @title Hank: The Erlang Dead Code Cleaner 7 | @doc This project provides a rebar3 plugin to use the rebar3 hank command. 8 | Use it to find dead code in your applications. 9 | 10 | Kill it with Fire 11 | 12 | @reference See our README for more information. 13 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, 2 | [warn_unused_import, warn_export_vars, warnings_as_errors, verbose, report, debug_info]}. 3 | 4 | {minimum_otp_vsn, "21"}. 5 | 6 | {deps, [{katana_code, "~> 2.1.0"}]}. 7 | 8 | {ex_doc, 9 | [{source_url, <<"https://github.com/AdRoll/rebar3_hank">>}, 10 | {extras, [<<"README.md">>, <<"LICENSE">>]}, 11 | {main, <<"readme">>}]}. 12 | 13 | {hex, [{doc, #{provider => ex_doc}}]}. 14 | 15 | {project_plugins, 16 | [{rebar3_hex, "~> 7.0.8"}, 17 | {rebar3_format, "~> 1.3.0"}, 18 | {rebar3_lint, "~> 3.2.5"}, 19 | {rebar3_sheldon, "~> 0.4.3"}, 20 | {rebar3_ex_doc, "~> 0.2.23"}]}. 21 | 22 | {dialyzer, 23 | [{warnings, 24 | [no_return, 25 | unmatched_returns, 26 | error_handling, 27 | missing_return, 28 | extra_return, 29 | no_unknown]}]}. 30 | 31 | {edoc_opts, 32 | [{todo, true}, 33 | {title, "Hank"}, 34 | {overview, "priv/overview.edoc"}, 35 | {packages, true}, 36 | {subpackages, true}, 37 | {source_path, "src"}, 38 | {application, rebar3_hank}, 39 | {new, true}, 40 | {report_missing_types, true}]}. 41 | 42 | {cover_enabled, true}. 43 | 44 | {cover_opts, [verbose]}. 45 | 46 | {format, 47 | [{files, 48 | ["priv/**/*.?rl", "src/**/*.app.src", "src/**/*.erl", "test/*.erl", "*.config"]}]}. 49 | 50 | {spellcheck, 51 | [{ignore_regex, 52 | "(eunit|=>|~t|_|[a-z][A-Z]|[*]|~>|[.]/|[A-Za-z0-9]/\\d|[.].rl|[.]config|[?][A-Za-z]|[a-z][:/][a-z]|\\d[.]\\d|[#][a-z0-9]|<[a-z/]|[a-z][(]|[{][a-z])"}, 53 | {files, ["src/**/*.?rl", "src/*.app.src", "test/**/*.?rl"]}, 54 | {additional_dictionaries, ["nextroll.dict", "test.dict"]}]}. 55 | 56 | {alias, 57 | [{test, [compile, format, spellcheck, lint, dialyzer, {ct, "--verbose"}, cover, edoc]}]}. 58 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"katana_code">>,{pkg,<<"katana_code">>,<<"2.1.1">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"katana_code">>, <<"9AC515E6B5AE4903CD7B6C9161ABFBA49B610B6F3E19E8F0542802A4316C2405">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"katana_code">>, <<"0680F33525B9A882E6F4D3022518B15C46F648BD7B0DBE86900980FE1C291404">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/hank.erl: -------------------------------------------------------------------------------- 1 | %%% @doc The Erlang Dead Code Cleaner 2 | -module(hank). 3 | 4 | %% It's dynamically called through rpc:pmap/3 5 | -ignore_xref([get_ast/1]). 6 | 7 | -export([analyze/5]). 8 | -export([get_ast/1]). 9 | 10 | -type ms() :: non_neg_integer(). 11 | -type stats() :: 12 | #{ignored := non_neg_integer(), 13 | parsing := ms(), 14 | analyzing => ms(), 15 | total => ms()}. 16 | -type parsing_style() :: parallel | sequential. 17 | 18 | -export_type([stats/0, parsing_style/0]). 19 | 20 | %% @doc Runs a list of rules over a list of files and returns all the 21 | %% dead code pieces it can find. 22 | -spec analyze([file:filename()], 23 | [hank_rule:ignore_spec()], 24 | [hank_rule:t()], 25 | parsing_style(), 26 | hank_context:t()) -> 27 | #{results := [hank_rule:result()], 28 | unused_ignores := [hank_rule:ignore_spec()], 29 | stats := stats()}. 30 | analyze(Files, IgnoreSpecsFromState, Rules, ParsingStyle, Context) -> 31 | StartMs = erlang:monotonic_time(millisecond), 32 | {ParsingNanos, ASTs} = timer:tc(fun() -> get_asts(Files, ParsingStyle) end), 33 | FilesAndASTs = lists:zip(Files, ASTs), 34 | HankAttributes = 35 | [{File, Attribute} || {File, AST} <- FilesAndASTs, Attribute <- hank_attributes(AST)], 36 | IgnoreRulesFromAST = 37 | [{File, IgnoreRule, IgnoreSpecs} 38 | || {File, Attribute} <- HankAttributes, 39 | not lists:member({File, all, []}, IgnoreSpecsFromState), 40 | {IgnoreRule, IgnoreSpecs} <- normalize_ignored_rules(Attribute, Rules)], 41 | IgnoreRulesFromConfig = 42 | [{File, Rule, Options} 43 | || {File, IgnoreRule, Options} <- IgnoreSpecsFromState, 44 | Rule <- Rules, 45 | IgnoreRule == all orelse IgnoreRule == Rule], 46 | WhollyIgnoredFiles = 47 | lists:usort([File || {File, all, _Options} <- IgnoreSpecsFromState] 48 | ++ [File || {File, ignore} <- HankAttributes]), 49 | IgnoreRules = IgnoreRulesFromAST ++ IgnoreRulesFromConfig, 50 | erlang:yield(), 51 | {AnalyzingNanos, AllResults} = 52 | timer:tc(fun() -> analyze(Rules, FilesAndASTs, Context) end), 53 | {Results, Ignored} = remove_ignored_results(AllResults, IgnoreRules), 54 | UnusedIgnores = unused_ignore_specs(WhollyIgnoredFiles, IgnoreRules, Ignored), 55 | TotalMs = erlang:monotonic_time(millisecond) - StartMs, 56 | #{results => Results, 57 | unused_ignores => UnusedIgnores, 58 | stats => 59 | #{ignored => length(Ignored), 60 | parsing => ParsingNanos div 1000, 61 | analyzing => AnalyzingNanos div 1000, 62 | total => TotalMs}}. 63 | 64 | -spec get_asts([file:filename()], parsing_style()) -> [erl_syntax:forms()]. 65 | get_asts(Files, parallel) -> 66 | rpc:pmap({?MODULE, get_ast}, [], Files); 67 | get_asts(Files, sequential) -> 68 | lists:map(fun get_ast/1, Files). 69 | 70 | %% @hidden Only used through rpc:pmap/3 71 | -spec get_ast(file:filename()) -> erl_syntax:forms(). 72 | get_ast(File) -> 73 | case ktn_dodger:parse_file(File, [no_fail, parse_macro_definitions]) of 74 | {ok, AST} -> 75 | AST; 76 | {error, OpenError} -> 77 | erlang:error({cant_parse, File, OpenError}) 78 | end. 79 | 80 | hank_attributes(AST) -> 81 | FoldFun = 82 | fun(Node, Acc) -> 83 | case erl_syntax:type(Node) of 84 | attribute -> 85 | try erl_syntax_lib:analyze_wild_attribute(Node) of 86 | {hank, Something} -> 87 | [Something | Acc]; 88 | _ -> 89 | Acc 90 | catch 91 | _:_ -> 92 | Acc 93 | end; 94 | _ -> 95 | Acc 96 | end 97 | end, 98 | erl_syntax_lib:fold(FoldFun, [], erl_syntax:form_list(AST)). 99 | 100 | normalize_ignored_rules(ignore, Rules) -> 101 | lists:map(fun normalize_ignored_rule/1, Rules); 102 | normalize_ignored_rules(RulesToIgnore, _) -> 103 | lists:map(fun normalize_ignored_rule/1, RulesToIgnore). 104 | 105 | normalize_ignored_rule(Rule) when is_atom(Rule) -> 106 | {Rule, all}; 107 | normalize_ignored_rule({Rule, Specs}) -> 108 | {Rule, Specs}. 109 | 110 | remove_ignored_results(AllResults, IgnoreRules) -> 111 | remove_ignored_results(AllResults, IgnoreRules, {[], []}). 112 | 113 | remove_ignored_results([], _, {FilteredResults, IgnoredResults}) -> 114 | {lists:reverse(FilteredResults), lists:reverse(IgnoredResults)}; 115 | remove_ignored_results([Result | Results], 116 | IgnoreRules, 117 | {FilteredResults, IgnoredResults}) -> 118 | #{file := File, 119 | rule := Rule, 120 | pattern := Pattern} = 121 | Result, 122 | IgnoreSpecs = ignore_specs(File, Rule, IgnoreRules), 123 | NewAcc = 124 | case lists:search(fun(IgnoreSpec) -> hank_rule:is_ignored(Rule, Pattern, IgnoreSpec) end, 125 | IgnoreSpecs) 126 | of 127 | {value, IgnoreSpec} -> 128 | {FilteredResults, [Result#{ignore_spec => IgnoreSpec} | IgnoredResults]}; 129 | false -> 130 | {[Result | FilteredResults], IgnoredResults} 131 | end, 132 | remove_ignored_results(Results, IgnoreRules, NewAcc). 133 | 134 | ignore_specs(File, Rule, IgnoreRules) -> 135 | Fun = fun ({File0, Rule0, Specs}, IgnoreSpecs) 136 | when File =:= File0 andalso Rule =:= Rule0 -> 137 | case is_list(Specs) of 138 | true -> 139 | Specs ++ IgnoreSpecs; 140 | false -> 141 | [Specs | IgnoreSpecs] 142 | end; 143 | (_, IgnoreSpecs) -> 144 | IgnoreSpecs 145 | end, 146 | lists:foldl(Fun, [], IgnoreRules). 147 | 148 | unused_ignore_specs(WhollyIgnoredFiles, IgnoreRules, IgnoredResults) -> 149 | FilteredIgnoreRules = 150 | [IR || IR = {File, _, _} <- IgnoreRules, not lists:member(File, WhollyIgnoredFiles)], 151 | lists:foldl(fun ({File, Rule, all}, Acc) -> 152 | case unused_ignored_spec(File, Rule, all, IgnoredResults) of 153 | true -> 154 | [{File, Rule, all} | Acc]; 155 | false -> 156 | Acc 157 | end; 158 | ({File, Rule, Specs}, Acc) -> 159 | case [Spec 160 | || Spec <- Specs, 161 | unused_ignored_spec(File, Rule, Spec, IgnoredResults)] 162 | of 163 | [] -> 164 | Acc; 165 | UnusedSpecs -> 166 | [{File, Rule, UnusedSpecs} | Acc] 167 | end 168 | end, 169 | [], 170 | FilteredIgnoreRules). 171 | 172 | unused_ignored_spec(File, Rule, Spec, IgnoredResults) -> 173 | not 174 | lists:any(fun(#{file := File0, 175 | rule := Rule0, 176 | ignore_spec := Spec0}) -> 177 | File == File0 andalso Rule == Rule0 andalso Spec == Spec0 178 | end, 179 | IgnoredResults). 180 | 181 | analyze(Rules, ASTs, Context) -> 182 | [Result || Rule <- Rules, Result <- hank_rule:analyze(Rule, ASTs, Context)]. 183 | -------------------------------------------------------------------------------- /src/hank_context.erl: -------------------------------------------------------------------------------- 1 | %%% @doc rebar3 contextual data turned into a type that we can also manually build, if needed 2 | -module(hank_context). 3 | 4 | -type apps_dirs() :: #{atom() => file:filename()}. 5 | 6 | -opaque t() :: #{app_dirs := apps_dirs(), project_apps := [atom()]}. 7 | 8 | -export_type([t/0, apps_dirs/0]). 9 | 10 | -export([from_rebar_state/1, new/2]). 11 | -export([app_dir/2, project_apps/1]). 12 | 13 | %% @doc Build a context from a rebar3 state. 14 | -spec from_rebar_state(rebar_state:t()) -> t(). 15 | from_rebar_state(State) -> 16 | AppDirs = 17 | [{binary_to_atom(rebar_app_info:name(App), utf8), rebar_app_info:dir(App)} 18 | || App <- rebar_state:project_apps(State)], 19 | ProjectApps = proplists:get_keys(AppDirs), 20 | new(maps:from_list(AppDirs), ProjectApps). 21 | 22 | %% @doc Build a context from scratch. 23 | -spec new(apps_dirs(), [atom()]) -> t(). 24 | new(AppDirs, ProjectApps) -> 25 | #{app_dirs => AppDirs, project_apps => ProjectApps}. 26 | 27 | %% @doc Return the root folder for an app. 28 | -spec app_dir(atom(), t()) -> undefined | file:filename(). 29 | app_dir(App, #{app_dirs := AppDirs}) -> 30 | maps:get(App, AppDirs, undefined). 31 | 32 | %% @doc OTP applications included in the project. 33 | -spec project_apps(t()) -> [atom()]. 34 | project_apps(#{project_apps := ProjectApps}) -> 35 | ProjectApps. 36 | -------------------------------------------------------------------------------- /src/hank_rule.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Behavior for defining a rule for Hank. 2 | -module(hank_rule). 3 | 4 | -type t() :: module(). 5 | -type asts() :: [{file:filename(), erl_syntax:forms()}]. 6 | -type result() :: 7 | #{file := file:filename(), 8 | line := non_neg_integer(), 9 | text := iodata(), 10 | rule => t(), 11 | pattern => ignore_pattern()}. 12 | -type ignore_pattern() :: undefined | tuple(). 13 | -type ignore_spec() :: {file:filename(), t() | all} | {file:filename(), t(), term()}. 14 | 15 | -export_type([t/0, asts/0, result/0, ignore_pattern/0, ignore_spec/0]). 16 | 17 | -callback analyze(asts(), hank_context:t()) -> [result()]. 18 | -callback ignored(ignore_pattern(), term()) -> boolean(). 19 | 20 | -export([default_rules/0]). 21 | -export([analyze/3]). 22 | -export([is_ignored/3]). 23 | 24 | %% @doc The list of default rules to apply 25 | -spec default_rules() -> []. 26 | default_rules() -> 27 | [Module 28 | || File 29 | <- filelib:wildcard( 30 | filename:join([code:lib_dir(rebar3_hank), "**/*.beam"])), 31 | Module <- [list_to_atom(filename:basename(File, ".beam"))], 32 | {behaviour, Behaviours} <- Module:module_info(attributes), 33 | lists:member(?MODULE, Behaviours)]. 34 | 35 | %% @doc Analyze the given files with the rule. 36 | -spec analyze(t(), asts(), hank_context:t()) -> [result()]. 37 | analyze(Rule, ASTs, Context) -> 38 | try 39 | [Result#{rule => Rule} || Result <- Rule:analyze(ASTs, Context)] 40 | catch 41 | _:Error:Stack -> 42 | logger:error("~p:analyze/3 failed with Error ~p \nStack: ~p", [Rule, Error, Stack]), 43 | erlang:error(analize_error) 44 | end. 45 | 46 | %% @doc Check if given rule should be ignored from results 47 | -spec is_ignored(t(), ignore_pattern(), all | term()) -> boolean(). 48 | is_ignored(Rule, Pattern, IgnoreSpec) -> 49 | IgnoreSpec =:= all orelse Rule:ignored(Pattern, IgnoreSpec). 50 | -------------------------------------------------------------------------------- /src/hank_utils.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Utility functions 2 | -module(hank_utils). 3 | 4 | %% Allow erl_syntax:syntaxTree/0 type spec 5 | -elvis([{elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z][a-z0-9]*_?)*$"}}]). 6 | 7 | -export([macro_arity/1, macro_name/1, macro_definition_name/1, function_name/1, 8 | function_tuple/1, function_description/1, application_node_to_mfa/1, 9 | macro_from_control_flow_attr/1, attr_name/1, node_has_attrs/2, attr_args_concrete/2, 10 | is_old_test_suite/1, node_line/1, paths_match/2, format_text/2, node_has_atom/2]). 11 | 12 | %% @doc Get the macro arity of given Node 13 | -spec macro_arity(erl_syntax:syntaxTree()) -> none | pos_integer(). 14 | macro_arity(Node) -> 15 | case erl_syntax:macro_arguments(Node) of 16 | none -> 17 | none; 18 | Args -> 19 | length(Args) 20 | end. 21 | 22 | %% @doc Get the parsed macro name of given Node 23 | -spec macro_name(erl_syntax:syntaxTree()) -> unknown | string(). 24 | macro_name(Node) -> 25 | parse_node_name(erl_syntax:macro_name(Node)). 26 | 27 | %% @doc Parse the given Node name 28 | -spec parse_node_name(erl_syntax:syntaxTree()) -> unknown | string(). 29 | parse_node_name(Node) -> 30 | case erl_syntax:type(Node) of 31 | variable -> 32 | erl_syntax:variable_literal(Node); 33 | atom -> 34 | erl_syntax:atom_name(Node); 35 | macro -> 36 | parse_node_name(erl_syntax:macro_name(Node)); 37 | _Other -> 38 | % Probably a case, a record field or some other block of code 39 | unknown 40 | end. 41 | 42 | %% @doc Get the macro definition name and arity of a given Macro Node. 43 | -spec macro_definition_name(erl_syntax:syntaxTree()) -> {string(), integer() | atom()}. 44 | macro_definition_name(Node) -> 45 | [MacroNameNode | _] = erl_syntax:attribute_arguments(Node), 46 | case erl_syntax:type(MacroNameNode) of 47 | application -> 48 | Operator = erl_syntax:application_operator(MacroNameNode), 49 | MacroName = parse_node_name(Operator), 50 | MacroArity = length(erl_syntax:application_arguments(MacroNameNode)), 51 | {MacroName, MacroArity}; 52 | variable -> 53 | {erl_syntax:variable_literal(MacroNameNode), none}; 54 | atom -> 55 | {erl_syntax:atom_literal(MacroNameNode), none} 56 | end. 57 | 58 | %% @doc Get the function name of a given Function Node. 59 | -spec function_name(erl_syntax:syntaxTree()) -> string(). 60 | function_name(Node) -> 61 | FuncNameNode = erl_syntax:function_name(Node), 62 | case erl_syntax:type(FuncNameNode) of 63 | macro -> 64 | [$? | macro_name(FuncNameNode)]; 65 | atom -> 66 | erl_syntax:atom_name(FuncNameNode) 67 | end. 68 | 69 | %% @doc Get the function definition tuple {name, arity} of a given Function Node. 70 | -spec function_tuple(erl_syntax:syntaxTree()) -> {atom(), pos_integer()}. 71 | function_tuple(Node) -> 72 | {erlang:list_to_atom(function_name(Node)), erl_syntax:function_arity(Node)}. 73 | 74 | %% @doc Get the function definition name and arity of a given Function Node. 75 | -spec function_description(erl_syntax:syntaxTree()) -> string(). 76 | function_description(Node) -> 77 | FuncName = function_name(Node), 78 | FuncArity = erl_syntax:function_arity(Node), 79 | FuncName ++ [$/ | integer_to_list(FuncArity)]. 80 | 81 | %% @doc Returns a MFA tuple for given application node 82 | -spec application_node_to_mfa(erl_syntax:syntaxTree()) -> 83 | undefined | 84 | {unknown | string(), 85 | unknown | string(), 86 | [erl_syntax:syntaxTree()]} | 87 | {string(), [erl_syntax:syntaxTree()]}. 88 | application_node_to_mfa(Node) -> 89 | case erl_syntax:type(Node) of 90 | application -> 91 | Operator = erl_syntax:application_operator(Node), 92 | case erl_syntax:type(Operator) of 93 | module_qualifier -> 94 | Module = erl_syntax:module_qualifier_argument(Operator), 95 | Function = erl_syntax:module_qualifier_body(Operator), 96 | {parse_node_name(Module), 97 | parse_node_name(Function), 98 | erl_syntax:application_arguments(Node)}; 99 | atom -> 100 | {erl_syntax:atom_name(Operator), erl_syntax:application_arguments(Node)}; 101 | variable -> 102 | {erl_syntax:variable_literal(Operator), erl_syntax:application_arguments(Node)}; 103 | _ -> 104 | undefined 105 | end; 106 | _ -> 107 | undefined 108 | end. 109 | 110 | %% @doc Generates a macro from the variable that's used in a control flow attribute. 111 | %% e.g. returns ?MACRO if it receives -ifdef(MACRO). 112 | -spec macro_from_control_flow_attr(erl_syntax:syntaxTree()) -> erl_syntax:syntaxTree(). 113 | macro_from_control_flow_attr(Node) -> 114 | [MacroName | _] = erl_syntax:attribute_arguments(Node), 115 | erl_syntax:macro(MacroName). 116 | 117 | %% @doc Macro dodging version of erl_syntax:attribute_name/1 118 | -spec attr_name(erl_syntax:syntaxTree()) -> atom() | string() | term(). 119 | attr_name(Node) -> 120 | N = erl_syntax:attribute_name(Node), 121 | try 122 | erl_syntax:concrete(N) 123 | catch 124 | _:_ -> 125 | N 126 | end. 127 | 128 | %% @doc Whether the given Node node 129 | %% has defined the given AttrNames attribute names or not 130 | -spec node_has_attrs(erl_syntax:syntaxTree(), atom() | [atom()]) -> boolean(). 131 | node_has_attrs(Node, AttrName) when not is_list(AttrName) -> 132 | node_has_attrs(Node, [AttrName]); 133 | node_has_attrs(Node, AttrNames) -> 134 | erl_syntax:type(Node) == attribute andalso lists:member(attr_name(Node), AttrNames). 135 | 136 | %% @doc Extract attribute arguments from given AST nodes list 137 | %% whose attribute name is AttrName and apply MapFunc to every element 138 | -spec attr_args(erl_syntax:forms(), atom() | [atom()], function()) -> [term()]. 139 | attr_args(AST, AttrName, MapFunc) when not is_list(AttrName) -> 140 | attr_args(AST, [AttrName], MapFunc); 141 | attr_args(AST, AttrNames, MapFunc) -> 142 | [MapFunc(AttrArg) 143 | || Node <- AST, 144 | node_has_attrs(Node, AttrNames), 145 | AttrArg <- erl_syntax:attribute_arguments(Node)]. 146 | 147 | %% @doc Same as attr_args/3 but calling erl_syntax:concrete/1 for each element 148 | -spec attr_args_concrete(erl_syntax:forms(), atom() | [atom()]) -> [term()]. 149 | attr_args_concrete(AST, AttrName) -> 150 | attr_args(AST, AttrName, fun erl_syntax:concrete/1). 151 | 152 | %% @doc Before OTP 23.2 test suites implemented an _implicit_ behavior. 153 | %% The only way to figure out that a module was actually a test suite was 154 | %% by its name. 155 | -spec is_old_test_suite(file:filename()) -> boolean(). 156 | is_old_test_suite(File) -> 157 | code:which(ct_suite) == non_existing % OTP < 23.2 158 | andalso re:run(File, "_SUITE.erl$") /= nomatch. 159 | 160 | %% @doc Returns the line number of the given node 161 | -spec node_line(erl_syntax:syntaxTree()) -> 162 | non_neg_integer() | {non_neg_integer(), pos_integer()}. 163 | node_line(Node) -> 164 | erl_anno:location( 165 | erl_syntax:get_pos(Node)). 166 | 167 | %% @doc Returns all the atoms found the given node list. 168 | -spec node_atoms([erl_syntax:syntaxTree()]) -> [atom()]. 169 | node_atoms(Nodes) -> 170 | FoldFun = 171 | fun(Node, Atoms) -> 172 | case erl_syntax:type(Node) of 173 | atom -> 174 | [Node | Atoms]; 175 | macro -> 176 | MacroName = erl_syntax:macro_name(Node), 177 | case erl_syntax:type(MacroName) of 178 | atom -> 179 | %% Note that erl_syntax_lib:fold/3 works in a DFS manner. 180 | %% That's why our macro-skipping trick works: 181 | %% it removes the atom that was previously introduced 182 | %% into the accumulator. 183 | Atoms -- [MacroName]; 184 | _ -> 185 | Atoms 186 | end; 187 | _ -> 188 | Atoms 189 | end 190 | end, 191 | AtomNodes = erl_syntax_lib:fold(FoldFun, [], erl_syntax:form_list(Nodes)), 192 | lists:usort( 193 | lists:map(fun erl_syntax:atom_value/1, AtomNodes)). 194 | 195 | %% @doc Whether one of the given paths is contained inside the other one or not. 196 | %% It doesn't matter which one is contained at which other. 197 | %% Verifies if FilePath and IncludePath refer both to the same file. 198 | %% Note that we can't just compare both filename:absname's here, since we 199 | %% don't really know what is the absolute path of the file referred by 200 | %% the include directive. 201 | -spec paths_match(string(), string()) -> boolean(). 202 | paths_match(IncludePath, IncludePath) -> 203 | % The path used in the include directive is exactly the file path 204 | true; 205 | paths_match(FilePath, IncludePath) -> 206 | % We remove relative paths because FilePath will not be a relative path and, 207 | % in any case, the paths will be relative to something that we don't know. 208 | % 209 | % Note that this might result in some false negatives. 210 | % For instance, Hank may think that lib/app1/include/header.hrl is used 211 | % if lib/app2/src/module.erl contains -include("header.hrl"). 212 | % when, in reality, module is including lib/app2/include/header.erl 213 | % That should be an extremely edge scenario and Hank never promised to find 214 | % ALL the dead code, anyway. It just promised that *if* it finds something, 215 | % that's dead code, 100% sure. 216 | compare_paths(clean_path(FilePath), clean_path(IncludePath)). 217 | 218 | %% @doc Whether one of the given paths is contained inside the other one or not 219 | %% It doesn't matter which one is contained at which other 220 | compare_paths({PathA, LenA}, {PathB, LenB}) when LenA > LenB -> 221 | PathB == string:find(PathA, PathB, trailing); 222 | compare_paths({PathA, _}, {PathB, _}) -> 223 | PathA == string:find(PathB, PathA, trailing); 224 | compare_paths(PathA, PathB) -> 225 | compare_paths({PathA, length(PathA)}, {PathB, length(PathB)}). 226 | 227 | %% @doc Removes "../" and "./" from a given Path 228 | clean_path(Path) -> 229 | unicode:characters_to_list( 230 | string:replace( 231 | string:replace(Path, "../", "", all), "./", "", all)). 232 | 233 | %% @doc Format rule result text for console output 234 | -spec format_text(string(), list()) -> binary(). 235 | format_text(Text, Args) -> 236 | Formatted = io_lib:format(Text, Args), 237 | case unicode:characters_to_binary(Formatted) of 238 | {_Error, Bin, _Rest} -> 239 | Bin; 240 | Bin -> 241 | Bin 242 | end. 243 | 244 | %% @doc Returns true if the node contains the atom. 245 | %% Only analyzes functions and attributes. 246 | -spec node_has_atom(erl_syntax:syntaxTree(), atom()) -> boolean(). 247 | node_has_atom(Node, Atom) -> 248 | ToCheck = 249 | case erl_syntax:type(Node) of 250 | function -> 251 | [Body 252 | || Clause <- erl_syntax:function_clauses(Node), 253 | Body <- erl_syntax:clause_body(Clause)]; 254 | attribute -> 255 | case attr_name(Node) of 256 | Name when Name == record; Name == define -> 257 | [_RecOrMacroName | Attrs] = erl_syntax:attribute_arguments(Node), 258 | Attrs; 259 | _ -> 260 | [] 261 | end; 262 | _ -> 263 | [] 264 | end, 265 | lists:member(Atom, node_atoms(ToCheck)). 266 | -------------------------------------------------------------------------------- /src/rebar3_hank.app.src: -------------------------------------------------------------------------------- 1 | {application, 2 | rebar3_hank, 3 | [{description, "A rebar plugin for dead code cleaning"}, 4 | {vsn, git}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib, syntax_tools, katana_code]}, 7 | {env, []}, 8 | {modules, []}, 9 | {licenses, ["MIT"]}, 10 | {links, [{"github", "https://github.com/AdRoll/rebar3_hank"}]}]}. 11 | -------------------------------------------------------------------------------- /src/rebar3_hank.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Main entry point for the rebar3 hank plugin 2 | -module(rebar3_hank). 3 | 4 | -export([init/1]). 5 | 6 | %% @private 7 | -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. 8 | init(State) -> 9 | rebar3_hank_prv:init(State). 10 | -------------------------------------------------------------------------------- /src/rebar3_hank_prv.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Plugin provider for rebar3 hank 2 | -module(rebar3_hank_prv). 3 | 4 | -export([init/1, do/1, format_error/1]). 5 | 6 | -define(FILES_PATTERN, "**/*.{erl,hrl,config,app.src,app.src.script}"). 7 | 8 | %% @private 9 | -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. 10 | init(State) -> 11 | HankProvider = 12 | providers:create([{name, hank}, 13 | {module, rebar3_hank_prv}, 14 | {bare, true}, 15 | {deps, [app_discovery]}, 16 | {example, "rebar3 hank"}, 17 | {opts, opts()}, 18 | {short_desc, "A rebar plugin for dead code cleaning"}, 19 | {desc, ""}]), 20 | KiwFProvider = 21 | providers:create([{name, kiwf}, 22 | {module, rebar3_hank_prv}, 23 | {bare, true}, 24 | {deps, [app_discovery]}, 25 | {example, "rebar3 kiwf"}, 26 | {opts, opts()}, 27 | {short_desc, "An alias for rebar3 hank"}, 28 | {desc, ""}]), 29 | {ok, 30 | rebar_state:add_provider( 31 | rebar_state:add_provider(State, HankProvider), KiwFProvider)}. 32 | 33 | opts() -> 34 | [{unused_ignores, 35 | $u, 36 | "unused_ignores", 37 | boolean, 38 | "Warn on unused ignores (default: true)."}]. 39 | 40 | %% @private 41 | -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, iodata()}. 42 | do(State) -> 43 | rebar_api:info("Looking for code to kill with fire...", []), 44 | Rules = get_rules(State), 45 | rebar_api:debug("Hank rules: ~p", [Rules]), 46 | Context = hank_context:from_rebar_state(State), 47 | rebar_api:debug("Hank Context: ~p", [Context]), 48 | %% All files except those under _build or _checkouts 49 | Files = [F || F <- filelib:wildcard(?FILES_PATTERN), not is_hidden(F)], 50 | rebar_api:debug("Hank will use ~p files for analysis: ~p", [length(Files), Files]), 51 | IgnoreSpecsFromState = 52 | case proplists:get_value(ignore, rebar_state:get(State, hank, []), none) of 53 | none -> 54 | []; 55 | IgnoreRules -> 56 | [{F, Rule, Options} 57 | || {Wildcard, Rule, Options} <- normalize(IgnoreRules), 58 | F <- filelib:wildcard(Wildcard)] 59 | end, 60 | ParsingStyle = 61 | proplists:get_value(parsing_style, rebar_state:get(State, hank, []), parallel), 62 | try hank:analyze(Files, IgnoreSpecsFromState, Rules, ParsingStyle, Context) of 63 | #{results := [], 64 | unused_ignores := UnusedIgnores, 65 | stats := Stats} -> 66 | instrument(Stats, UnusedIgnores, State), 67 | {ok, State}; 68 | #{results := Results, 69 | unused_ignores := UnusedIgnores, 70 | stats := Stats} -> 71 | instrument(Stats, UnusedIgnores, State), 72 | {error, format_results(Results)} 73 | catch 74 | Kind:Error:Stack -> 75 | rebar_api:warn("~p analyzing files: ~p\nStack: ~p", [Kind, Error, Stack]), 76 | {error, format_error(Error)} 77 | end. 78 | 79 | instrument(#{ignored := Ignored, 80 | parsing := Parsing, 81 | analyzing := Analyzing, 82 | total := Total}, 83 | UnusedIgnores, 84 | State) -> 85 | rebar_api:debug("Hank ignored ~p warnings", [Ignored]), 86 | rebar_api:debug("Hank spent ~pms parsing and ~pms analyzing the system (~pms total time)", 87 | [Parsing, Analyzing, Total]), 88 | {Args, _} = rebar_state:command_parsed_args(State), 89 | Verbose = 90 | case lists:keyfind(unused_ignores, 1, Args) of 91 | {unused_ignores, Value} -> 92 | Value; 93 | false -> 94 | true % The default is to print out warnings 95 | end, 96 | case {Verbose, UnusedIgnores} of 97 | {false, _} -> 98 | ok; 99 | {true, []} -> 100 | ok; 101 | {true, UnusedIgnores} -> 102 | Msg = "The following ignore specs are no longer needed and can be removed:\n" 103 | ++ lists:flatmap(fun format_unused_ignore/1, UnusedIgnores), 104 | rebar_api:warn(Msg, []) 105 | end. 106 | 107 | format_unused_ignore({File, Rule, all}) -> 108 | io_lib:format("* ~ts: ~p~n", [File, Rule]); 109 | format_unused_ignore({File, Rule, Specs}) -> 110 | io_lib:format("* ~ts: ~p: ~p~n", [File, Rule, Specs]). 111 | 112 | -spec format_results([hank_rule:result()]) -> string(). 113 | format_results(Results) -> 114 | lists:foldr(fun(Result, Acc) -> [Acc, format_result(Result), $\n] end, 115 | "The following pieces of code are dead and should be removed:\n", 116 | Results). 117 | 118 | format_result(#{file := File, 119 | line := Line, 120 | text := Msg}) -> 121 | hank_utils:format_text("~ts:~tp: ~ts", [File, Line, Msg]). 122 | 123 | %% @private 124 | %% @doc Determines files that should be fully hidden to Hank. 125 | is_hidden(Filename) -> 126 | lists:any(fun is_hidden_name/1, filename:split(Filename)). 127 | 128 | is_hidden_name(".") -> 129 | false; 130 | is_hidden_name("..") -> 131 | false; 132 | is_hidden_name("." ++ _) -> 133 | true; 134 | is_hidden_name("_" ++ _) -> 135 | true; 136 | is_hidden_name(_) -> 137 | false. 138 | 139 | %% @private 140 | -spec format_error(any()) -> binary(). 141 | format_error(Reason) -> 142 | hank_utils:format_text("~tp", [Reason]). 143 | 144 | -spec get_rules(rebar_state:t()) -> [hank_rule:t()]. 145 | get_rules(State) -> 146 | case proplists:get_value(rules, rebar_state:get(State, hank, []), all) of 147 | all -> 148 | hank_rule:default_rules(); 149 | Rules -> 150 | Rules 151 | end. 152 | 153 | normalize(IgnoreRules) -> 154 | lists:foldl(fun (WildcardRuleMaybeOpts, Acc) when is_tuple(WildcardRuleMaybeOpts) -> 155 | normalize_rules(WildcardRuleMaybeOpts, Acc); 156 | (Wildcard, Acc) -> 157 | [{Wildcard, all, all} | Acc] 158 | end, 159 | [], 160 | IgnoreRules). 161 | 162 | normalize_rules({Wildcard, Rules, Options}, Acc) when is_list(Rules) -> 163 | [{Wildcard, Rule, Options} || Rule <- Rules] ++ Acc; 164 | normalize_rules({Wildcard, Rule, Options}, Acc) -> 165 | normalize_rules({Wildcard, [Rule], Options}, Acc); 166 | normalize_rules({Wildcard, Rules}, Acc) when is_list(Rules) -> 167 | [{Wildcard, Rule, all} || Rule <- Rules] ++ Acc; 168 | normalize_rules({Wildcard, Rule}, Acc) -> 169 | normalize_rules({Wildcard, [Rule]}, Acc). 170 | -------------------------------------------------------------------------------- /src/rules/single_use_hrl_attrs.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect hrl attributes used in just one module: 2 | %% Attributes supported: 3 | %% -define 4 | %% -record 5 | %% It will suggest to place those attributes inside the module to avoid 6 | %% having (and including) a hrl file. 7 | %% 8 | %%

Note

9 | %%
10 | %% This rule assumes that hrl files will not be used outside your project. 11 | %% If you are writing a library that requires your clients to use some of 12 | %% your header files and attributes, you can add an ignore rule in 13 | %% rebar.config for it. 14 | %%
15 | -module(single_use_hrl_attrs). 16 | 17 | -behaviour(hank_rule). 18 | 19 | -export([analyze/2, ignored/2]). 20 | 21 | %% @doc This builds a list of header files with its attributes. 22 | %% Then traverse the file ASTs mapping their macros and records 23 | %% And checks whether they were used just once. 24 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 25 | analyze(FilesAndASTs, _Context) -> 26 | HrlDefs = hrl_attrs(FilesAndASTs), 27 | AttributesUsed = lists:foldl(fun file_using/2, #{}, FilesAndASTs), 28 | [build_macro_result(HrlFile, MacroKey, AttributesUsed) 29 | || {HrlFile, #{define := Defines}} <- HrlDefs, 30 | MacroKey <- Defines, 31 | is_used_only_once(HrlFile, MacroKey, AttributesUsed)] 32 | ++ [build_record_result(HrlFile, RecordKey, AttributesUsed) 33 | || {HrlFile, #{record := Records}} <- HrlDefs, 34 | RecordKey <- Records, 35 | is_used_only_once(HrlFile, RecordKey, AttributesUsed)]. 36 | 37 | build_macro_result(HrlFile, {Macro, Line}, AttributesUsed) -> 38 | [File] = maps:get(Macro, AttributesUsed), 39 | Text = 40 | case Macro of 41 | {MacroName, none} -> 42 | hank_utils:format_text("?~ts is used only at ~ts", [MacroName, File]); 43 | {MacroName, MacroArity} -> 44 | hank_utils:format_text("?~ts/~tp is used only at ~ts", 45 | [MacroName, MacroArity, File]) 46 | end, 47 | #{file => HrlFile, 48 | line => Line, 49 | text => Text, 50 | pattern => Macro}. 51 | 52 | build_record_result(HrlFile, {Record, Line}, AttributesUsed) -> 53 | [File] = maps:get(Record, AttributesUsed), 54 | #{file => HrlFile, 55 | line => Line, 56 | text => hank_utils:format_text("#~tp is used only at ~ts", [Record, File]), 57 | pattern => Record}. 58 | 59 | is_used_only_once(HrlFile, {Key, _Line}, AttributesUsed) -> 60 | case maps:get(Key, AttributesUsed, []) of 61 | [SingleFile] -> 62 | %% There is nothing wrong with using an attribute only in the same 63 | %% file where it's defined. 64 | SingleFile /= HrlFile; 65 | _ -> 66 | false 67 | end. 68 | 69 | file_using({File, FileAST}, CurrentFiles) -> 70 | AddFun = fun(Files) -> lists:usort([File | Files]) end, 71 | FoldFun = 72 | fun(Node, Result) -> 73 | case erl_syntax:type(Node) of 74 | macro -> 75 | Key = macro_application_name(Node), 76 | maps:update_with(Key, AddFun, [File], Result); 77 | Attr 78 | when Attr =:= record_expr; 79 | Attr =:= record_access; 80 | Attr =:= record_index_expr; 81 | Attr =:= record_type -> 82 | Key = record_name(Node, Attr), 83 | maps:update_with(Key, AddFun, [File], Result); 84 | attribute -> 85 | case hank_utils:attr_name(Node) of 86 | ControlFlowAttr 87 | when ControlFlowAttr == ifdef; 88 | ControlFlowAttr == ifndef; 89 | ControlFlowAttr == undef -> 90 | Key = macro_control_flow_name(Node), 91 | maps:update_with(Key, AddFun, [File], Result); 92 | _ -> 93 | Result 94 | end; 95 | _ -> 96 | Result 97 | end 98 | end, 99 | erl_syntax_lib:fold(FoldFun, CurrentFiles, erl_syntax:form_list(FileAST)). 100 | 101 | %% @doc It collects the hrl attrs like {file, [attrs]} 102 | hrl_attrs(FilesAndASTs) -> 103 | [{File, attrs(AST)} || {File, AST} <- FilesAndASTs, filename:extension(File) == ".hrl"]. 104 | 105 | %% @doc A map with #{define => [], record => []} for each hrl tree 106 | attrs(AST) -> 107 | FoldFun = 108 | fun(Node, #{define := Defines, record := Records} = Acc) -> 109 | case erl_syntax:type(Node) of 110 | attribute -> 111 | case hank_utils:attr_name(Node) of 112 | define -> 113 | maps:put(define, 114 | [{hank_utils:macro_definition_name(Node), line(Node)} 115 | | Defines], 116 | Acc); 117 | record -> 118 | maps:put(record, 119 | [{record_definition_name(Node), line(Node)} | Records], 120 | Acc); 121 | _ -> 122 | Acc 123 | end; 124 | _ -> 125 | Acc 126 | end 127 | end, 128 | erl_syntax_lib:fold(FoldFun, #{define => [], record => []}, erl_syntax:form_list(AST)). 129 | 130 | macro_control_flow_name(Node) -> 131 | macro_application_name(hank_utils:macro_from_control_flow_attr(Node)). 132 | 133 | macro_application_name(Node) -> 134 | {hank_utils:macro_name(Node), hank_utils:macro_arity(Node)}. 135 | 136 | record_definition_name(Node) -> 137 | try erl_syntax_lib:analyze_record_attribute(Node) of 138 | {RecordName, _} -> 139 | RecordName 140 | catch 141 | _:syntax_error -> 142 | %% There is a macro in the record definition 143 | "" 144 | end. 145 | 146 | record_name(Node, Type) -> 147 | RecordName = 148 | case Type of 149 | record_expr -> 150 | erl_syntax:record_expr_type(Node); 151 | record_index_expr -> 152 | erl_syntax:record_index_expr_type(Node); 153 | record_access -> 154 | erl_syntax:record_access_type(Node); 155 | record_type -> 156 | erl_syntax:record_type_name(Node) 157 | end, 158 | erl_syntax:atom_value(RecordName). 159 | 160 | line(Node) -> 161 | hank_utils:node_line(Node). 162 | 163 | %% @doc Rule ignore specifications. Example: 164 | %%
165 | %%      -hank([{single_use_hrl_attrs,
166 | %%              ["ALL",          %% Will ignore ?ALL, ?ALL() and ?ALL(X)
167 | %%               {"ZERO", 0},    %% Will ignore ?ZERO() but not ?ZERO(X) nor ?ZERO
168 | %%               {"ONE",  1},    %% Will ignore ?ONE(X) but not ?ONE()   nor ?ONE
169 | %%               {"NONE", none}, %% Will ignore ?NONE but not ?NONE(X) nor ?NONE()
170 | %%               record_name     %% Will ignore #record_name
171 | %%              ]},
172 | %%      
173 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 174 | ignored({MacroName, Arity}, {MacroName, Arity}) -> 175 | true; 176 | ignored({MacroName, _Arity}, MacroName) -> 177 | true; 178 | ignored(RecordName, RecordName) -> 179 | true; 180 | ignored(_Pattern, _IgnoreSpec) -> 181 | false. 182 | -------------------------------------------------------------------------------- /src/rules/single_use_hrls.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect header files used in just one module. 2 | %%

To avoid this warning, include the content of the header file into 3 | %% the module.

4 | %% 5 | %%

Note

6 | %%
7 | %% This rule assumes that hrl files will not be used outside your project. 8 | %% If you are writing a library that requires your clients to use some of 9 | %% your header files, you can add an ignore rule in rebar.config for it. 10 | %%
11 | -module(single_use_hrls). 12 | 13 | -behaviour(hank_rule). 14 | 15 | -export([analyze/2, ignored/2]). 16 | 17 | %% @private 18 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 19 | analyze(FilesAndASTs, _Context) -> 20 | [set_result(HeaderFile, IncludedAtFile) 21 | || {HeaderFile, [IncludedAtFile]} <- build_include_list(FilesAndASTs)]. 22 | 23 | set_result(HeaderFile, IncludedAtFile) -> 24 | #{file => HeaderFile, 25 | line => 0, 26 | text => 27 | hank_utils:format_text("This header file is only included at: ~ts", [IncludedAtFile]), 28 | pattern => undefined}. 29 | 30 | build_include_list(FilesAndASTs) -> 31 | {Files, _ASTs} = lists:unzip(FilesAndASTs), 32 | lists:foldl(fun({File, AST}, Acc) -> 33 | lists:foldl(fun(IncludedFile, AccInner) -> 34 | AtFiles = 35 | case lists:keyfind(IncludedFile, 1, AccInner) of 36 | false -> 37 | []; 38 | {IncludedFile, IncludedAtFiles} -> 39 | IncludedAtFiles 40 | end, 41 | NewTuple = {IncludedFile, [File | AtFiles]}, 42 | lists:keystore(IncludedFile, 1, AccInner, NewTuple) 43 | end, 44 | Acc, 45 | included_files(Files, AST)) 46 | end, 47 | [], 48 | FilesAndASTs). 49 | 50 | included_files(Files, AST) -> 51 | [included_file_path(Files, IncludedFile) 52 | || IncludedFile <- hank_utils:attr_args_concrete(AST, include), 53 | is_file_included(Files, IncludedFile) =/= false]. 54 | 55 | included_file_path(Files, IncludedFile) -> 56 | case is_file_included(Files, IncludedFile) of 57 | false -> 58 | IncludedFile; 59 | IncludedFileWithPath -> 60 | IncludedFileWithPath 61 | end. 62 | 63 | is_file_included(Files, IncludedFile) -> 64 | MatchFunc = fun(File) -> hank_utils:paths_match(IncludedFile, File) end, 65 | case lists:search(MatchFunc, Files) of 66 | {value, IncludedFileWithPath} -> 67 | IncludedFileWithPath; 68 | _ -> 69 | false 70 | end. 71 | 72 | %% @doc It doesn't make sense to provide individual ignore spec support here. 73 | %% The rule's basic unit is already a file. 74 | -spec ignored(hank_rule:ignore_pattern(), term()) -> false. 75 | ignored(undefined, _IgnoreSpec) -> 76 | false. 77 | -------------------------------------------------------------------------------- /src/rules/unnecessary_function_arguments.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect unnecessary function arguments. 2 | %%

The rule emits a warning for each function argument that is consistently 3 | %% ignored in all function clauses.

4 | %%

To avoid this warning, remove the unused argument(s).

5 | %%

Note

6 | %%
7 | %% This rule will not emit a warning if the function 8 | %% implements a NIF call (assuming that the stub function calls 9 | %% erlang:nif_error/1,2) or if it's a behaviour callback. 10 | %% In particular, the rule will not emit a warning for any exported 11 | %% function in modules that implement non-OTP behaviors or OTP behaviors 12 | %% that have dynamic callbacks, like gen_statem or ct_suite. 13 | %% It will also not emit a warning if the function is "known" 14 | %% even if not in a behaviour, like parse_transform/2. 15 | %%
16 | -module(unnecessary_function_arguments). 17 | 18 | %% Throw is used correctly in this module as a nonlocal return within a fold function 19 | -elvis([{elvis_style, no_throw, disable}]). 20 | 21 | -behaviour(hank_rule). 22 | 23 | -export([analyze/2, ignored/2]). 24 | 25 | %% Known OTP behaviours which do not implement dynamic callbacks like ct_suite. 26 | -define(KNOWN_BEHAVIOURS, 27 | [application, 28 | gen_event, 29 | gen_server, 30 | ssh_channel, 31 | ssh_client_channel, 32 | ssh_client_key_api, 33 | ssh_server_channel, 34 | ssh_server_key_api, 35 | ssl_crl_cache_api, 36 | ssl_session_cache_api, 37 | supervisor, 38 | supervisor_bridge, 39 | tftp]). 40 | 41 | %% Allow erl_syntax:syntaxTree/0 type spec 42 | %% Allow Module:behaviour_info/1 call 43 | -elvis([{elvis_style, invalid_dynamic_call, disable}, 44 | {elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z][a-z0-9]*_?)*$"}}]). 45 | 46 | -type imp_callbacks() :: #{File :: string() => [tuple()] | syntax_error}. 47 | 48 | %% @private 49 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 50 | analyze(FilesAndASTs, _Context) -> 51 | ImpCallbacks = callback_usage(FilesAndASTs), 52 | [Result 53 | || {File, AST} <- FilesAndASTs, 54 | not hank_utils:is_old_test_suite(File), 55 | is_parseable(File, ImpCallbacks), 56 | Node <- AST, 57 | erl_syntax:type(Node) == function, 58 | not is_exception_fun(hank_utils:function_tuple(Node)), 59 | not is_callback(Node, File, ImpCallbacks), 60 | Result <- analyze_function(File, Node)]. 61 | 62 | %% @doc Constructs a map with the callbacks of all the files. 63 | %% 1. collect all the behaviors that the file implements. 64 | %% 2. for each one of them, build the list of their possible callbacks. 65 | %% 3. if that list could not be built (usually because of macros), adds 'syntax_error' instead. 66 | -spec callback_usage(hank_rule:asts()) -> imp_callbacks(). 67 | callback_usage(FilesAndASTs) -> 68 | lists:foldl(fun({File, AST}, Result) -> 69 | FoldFun = 70 | fun(Node, FileCallbacks) -> 71 | case hank_utils:node_has_attrs(Node, [behaviour, behavior]) of 72 | true -> 73 | FileCallbacks ++ behaviour_callbacks(Node, AST); 74 | _ -> 75 | FileCallbacks 76 | end 77 | end, 78 | ResultsForFile = 79 | try 80 | erl_syntax_lib:fold(FoldFun, [], erl_syntax:form_list(AST)) 81 | catch 82 | syntax_error -> 83 | syntax_error 84 | end, 85 | maps:put(File, ResultsForFile, Result) 86 | end, 87 | #{}, 88 | FilesAndASTs). 89 | 90 | %% @doc Returns the behaviour's callback list if the given behaviour Node is a "known behaviour", 91 | %% this means it is an OTP behaviour without "dynamic" callbacks. 92 | %% If this is not satisfied or the behaviour attribute contains a macro, 93 | %% this function returns the whole list of functions that exported from the file. 94 | %% That's because, for dynamic behaviors, any exported function can be the implementation 95 | %% of a callback. 96 | -spec behaviour_callbacks(erl_syntax:syntaxTree(), erl_syntax:forms()) -> 97 | [{atom(), non_neg_integer()}]. 98 | behaviour_callbacks(Node, AST) -> 99 | try erl_syntax_lib:analyze_wild_attribute(Node) of 100 | {_, BehaviourMod} -> 101 | case lists:member(BehaviourMod, ?KNOWN_BEHAVIOURS) of 102 | true -> 103 | BehaviourMod:behaviour_info(callbacks); 104 | false -> 105 | module_exports(AST) 106 | end 107 | catch 108 | _:syntax_error -> 109 | %% There is a macro, then return all its exports, just in case 110 | module_exports(AST) 111 | end. 112 | 113 | -spec module_exports(erl_syntax:forms()) -> [{atom(), non_neg_integer()}]. 114 | module_exports(AST) -> 115 | FoldFun = 116 | fun(Node, {ExportAll, Exports, Functions} = Acc) -> 117 | case erl_syntax:type(Node) of 118 | attribute -> 119 | try erl_syntax_lib:analyze_attribute(Node) of 120 | {export, NewExports} -> 121 | {ExportAll, Exports ++ NewExports, Functions}; 122 | {compile, Opts} -> 123 | {ExportAll orelse has_export_all(Opts), Exports, Functions}; 124 | _ -> 125 | Acc 126 | catch 127 | _:syntax_error -> 128 | %% Probably macros, we can't parse this module 129 | throw(syntax_error) 130 | end; 131 | function -> 132 | Function = erl_syntax_lib:analyze_function(Node), 133 | {ExportAll, Exports, [Function | Functions]}; 134 | _ -> 135 | Acc 136 | end 137 | end, 138 | case erl_syntax_lib:fold(FoldFun, {false, [], []}, erl_syntax:form_list(AST)) of 139 | {true, _Exports, Functions} -> 140 | Functions; 141 | {false, Exports, _Functions} -> 142 | Exports 143 | end. 144 | 145 | has_export_all(List) when is_list(List) -> 146 | lists:member(export_all, List); 147 | has_export_all(export_all) -> 148 | true; 149 | has_export_all(_Opt) -> 150 | false. 151 | 152 | %% @doc It will check if arguments are ignored in all function clauses: 153 | %% [(_a, b, _c), (_x, b, c)] 154 | %% [[1, 0, 1], [1, 0, 0]] => [1, 0, 0] => warning 1st param! 155 | %% [(a, _b, c), (_, b, c)] 156 | %% [[0, 1, 0], [1, 0, 0]] => [0, 0, 0] => ok 157 | analyze_function(File, Function) -> 158 | lists:foldl(fun(Result, Acc) -> 159 | case set_result(File, Result) of 160 | ok -> 161 | Acc; 162 | Error -> 163 | [Error | Acc] 164 | end 165 | end, 166 | [], 167 | check_function(Function)). 168 | 169 | set_result(File, {error, Line, Text, IgnorePattern}) -> 170 | #{file => File, 171 | line => Line, 172 | text => Text, 173 | pattern => IgnorePattern}; 174 | set_result(_File, _) -> 175 | ok. 176 | 177 | check_function(FunctionNode) -> 178 | Clauses = erl_syntax:function_clauses(FunctionNode), 179 | ComputedResults = 180 | lists:foldl(fun(Clause, Result) -> 181 | case is_clause_a_nif_stub(Clause) of 182 | true -> 183 | Result; %% Discard NIF stubs! 184 | false -> 185 | Patterns = erl_syntax:clause_patterns(Clause), 186 | ClausePatterns = 187 | [pattern_to_integer(Pattern) || Pattern <- Patterns], 188 | check_unused_args(Result, ClausePatterns) 189 | end 190 | end, 191 | [], 192 | Clauses), 193 | check_computed_results(FunctionNode, ComputedResults). 194 | 195 | %% @doc Checks if the last expression in a clause body applies erlang:nif_error/x 196 | is_clause_a_nif_stub(Clause) -> 197 | LastClauseBodyNode = 198 | lists:last( 199 | erl_syntax:clause_body(Clause)), 200 | case hank_utils:application_node_to_mfa(LastClauseBodyNode) of 201 | {"erlang", "nif_error", _Args} -> 202 | true; 203 | _ -> 204 | false 205 | end. 206 | 207 | %% @doc Checks if the given function node implements a callback 208 | -spec is_callback(erl_syntax:syntaxTree(), string(), imp_callbacks()) -> boolean(). 209 | is_callback(FunctionNode, File, ImpCallbacks) -> 210 | lists:member( 211 | hank_utils:function_tuple(FunctionNode), maps:get(File, ImpCallbacks, [])). 212 | 213 | %% @doc Allows exceptions for functions whose name and arity are known but are 214 | %% not associated with a given behaviour (e.g. parse_transform/2) 215 | is_exception_fun({parse_transform, 2}) -> 216 | true; 217 | is_exception_fun(_) -> 218 | false. 219 | 220 | %% @doc Returns true if hank could parse the file. 221 | %% Otherwise the file is ignored and no warnings are reported for it 222 | -spec is_parseable(string(), imp_callbacks()) -> boolean(). 223 | is_parseable(File, ImpCallbacks) -> 224 | maps:get(File, ImpCallbacks, []) =/= syntax_error. 225 | 226 | %% @doc Computes position by position (multiply/and) 227 | %% Will be 1 only when an argument is unused over all the function clauses 228 | check_unused_args([], Arguments) -> 229 | Arguments; 230 | check_unused_args(Result, Arguments) -> 231 | lists:zipwith(fun(A, B) -> A * B end, Result, Arguments). 232 | 233 | pattern_to_integer({var, _Line, ArgNameAtom}) -> 234 | is_arg_ignored(atom_to_list(ArgNameAtom)); 235 | pattern_to_integer(_) -> 236 | 0. 237 | 238 | is_arg_ignored("_") -> 239 | 1; 240 | is_arg_ignored("_" ++ _) -> 241 | 1; 242 | is_arg_ignored(_) -> 243 | 0. 244 | 245 | check_computed_results(FunctionNode, Results) -> 246 | {_, Errors} = 247 | lists:foldl(fun(Result, {ArgNum, Errors}) -> 248 | NewErrors = 249 | case Result of 250 | 0 -> 251 | Errors; 252 | 1 -> 253 | [set_error(FunctionNode, ArgNum) | Errors] 254 | end, 255 | {ArgNum + 1, NewErrors} 256 | end, 257 | {1, []}, 258 | Results), 259 | Errors. 260 | 261 | set_error(FuncNode, ArgNum) -> 262 | Line = hank_utils:node_line(FuncNode), 263 | FuncDesc = hank_utils:function_description(FuncNode), 264 | Text = hank_utils:format_text("~ts doesn't need its #~p argument", [FuncDesc, ArgNum]), 265 | FuncName = hank_utils:function_name(FuncNode), 266 | IgnorePattern = {list_to_atom(FuncName), erl_syntax:function_arity(FuncNode), ArgNum}, 267 | {error, Line, Text, IgnorePattern}. 268 | 269 | %% @doc Rule ignore specifications. Example: 270 | %%
271 | %%      -hank([{unnecessary_function_arguments,
272 | %%               %% You can give a list of multiple specs or a single one
273 | %%               [%% Will ignore any unused argument from ignore_me/2 within the module
274 | %%                {ignore_me, 2},
275 | %%                %% Will ignore the 2nd argument from ignore_me_too/3 within the module
276 | %%                {ignore_me_too, 3, 2},
277 | %%                %% Will ignore any unused argument from any ignore_me_again/x
278 | %%                %% within the module (no matter the function arity)
279 | %%                ignore_me_again]}]).
280 | %%      
281 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 282 | ignored(Pattern, Pattern) -> 283 | true; 284 | ignored({FuncName, _, _}, FuncName) -> 285 | true; 286 | ignored({FuncName, FuncArity, _}, {FuncName, FuncArity}) -> 287 | true; 288 | ignored(_Pattern, _IgnoreSpec) -> 289 | false. 290 | -------------------------------------------------------------------------------- /src/rules/unused_callbacks.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect unused callbacks. 2 | %%

This rule will check all callbacks defined in a module and find 3 | %% those that are not used anywhere in the module itself.

4 | %%

It will emit a warning if it can't find the callback's atom name 5 | %% being used anywhere within a module. It will NOT emit a warning if an atom 6 | %% named as a callback is being used, no matter for what that atom is used.

7 | %%

This limitation is due to the fact that there are many ways to call 8 | %% a function in Erlang (particularly when dynamic calls are involved).

9 | %%

The assumption is that if you define a callback for a behavior, 10 | %% your generic module (where the callback is defined) should call 11 | %% that function at some point, using the implementation provided 12 | %% by the specific module (the one that implements the behavior).

13 | %%

To avoid this warning, remove the unused callback definition.

14 | %% 15 | %%

Note

16 | %%
17 | %% For this rule to apply, it's assumed that callbacks defined for a 18 | %% particular behavior are only used within the same module that defines it. 19 | %% If you define behaviors in your project and you use their callbacks from 20 | %% other modules, you can add an ignore rule in rebar.config 21 | %% for it. 22 | %%
23 | %% @todo [#81 + #82] Correctly handle macros 24 | -module(unused_callbacks). 25 | 26 | -behaviour(hank_rule). 27 | 28 | -export([analyze/2, ignored/2]). 29 | 30 | %% @private 31 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 32 | analyze(FilesAndASTs, _Context) -> 33 | [Result || {File, AST} <- FilesAndASTs, Result <- analyze_file(File, AST)]. 34 | 35 | analyze_file(File, AST) -> 36 | CbNodes = [Node || Node <- AST, hank_utils:node_has_attrs(Node, [callback])], 37 | Callbacks = 38 | lists:map(fun(CbNode) -> 39 | [CbDataArgs | _] = erl_syntax:attribute_arguments(CbNode), 40 | [CbDataTuple | _] = erl_syntax:tuple_elements(CbDataArgs), 41 | [CbName, CBArit] = erl_syntax:tuple_elements(CbDataTuple), 42 | {hank_utils:node_line(CbNode), 43 | erl_syntax:atom_value(CbName), 44 | erl_syntax:integer_value(CBArit)} 45 | end, 46 | CbNodes), 47 | analyze_callbacks(File, AST, Callbacks). 48 | 49 | analyze_callbacks(_File, _AST, []) -> 50 | []; %% Skip files with no callback definitions 51 | analyze_callbacks(File, AST, Callbacks) -> 52 | [set_result(File, Line, Callback, Arity) 53 | || {Line, Callback, Arity} <- Callbacks, not is_used_callback(Callback, AST)]. 54 | 55 | is_used_callback(Callback, Nodes) -> 56 | lists:any(fun(Node) -> hank_utils:node_has_atom(Node, Callback) end, Nodes). 57 | 58 | set_result(File, Line, Callback, Arity) -> 59 | #{file => File, 60 | line => Line, 61 | text => 62 | hank_utils:format_text("Callback ~tw/~B is not used anywhere in the module", 63 | [Callback, Arity]), 64 | pattern => {Callback, Arity}}. 65 | 66 | %% @doc Rule ignore specifications. Example: 67 | %%
68 | %%      -hank([{unused_callbacks,
69 | %%              [all, %% Will ignore all versions of the all callback (i.e. any arity)
70 | %%               {just, 1} %% Will ignore just(term()) but not just() nor just(_, _) callbacks
71 | %%              ]},
72 | %%      
73 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 74 | ignored({Callback, Arity}, {Callback, Arity}) -> 75 | true; 76 | ignored({Callback, _Arity}, Callback) -> 77 | true; 78 | ignored(_Pattern, _IgnoreSpec) -> 79 | false. 80 | -------------------------------------------------------------------------------- /src/rules/unused_configuration_options.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect unused configuration options 2 | %% It will find options that are no longer used around the code: 3 | %% - All the options from the *.config files 4 | %% (excepting rebar.config, elvis.config and relx.config) 5 | %% - The env list inside any *.app.src files 6 | %%

To avoid this warning, remove the unused parameters.

7 | %% 8 | %%

Note

9 | %%
10 | %% For this rule to apply, it's assumed that configuration options for an 11 | %% Erlang application are only consumed within said Erlang application or 12 | %% the other applications in the same umbrella project. 13 | %% If you have a dependency that consumes an environment parameter from one 14 | %% of your project applications, you can add an ignore rule in rebar.config 15 | %% for it. 16 | %%
17 | -module(unused_configuration_options). 18 | 19 | -behaviour(hank_rule). 20 | 21 | -export([analyze/2, ignored/2]). 22 | 23 | -define(IGNORED_FILES, ["rebar.config", "elvis.config", "relx.config"]). 24 | 25 | %% @doc Detects unused config options. 26 | %% It gets the options from .config and .app.src files and then: 27 | %%
    28 | %%
  1. Builds an index with file/options.
  2. 29 | %%
  3. Gets the atoms used around the .erl and .hrl files.
  4. 30 | %%
  5. Calculates the unused atoms (options) and return the results.
  6. 31 | %%
32 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 33 | analyze(FilesAndASTs, Context) -> 34 | % get the config options (keys) by file 35 | ConfigOptionsByFile = 36 | [{File, config_options(File, Context)} 37 | || {File, _AST} <- FilesAndASTs, 38 | filename:extension(File) == ".config" orelse filename:extension(File) == ".src", 39 | not is_ignored(File)], 40 | 41 | % get just the options (keys) to search usages 42 | ConfigOptions = extract_options(ConfigOptionsByFile), 43 | 44 | % get all the options used by the .erl/.hrl files 45 | Uses = 46 | [UsedOption 47 | || {File, AST} <- FilesAndASTs, 48 | filename:extension(File) == ".erl" orelse filename:extension(File) == ".hrl", 49 | UsedOption <- options_usage(AST, ConfigOptions)], 50 | 51 | % calculate the unused options 52 | UnusedOptions = ConfigOptions -- lists:usort(Uses), 53 | 54 | % build results 55 | [result(File, Option) 56 | || {File, Options} <- ConfigOptionsByFile, 57 | Option <- Options, 58 | lists:member(Option, UnusedOptions)]. 59 | 60 | %% @doc It receives a file path and returns a list of options 61 | %% It's prepared for .config and .app.src files, which contain Erlang Terms 62 | %% If the file cannot be parsed, it will be ignored (like other user's .config files) 63 | -spec config_options(file:filename(), hank_context:t()) -> [atom()]. 64 | config_options(File, Context) -> 65 | case file:consult(File) of 66 | {ok, [ErlangTerms]} -> 67 | case is_app_src_file(File) of 68 | true -> 69 | {application, _AppName, Options} = ErlangTerms, 70 | EnvOptions = proplists:get_value(env, Options, []), 71 | proplists:get_keys(EnvOptions); 72 | false -> 73 | config_keys(ErlangTerms, Context) 74 | end; 75 | _ -> 76 | %% error parsing: ignore the file! 77 | [] 78 | end. 79 | 80 | %% @doc Get all the config keys of the project_apps only. 81 | %% If ConfigTuples is a tuple (a one-tuple config file), it is converted into a proplist. 82 | %% When ConfigTuples files contain more than one tuple, they are parsed as a proplist. 83 | %% When ConfigTuples are not actually tuples, we just ignore the file. 84 | config_keys(ConfigTuples, Context) when is_tuple(ConfigTuples) -> 85 | config_keys([ConfigTuples], Context); 86 | config_keys(ConfigTuples, Context) when is_list(ConfigTuples) -> 87 | [Key 88 | || {AppName, Proplist} <- ConfigTuples, 89 | lists:member(AppName, hank_context:project_apps(Context)), 90 | Key <- proplists:get_keys(Proplist)]; 91 | config_keys(_NotTuples, _Context) -> 92 | []. 93 | 94 | is_app_src_file(File) -> 95 | filename:extension(File) == ".src". 96 | 97 | extract_options(OptionsByFile) -> 98 | lists:usort([Option || {_File, FileOptions} <- OptionsByFile, Option <- FileOptions]). 99 | 100 | options_usage(_AST, []) -> 101 | []; 102 | options_usage(AST, Options) -> 103 | [Option || Node <- AST, Option <- Options, hank_utils:node_has_atom(Node, Option)]. 104 | 105 | is_ignored(File) -> 106 | lists:member( 107 | filename:basename(File), ?IGNORED_FILES). 108 | 109 | result(File, Option) -> 110 | #{file => File, 111 | line => 0, 112 | text => hank_utils:format_text("~tw is not used anywhere in the code", [Option]), 113 | pattern => Option}. 114 | 115 | %% @doc Rule ignore specifications. 116 | %% Only valid in rebar.config since attributes are not allowed in config files. 117 | %% Example: 118 | %%
119 | %%      {hank, [{ignore, [
120 | %%          {"this_file.config", unused_configuration_options, [ignore_option]}
121 | %%      ]}]}.
122 | %%      
123 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 124 | ignored(Option, Option) -> 125 | true; 126 | ignored(_, _) -> 127 | false. 128 | -------------------------------------------------------------------------------- /src/rules/unused_hrls.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect unused header files. 2 | %%

To avoid this warning, remove the unused header files.

3 | %% 4 | %%

Note

5 | %%
6 | %% This rule assumes that hrl files will not be used outside your project. 7 | %% If you are writing a library that requires your clients to use some of 8 | %% your header files, you can add an ignore rule in rebar.config for it. 9 | %%
10 | %% @todo Figure out the absname of IncludePath 11 | %% [https://github.com/AdRoll/rebar3_hank/issues/31] 12 | -module(unused_hrls). 13 | 14 | -behaviour(hank_rule). 15 | 16 | -export([analyze/2, ignored/2]). 17 | 18 | %% @private 19 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 20 | analyze(FilesAndASTs, Context) -> 21 | {Files, ASTs} = lists:unzip(FilesAndASTs), 22 | IncludePaths = [IncludePath || AST <- ASTs, IncludePath <- include_paths(AST)], 23 | IncludeLibPaths = 24 | [expand_lib_dir(IncludeLibPath, Context) 25 | || AST <- ASTs, IncludeLibPath <- include_lib_paths(AST)], 26 | [#{file => File, 27 | line => 0, 28 | text => "This file is unused", 29 | pattern => undefined} 30 | || File <- Files, 31 | filename:extension(File) == ".hrl", 32 | is_unused_local(File, IncludePaths), 33 | is_unused_lib(File, IncludeLibPaths)]. 34 | 35 | include_paths(AST) -> 36 | [erl_syntax:concrete(IncludedFile) 37 | || Node <- AST, 38 | % Yeah, include_lib can also be used as include ¯\_(ツ)_/¯ (check epp's code) 39 | hank_utils:node_has_attrs(Node, [include, include_lib]), 40 | IncludedFile <- erl_syntax:attribute_arguments(Node)]. 41 | 42 | include_lib_paths(AST) -> 43 | hank_utils:attr_args_concrete(AST, include_lib). 44 | 45 | is_unused_local(FilePath, IncludePaths) -> 46 | not 47 | lists:any(fun(IncludePath) -> hank_utils:paths_match(IncludePath, FilePath) end, 48 | IncludePaths). 49 | 50 | is_unused_lib(File, IncludeLibPaths) -> 51 | % Note that IncludeLibPaths here are absolute paths, not relative ones. 52 | not 53 | lists:member( 54 | filename:absname(File), IncludeLibPaths). 55 | 56 | expand_lib_dir(IncludeLibPath, Context) -> 57 | [App | Path] = filename:split(IncludeLibPath), 58 | case hank_context:app_dir(list_to_atom(App), Context) of 59 | undefined -> 60 | IncludeLibPath; 61 | AppDir -> 62 | fname_join([AppDir | Path]) 63 | end. 64 | 65 | %% @doc Copied verbatim from epp:fname_join(Name). 66 | fname_join(["." | [_ | _] = Rest]) -> 67 | fname_join(Rest); 68 | fname_join(Components) -> 69 | filename:join(Components). 70 | 71 | %% @doc It doesn't make sense to provide individual ignore spec support here. 72 | %% The rule's basic unit is already a file. 73 | -spec ignored(hank_rule:ignore_pattern(), term()) -> false. 74 | ignored(undefined, _IgnoreSpec) -> 75 | false. 76 | -------------------------------------------------------------------------------- /src/rules/unused_macros.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect unused macros. 2 | %%

To avoid this warning, remove the unused macros.

3 | %% Note that for header files, this rule will fail to detect some unused 4 | %% macros. Particularly, in the case where you have an unused macro defined 5 | %% in a header file and another macro with the same name and arity defined 6 | %% somewhere else that is used. 7 | %% Since determining precisely what files are included in each -include 8 | %% attribute is not trivial, Hank will act conservatively and not make any 9 | %% effort to verify where each macro that's used is defined. 10 | %% So, if you have a project with multiple definitions of the same macro 11 | %% with the same arity... well... as long as one of them is used, none of 12 | %% them will be reported as unused. 13 | %% 14 | %%

Note

15 | %%
16 | %% This rule assumes that hrl files will not be used outside your project. 17 | %% If you are writing a library that requires your clients to use a macro 18 | %% defined in some of your header files, you can add an ignore rule in 19 | %% rebar.config for it. 20 | %%
21 | %% @todo Detect unparsable macros [https://github.com/AdRoll/rebar3_hank/issues/37] 22 | -module(unused_macros). 23 | 24 | -behaviour(hank_rule). 25 | 26 | -export([analyze/2, ignored/2]). 27 | 28 | %% @private 29 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 30 | analyze(FilesAndASTs, _Context) -> 31 | MacrosInFiles = lists:map(fun macro_usage/1, FilesAndASTs), 32 | AllUsedMacros = [UsedMacro || #{used := Used} <- MacrosInFiles, UsedMacro <- Used], 33 | [Result 34 | || #{file := File, 35 | defined := DefinedMacros, 36 | used := UsedMacros} 37 | <- MacrosInFiles, 38 | Result <- analyze(File, DefinedMacros, UsedMacros, AllUsedMacros)]. 39 | 40 | macro_usage({File, AST}) -> 41 | FoldFun = 42 | fun(Node, {Definitions, Usage}) -> 43 | case erl_syntax:type(Node) of 44 | attribute -> 45 | case hank_utils:attr_name(Node) of 46 | define -> 47 | {[Node | Definitions], Usage}; 48 | ControlFlowAttr 49 | when ControlFlowAttr == ifdef; 50 | ControlFlowAttr == ifndef; 51 | ControlFlowAttr == undef -> 52 | {Definitions, [hank_utils:macro_from_control_flow_attr(Node) | Usage]}; 53 | _ -> 54 | {Definitions, Usage} 55 | end; 56 | macro -> 57 | {Definitions, [Node | Usage]}; 58 | _ -> 59 | {Definitions, Usage} 60 | end 61 | end, 62 | {MacroDefinitions, MacroUsage} = 63 | erl_syntax_lib:fold(FoldFun, {[], []}, erl_syntax:form_list(AST)), 64 | DefinedMacros = lists:map(fun macro_definition_name_and_line/1, MacroDefinitions), 65 | UsedMacros = lists:map(fun macro_application_name/1, MacroUsage), 66 | #{file => File, 67 | defined => DefinedMacros, 68 | used => UsedMacros}. 69 | 70 | analyze(File, DefinedMacros, UsedMacros, AllUsedMacros) -> 71 | case filename:extension(File) of 72 | ".erl" -> 73 | analyze(File, DefinedMacros, UsedMacros); 74 | ".hrl" -> 75 | analyze(File, DefinedMacros, AllUsedMacros); 76 | _ -> 77 | [] 78 | end. 79 | 80 | analyze(File, DefinedMacros, UsedMacros) -> 81 | [result(File, MacroName, MacroArity, MacroLine) 82 | || {MacroName, MacroArity, MacroLine} <- DefinedMacros, 83 | not is_member({MacroName, MacroArity}, UsedMacros)]. 84 | 85 | macro_definition_name_and_line(Node) -> 86 | {MacroName, MacroArity} = hank_utils:macro_definition_name(Node), 87 | Line = hank_utils:node_line(Node), 88 | {MacroName, MacroArity, Line}. 89 | 90 | macro_application_name(Node) -> 91 | {hank_utils:macro_name(Node), hank_utils:macro_arity(Node)}. 92 | 93 | result(File, Name, Arity, Line) -> 94 | Text = 95 | case Arity of 96 | none -> 97 | hank_utils:format_text("?~ts is unused", [Name]); 98 | Arity -> 99 | hank_utils:format_text("?~ts/~p is unused", [Name, Arity]) 100 | end, 101 | #{file => File, 102 | line => Line, 103 | text => Text, 104 | pattern => {Name, Arity}}. 105 | 106 | %% @doc Rule ignore specifications. Example: 107 | %%
108 | %%      -hank([{unused_macros,
109 | %%              ["ALL", %% Will ignore ?ALL, ?ALL() and ?ALL(X)
110 | %%               {"ZERO", 0}, %% Will ignore ?ZERO() but not ?ZERO(X) nor ?ZERO
111 | %%               {"ONE",  1}, %% Will ignore ?ONE(X) but not ?ONE()   nor ?ONE
112 | %%               {"NONE", none} %% Will ignore ?NONE but not ?NONE(X) nor ?NONE()
113 | %%              ]},
114 | %%      
115 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 116 | ignored({Name, Arity}, {Name, Arity}) -> 117 | true; 118 | ignored({Name, _Arity}, Name) -> 119 | true; 120 | ignored(_Pattern, _IgnoreSpec) -> 121 | false. 122 | 123 | is_member({MacroName, none}, UsedMacros) -> 124 | lists:keymember(MacroName, 1, UsedMacros); 125 | is_member(Macro, UsedMacros) -> 126 | lists:member(Macro, UsedMacros). 127 | -------------------------------------------------------------------------------- /src/rules/unused_record_fields.erl: -------------------------------------------------------------------------------- 1 | %% @doc A rule to detect unused record fields. 2 | %%

The rule will detect fields that are defined as part of a record but 3 | %% never actually used anywhere.

4 | %%

To avoid this warning, remove the unused record fields.

5 | %% Note that for header files, this rule will fail to detect some unused 6 | %% fields. Particularly, in the case where you have an unused field defined 7 | %% in a header file and another record with the same name and the same field 8 | %% defined somewhere else that is used. 9 | %% Since determining precisely what files are included in each -include 10 | %% attribute is not trivial, Hank will act conservatively and not make 11 | %% any effort to verify where each record field that's used is defined. 12 | %% So, if you have a project with multiple definitions of the same record 13 | %% with the same field... well... as long as one of them is used, none of 14 | %% them will be reported as unused. 15 | %% 16 | %%

Note

17 | %%
18 | %% This rule assumes that your code will never use the underlying tuple 19 | %% structure of your records directly. 20 | %% If you do so, you can add an ignore rule in rebar.config for it. 21 | %%
22 | %% @todo Don't count record construction as usage [https://github.com/AdRoll/rebar3_hank/issues/35] 23 | -module(unused_record_fields). 24 | 25 | -behaviour(hank_rule). 26 | 27 | -export([analyze/2, ignored/2]). 28 | 29 | %% @private 30 | -spec analyze(hank_rule:asts(), hank_context:t()) -> [hank_rule:result()]. 31 | analyze(FilesAndASTs, _Context) -> 32 | Parsed = lists:map(fun field_usage/1, FilesAndASTs), 33 | AllUsedRecords = [UsedRecord || #{used_records := Used} <- Parsed, UsedRecord <- Used], 34 | AllUsedFields = [UsedField || #{used_fields := Used} <- Parsed, UsedField <- Used], 35 | [Result 36 | || #{file := File, 37 | record_definitions := RecordDefinitions, 38 | defined_fields := DefinedFields, 39 | used_records := UsedRecords, 40 | used_fields := UsedFields} 41 | <- Parsed, 42 | Result 43 | <- analyze(File, 44 | RecordDefinitions, 45 | DefinedFields, 46 | UsedRecords, 47 | UsedFields, 48 | AllUsedRecords, 49 | AllUsedFields)]. 50 | 51 | field_usage({File, AST}) -> 52 | FoldFun = 53 | fun(Node, {Records, Usage}) -> 54 | case erl_syntax:type(Node) of 55 | attribute -> 56 | case hank_utils:attr_name(Node) of 57 | record -> 58 | {[Node | Records], Usage}; 59 | _ -> 60 | {Records, Usage} 61 | end; 62 | record_expr -> 63 | {Records, [Node | Usage]}; 64 | record_access -> 65 | {Records, [Node | Usage]}; 66 | record_index_expr -> 67 | {Records, [Node | Usage]}; 68 | _ -> 69 | % Ignored: record_field, typed_record_field, record_type, record_type_field 70 | {Records, Usage} 71 | end 72 | end, 73 | {RecordDefinitions, RecordUsage} = 74 | erl_syntax_lib:fold(FoldFun, {[], []}, erl_syntax:form_list(AST)), 75 | DefinedFields = 76 | [{RecordName, FieldName} 77 | || Node <- RecordDefinitions, {RecordName, FieldName} <- analyze_record_attribute(Node)], 78 | {UsedRecords, UsedFields} = 79 | lists:foldl(fun(Node, {URs, UFs}) -> 80 | case analyze_record_expr(Node) of 81 | {RecordName, all_fields} -> 82 | {[RecordName | URs], UFs}; 83 | Fields -> 84 | {URs, Fields ++ UFs} 85 | end 86 | end, 87 | {[], []}, 88 | RecordUsage), 89 | #{file => File, 90 | record_definitions => RecordDefinitions, 91 | defined_fields => DefinedFields, 92 | used_records => UsedRecords, 93 | used_fields => UsedFields}. 94 | 95 | analyze(File, 96 | RecordDefinitions, 97 | DefinedFields, 98 | UsedRecords, 99 | UsedFields, 100 | AllUsedRecords, 101 | AllUsedFields) -> 102 | case filename:extension(File) of 103 | ".erl" -> 104 | analyze(File, RecordDefinitions, DefinedFields, UsedRecords, UsedFields); 105 | ".hrl" -> 106 | analyze(File, RecordDefinitions, DefinedFields, AllUsedRecords, AllUsedFields); 107 | _ -> 108 | [] 109 | end. 110 | 111 | analyze(File, RecordDefinitions, DefinedFields, UsedRecords, UsedFields) -> 112 | [result(File, RecordName, FieldName, RecordDefinitions) 113 | || {RecordName, FieldName} <- DefinedFields -- UsedFields, 114 | not lists:member(RecordName, UsedRecords)]. 115 | 116 | analyze_record_attribute(Node) -> 117 | try erl_syntax_lib:analyze_record_attribute(Node) of 118 | {RecordName, Fields} -> 119 | [{RecordName, FieldName} || {FieldName, _} <- Fields] 120 | catch 121 | _:syntax_error -> 122 | %% There is a macro in the record definition 123 | [] 124 | end. 125 | 126 | analyze_record_expr(Node) -> 127 | try erl_syntax_lib:analyze_record_expr(Node) of 128 | {record_expr, {RecordName, Fields}} -> 129 | [{RecordName, FieldName} || {FieldName, _} <- Fields]; 130 | {_, {RecordName, FieldName}} -> 131 | [{RecordName, FieldName}] 132 | catch 133 | _:syntax_error -> 134 | %% Probably the record expression uses stuff like #rec{_ = '_'} or Macros 135 | RecordName = 136 | case erl_syntax:type(Node) of 137 | record_expr -> 138 | erl_syntax:record_expr_type(Node); 139 | record_index_expr -> 140 | erl_syntax:record_index_expr_type(Node); 141 | record_access -> 142 | erl_syntax:record_access_type(Node) 143 | end, 144 | {erl_syntax:atom_value(RecordName), all_fields} 145 | end. 146 | 147 | result(File, RecordName, FieldName, RecordDefinitions) -> 148 | L = case find_record_definition(RecordName, RecordDefinitions) of 149 | false -> 150 | 0; 151 | {value, RecordDefinition} -> 152 | [_, RecordFields] = erl_syntax:attribute_arguments(RecordDefinition), 153 | case find_record_field(FieldName, erl_syntax:tuple_elements(RecordFields)) of 154 | false -> 155 | hank_utils:node_line(RecordDefinition); 156 | {value, FieldDefinition} -> 157 | hank_utils:node_line(FieldDefinition) 158 | end 159 | end, 160 | #{file => File, 161 | line => L, 162 | text => 163 | hank_utils:format_text("Field ~tp in record ~tp is unused", [FieldName, RecordName]), 164 | pattern => {RecordName, FieldName}}. 165 | 166 | find_record_definition(RecordName, Definitions) -> 167 | lists:search(fun(Definition) -> 168 | case erl_syntax:attribute_arguments(Definition) of 169 | [RN | _] -> 170 | erl_syntax:type(RN) == atom 171 | andalso erl_syntax:atom_value(RN) == RecordName; 172 | [] -> 173 | false 174 | end 175 | end, 176 | Definitions). 177 | 178 | find_record_field(FieldName, Definitions) -> 179 | lists:search(fun(Definition) -> 180 | {FN, _} = erl_syntax_lib:analyze_record_field(Definition), 181 | FN == FieldName 182 | end, 183 | Definitions). 184 | 185 | %% @doc Rule ignore specifications. Example: 186 | %%
187 | %%      -hank([{unused_record_fields,
188 | %%               [a_record, %% Will ignore all fields in #a_record
189 | %%                {a_record, a_field} %% Will ignore #a_record.a_field
190 | %%               ]}]).
191 | %%      
192 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 193 | ignored({RecordName, FieldName}, {RecordName, FieldName}) -> 194 | true; 195 | ignored({RecordName, _FieldName}, RecordName) -> 196 | true; 197 | ignored(_Pattern, _IgnoreSpec) -> 198 | false. 199 | -------------------------------------------------------------------------------- /test.dict: -------------------------------------------------------------------------------- 1 | _ 2 | _arg2 3 | a_rec 4 | a_record 5 | app0 6 | app1 7 | app1_include_lib 8 | app2 9 | app2_include_lib 10 | app_header_3 11 | arg2 12 | attr_αåβö 13 | global_rejector 14 | macro_0 15 | macro_1 16 | macro_all 17 | macro_from_config 18 | macro_none 19 | really_unused_field 20 | really_unused_record 21 | some_macro_3 22 | test_app 23 | test_app_suite 24 | unicode_αåβö 25 | unused_field 26 | unused_field_with_default 27 | unused_hrls 28 | unused_ignores 29 | unused_typed_field 30 | unused_typed_field_with_default 31 | used_macro_sample 32 | -------------------------------------------------------------------------------- /test/files/hidden/.hidden.module.erl: -------------------------------------------------------------------------------- 1 | -module('.hidden.module'). 2 | 3 | -define(UNUSED_MACRO, value). 4 | -------------------------------------------------------------------------------- /test/files/hidden/.hidden_folder/regular_module.erl: -------------------------------------------------------------------------------- 1 | -module(regular_module). 2 | 3 | -define(UNUSED_MACRO, value). 4 | -------------------------------------------------------------------------------- /test/files/hidden/_hidden_folder/regular_module.erl: -------------------------------------------------------------------------------- 1 | -module(regular_module). 2 | 3 | -define(UNUSED_MACRO, value). 4 | -------------------------------------------------------------------------------- /test/files/hidden/_hidden_module.erl: -------------------------------------------------------------------------------- 1 | -module('_hidden_module'). 2 | 3 | -define(UNUSED_MACRO, value). 4 | -------------------------------------------------------------------------------- /test/files/ignore/no_ignore.erl: -------------------------------------------------------------------------------- 1 | -module(no_ignore). 2 | 3 | %% This breaks erl_syntax_lib:analyze_attribute 4 | -ignore_xref([{?MODULE, no_ignore, 0}]). 5 | 6 | -export([no_ignore/0]). 7 | 8 | no_ignore(_, _) -> 9 | no_ignore. 10 | -------------------------------------------------------------------------------- /test/files/ignore/no_ignore.hrl: -------------------------------------------------------------------------------- 1 | -define(NO_IGNORE, no_ignore). 2 | -------------------------------------------------------------------------------- /test/files/ignore/rebar_config_ignore.erl: -------------------------------------------------------------------------------- 1 | -module(rebar_config_ignore). 2 | 3 | -define(unused_macro, unused_macro). 4 | 5 | unnecessary_function_arguments(_) -> unnecessary_function_arguments. 6 | -------------------------------------------------------------------------------- /test/files/ignore/specific_ignore.erl: -------------------------------------------------------------------------------- 1 | -module(specific_ignore). 2 | 3 | -hank([unused_macros, 4 | {unnecessary_function_arguments, [{no_ignore, 2}, {do_ignore, 1, 1}, do_ignore_me_too]}]). 5 | 6 | -define(UNUSED_MACRO, unused_macro). 7 | 8 | -export([no_ignore/2, do_ignore/1, do_ignore_me_too/1, do_ignore_me_too/2, 9 | do_ignore_me_too/3]). 10 | 11 | no_ignore(_, _) -> 12 | no_ignore. 13 | 14 | do_ignore(_Arg1) -> 15 | ok. 16 | 17 | do_ignore_me_too(_Arg1) -> 18 | ok. 19 | 20 | do_ignore_me_too(_Arg1, _Arg2) -> 21 | ok. 22 | 23 | do_ignore_me_too(_Arg1, _Arg2, _Arg3) -> 24 | ok. 25 | -------------------------------------------------------------------------------- /test/files/ignore/with_ignore.erl: -------------------------------------------------------------------------------- 1 | -module(with_ignore). 2 | 3 | -hank ignore. 4 | 5 | -export([with_ignore/0]). 6 | 7 | with_ignore(_, _) -> 8 | with_ignore. 9 | -------------------------------------------------------------------------------- /test/files/ignore/with_ignore.hrl: -------------------------------------------------------------------------------- 1 | -define(WITH_IGNORE, with_ignore). 2 | 3 | -hank ignore. 4 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/include/flow.hrl: -------------------------------------------------------------------------------- 1 | -define(IFNDEF, true). 2 | -define(IFDEF, true). 3 | -define(IF(X), not X). 4 | -define(UNDEF, undef). 5 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/include/header1.hrl: -------------------------------------------------------------------------------- 1 | -define(APP_HEADER_1, "this is header from app that will be used in just one module"). 2 | -define(SOME_MACRO_1(A), A). 3 | -define(THIS_MACRO, {is_used, "and", it, shouldnt, generate, a, warning}). 4 | -define(SOME_DEFINE, ?THIS_MACRO). 5 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/include/header2.hrl: -------------------------------------------------------------------------------- 1 | -define(APP_HEADER_2, "this is header from app that will be used in just one module"). 2 | -define(SOME_MACRO_2(A), A). 3 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/include/header3.hrl: -------------------------------------------------------------------------------- 1 | -define(APP_HEADER_3, "this is header from app that will be used in different modules"). 2 | -define(SOME_MACRO_3(A), A). 3 | 4 | -record(a_record, {used_field, used_typed_field :: used_typed_field}). 5 | -record(another_record, {used_field, used_typed_field :: used_typed_field}). 6 | %% Unicode should be supported and not break! 7 | -record('unicode_αåβö', {'attr_αåβö' :: a_type()}). 8 | 9 | %% This doesn't count as usage 10 | -type a_type() :: #a_record{}. 11 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/include/ignore.hrl: -------------------------------------------------------------------------------- 1 | -hank([{single_use_hrl_attrs, 2 | ["MACRO_ALL", 3 | {"MACRO_0", 0}, 4 | {"MACRO_1", 1}, 5 | {"MACRO_NONE", none}, 6 | ignored_record, 7 | reported_record]}]). 8 | 9 | -define(MACRO_ALL, "this macro is always ignored"). 10 | -define(MACRO_ALL(), "regardless of its arity"). 11 | -define(MACRO_ALL(It), "is never reported as unused"). 12 | -define(MACRO_ALL(Not, Even), "if it has multiple arguments"). 13 | -define(MACRO_0, "This version of the macro should not be ignored"). 14 | -define(MACRO_0(), "This one should since it has 0 arguments"). 15 | -define(MACRO_0(And), "this one should be reported since it has one"). 16 | -define(MACRO_0(Also, This), "one, that has 2"). 17 | -define(MACRO_1, "For this macro"). 18 | -define(MACRO_1(), "there should be a report"). 19 | -define(MACRO_1(But), "not for this version with arity 1"). 20 | -define(MACRO_1(Just, For), "all the others"). 21 | -define(MACRO_NONE, "This instance should be ignored"). 22 | -define(MACRO_NONE(), "But there should be a report"). 23 | -define(MACRO_NONE(For), "each of the"). 24 | -define(MACRO_NONE(Other, Versions), ""). 25 | 26 | -record(ignored_record, {list}). 27 | -record(reported_record, {list}). 28 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/src/app.app.src: -------------------------------------------------------------------------------- 1 | {application, app, [{vsn, "0"}]}. 2 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/src/app_include.erl: -------------------------------------------------------------------------------- 1 | -module(app_include). 2 | 3 | -include("header2.hrl"). 4 | -include("header3.hrl"). 5 | 6 | -export([my_function/0]). 7 | 8 | my_function() -> 9 | % those are only used here! 10 | Val = ?SOME_MACRO_2(?APP_HEADER_2), 11 | % and those are used in other module :) 12 | Val ++ ?SOME_MACRO_3(?APP_HEADER_3). 13 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/src/app_include_lib.erl: -------------------------------------------------------------------------------- 1 | -module(app_include_lib). 2 | 3 | -include_lib("app/include/header1.hrl"). 4 | 5 | -export([my_function/0]). 6 | 7 | my_function() -> 8 | % those are only used here! 9 | ?SOME_MACRO_1(?APP_HEADER_1). 10 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/src/app_other.erl: -------------------------------------------------------------------------------- 1 | -module(app_other). 2 | 3 | -include("ignore.hrl"). 4 | -include("header1.hrl"). 5 | -include("header3.hrl"). 6 | 7 | -record(yet_another_record, {another_record :: #another_record{}}). 8 | 9 | -export([my_function/0]). 10 | 11 | my_function() -> 12 | R = #a_record{used_field = used_field, used_typed_field = used_typed_field}, 13 | U = #'unicode_αåβö'{'attr_αåβö' = R}, 14 | _ = ?SOME_MACRO_3(U), 15 | ?APP_HEADER_3 ++ ?SOME_DEFINE. 16 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/src/flow.erl: -------------------------------------------------------------------------------- 1 | -module(flow). 2 | 3 | -include("flow.hrl"). 4 | 5 | -export([ifndef/0, iif/0]). 6 | 7 | -ifndef(IFNDEF). 8 | ifndef() -> notdefined. 9 | -else. 10 | ifndef() -> defined. 11 | -endif. 12 | 13 | 14 | -ifdef(IFDEF). 15 | -type ifdef() :: ifdef. 16 | -export_type([ifdef/0]). 17 | -endif. 18 | 19 | -if(?IF(?VALUE)). 20 | 21 | iif() -> true. 22 | 23 | -elif(not ?IF(?VALUE)). 24 | 25 | iif() -> false. 26 | 27 | -else. 28 | 29 | iif() -> wat. 30 | 31 | -endif. 32 | 33 | -undef(UNDEF). 34 | -------------------------------------------------------------------------------- /test/files/single_use_hrl_attrs/lib/app/src/ignore.erl: -------------------------------------------------------------------------------- 1 | -module ignore. 2 | 3 | -include("ignore.hrl"). 4 | 5 | -export([usage/0]). 6 | 7 | usage() -> 8 | #{ignored => 9 | #ignored_record{list = 10 | [?MACRO_ALL, 11 | ?MACRO_ALL(), 12 | ?MACRO_ALL(x), 13 | ?MACRO_ALL(x, x), 14 | ?MACRO_0(), 15 | ?MACRO_1(x), 16 | ?MACRO_NONE]}, 17 | reported => 18 | #reported_record{list = 19 | [?MACRO_0, 20 | ?MACRO_0(x), 21 | ?MACRO_0(x, x), 22 | ?MACRO_1, 23 | ?MACRO_1(), 24 | ?MACRO_1(x, x), 25 | ?MACRO_NONE(), 26 | ?MACRO_NONE(x), 27 | ?MACRO_NONE(x, x)]}}. 28 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/include/ignored.hrl: -------------------------------------------------------------------------------- 1 | -hank ignore. 2 | 3 | -define(HEADER_IGNORED, "this header file is included at include_ignored.erl"). 4 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/include/multi.hrl: -------------------------------------------------------------------------------- 1 | -define(HEADER_MULTI, 2 | "this header file is included at include_multi.erl and include_single.erl"). 3 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/include/single.hrl: -------------------------------------------------------------------------------- 1 | -define(HEADER_SINGLE, "this header file is included at include_single.erl"). 2 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/include/single_unicode.hrl: -------------------------------------------------------------------------------- 1 | -define(HEADER_SINGLE_UNICODE, "this header file is included at include_unicode_åö.erl"). 2 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/src/include_ignored.erl: -------------------------------------------------------------------------------- 1 | -module(include_ignored). 2 | 3 | -include("ignored.hrl"). 4 | -include("multi.hrl"). 5 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/src/include_missing.erl: -------------------------------------------------------------------------------- 1 | -module(include_missing). 2 | 3 | -include("missing.hrl"). 4 | -include("multi.hrl"). 5 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/src/include_multi.erl: -------------------------------------------------------------------------------- 1 | -module(include_multi). 2 | 3 | -include("multi.hrl"). 4 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/src/include_single.erl: -------------------------------------------------------------------------------- 1 | -module(include_single). 2 | 3 | -include("single.hrl"). 4 | -include("multi.hrl"). 5 | -------------------------------------------------------------------------------- /test/files/single_use_hrls/src/include_unicode_åö.erl: -------------------------------------------------------------------------------- 1 | -module(include_unicode_åö). 2 | 3 | -include("single_unicode.hrl"). 4 | -include("multi.hrl"). 5 | -------------------------------------------------------------------------------- /test/files/test_app/another_sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {test_app, [ 3 | {this_used_key, test}, 4 | {another_used_key, test}, 5 | {yet_another_key, ok}, 6 | {yet_another_key_2, ok}, 7 | {yet_another_key_3, [{part, [0,0,1]}]}, 8 | {my_great_option, 0}, 9 | {another_great_option, 0} 10 | ]} 11 | ]. 12 | -------------------------------------------------------------------------------- /test/files/test_app/elvis.config: -------------------------------------------------------------------------------- 1 | %% This is not really used, but the rule `unused_configuration_options` should ignore this whole file :) 2 | [ 3 | { 4 | elvis, 5 | [ 6 | {config, 7 | [#{dirs => ["include"], 8 | filter => "*.hrl", 9 | rules => [ 10 | {elvis_text_style, line_length, #{limit => 150, skip_comments => false}}, 11 | {elvis_text_style, no_tabs}, 12 | {elvis_text_style, no_trailing_whitespace}, 13 | {elvis_style, macro_module_names}, 14 | {elvis_style, no_if_expression}, 15 | {elvis_style, no_behavior_info} 16 | ] 17 | }, 18 | #{dirs => ["."], 19 | filter => "elvis.config", 20 | ruleset => elvis_config 21 | } 22 | ] 23 | } 24 | ] 25 | } 26 | ]. 27 | -------------------------------------------------------------------------------- /test/files/test_app/include/a_folder/a_header.hrl: -------------------------------------------------------------------------------- 1 | -define(A_SUB_MACRO, a_sub_macro). 2 | -------------------------------------------------------------------------------- /test/files/test_app/include/a_header.hrl: -------------------------------------------------------------------------------- 1 | -define(A_MACRO, a_macro). 2 | -------------------------------------------------------------------------------- /test/files/test_app/lib/a_project/src/a_folder/a_subfolder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/one_tuple.config: -------------------------------------------------------------------------------- 1 | %% Taken from erlang/otp 2 | {unix,[{telnet,"belegost"},{username,"telnet-test"},{password,"tset-tenlet"},{keep_alive,true}]}. 3 | -------------------------------------------------------------------------------- /test/files/test_app/rebar.config: -------------------------------------------------------------------------------- 1 | %% This is not really used, but the rule `unused_configuration_options` should ignore this whole file :) 2 | {erl_opts, [ 3 | debug_info, 4 | warnings_as_errors 5 | ]}. 6 | 7 | {minimum_otp_vsn, "23"}. 8 | 9 | {deps, []}. 10 | 11 | {profiles, []}. 12 | 13 | {project_plugins, [ 14 | {rebar3_format, "~> 0.9.0"} 15 | ]}. 16 | 17 | {eunit_opts, [verbose]}. 18 | 19 | {ct_opts, [{sys_config, "rebar3.test.config"}]}. 20 | -------------------------------------------------------------------------------- /test/files/test_app/relx.config: -------------------------------------------------------------------------------- 1 | %% This is not really used, but the rule `unused_configuration_options` should ignore this whole file :) 2 | {release, 3 | {erlorg, "1.0.0"}, 4 | [erlorg]}. 5 | 6 | {sys_config, "./rel/sys.config"}. 7 | 8 | { overlay 9 | , [ {copy, "assets", "assets"} 10 | ] 11 | }. 12 | 13 | {extended_start_script, true}. -------------------------------------------------------------------------------- /test/files/test_app/src/a_folder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/src/a_folder/a_subfolder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/src/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/src/test_app.app.src: -------------------------------------------------------------------------------- 1 | {application, test_app, [ 2 | {description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, {'test_app_a_module', []}}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {env, [ 11 | {my_config_from_app_src, an_atom}, 12 | {other_config_from_app_src, <<"other">>} 13 | ]}, 14 | {modules, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /test/files/test_app/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {test_app, [ 3 | {environment, test}, 4 | {a_key, ok}, 5 | {not_used_but_ignored, hum} 6 | ]}, 7 | "another_sys.config" 8 | ]. 9 | -------------------------------------------------------------------------------- /test/files/test_app/test/a_folder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/test/a_module_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/unconventional.config: -------------------------------------------------------------------------------- 1 | #{this => file, 2 | holds => an, 3 | unconventional => config, 4 | since => it, 5 | has => "a map", 6 | <<"instead_of">> => [a, list, 'of', tuples]}. 7 | -------------------------------------------------------------------------------- /test/files/test_app/user.config: -------------------------------------------------------------------------------- 1 | this-is-a-user-config-file: "which is yaml" 2 | other-props: 3 | - one 4 | - two 5 | - three 6 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/include/a_folder/a_header.hrl: -------------------------------------------------------------------------------- 1 | -define(A_SUB_MACRO, a_sub_macro). 2 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/include/a_header.hrl: -------------------------------------------------------------------------------- 1 | -define(A_MACRO, a_macro). 2 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/lib/a_project/src/a_folder/a_subfolder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | 3 | -include("a_header.hrl"). 4 | -include("a_folder/a_header.hrl"). 5 | 6 | -define(A_MACRO_WITH_AN_OPTION, application:get_env(my_config_from_app_src)). 7 | 8 | -export([a_function/0]). 9 | 10 | a_function() -> 11 | [?A_MACRO_WITH_AN_OPTION, 12 | ?A_MACRO, 13 | ?A_SUB_MACRO, 14 | application:get_env(test_app, environment, default_value)]. 15 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/lib/a_project/src/a_folder/a_subfolder/another_module.erl: -------------------------------------------------------------------------------- 1 | -module(another_module). 2 | 3 | -include("a_header.hrl"). 4 | -include("a_folder/a_header.hrl"). 5 | 6 | -define(A_COMPLEX_MACRO, 7 | fun() -> 8 | All = application:get_all_env(test_app), 9 | proplists:get_value(this_used_key, All) 10 | end). 11 | -define(A_COMPLEX_MACRO(App), 12 | fun() -> 13 | All = application:get_all_env(App), 14 | proplists:get_value(another_used_key, All) 15 | end). 16 | -define(A_COMPLEX_MACRO(), 17 | fun() -> 18 | Value = application:get_env(yet_another_key), 19 | list_to_binary(Value) 20 | end). 21 | -define(A_MACRO_FUN(), fun() -> application:get_env(yet_another_key_2) end). 22 | -define(A_MACRO_FUN_USED, ?A_MACRO_FUN()). 23 | -define(A_MACRO_WITH_BODY, 24 | begin 25 | erlang:time(), 26 | application:get_env(yet_another_key_3) 27 | end). 28 | 29 | -record(state, {foo = application:get_env(my_great_option), bar = ok}). 30 | 31 | -export([a_function/0]). 32 | 33 | a_function() -> 34 | R = #state{}, 35 | R#state{bar = application:get_env(another_great_option)}, 36 | [proplists:get_value(a_key, application:get_all_env(my_app)), 37 | ?A_MACRO, 38 | ?A_SUB_MACRO, 39 | ?A_COMPLEX_MACRO, 40 | ?A_COMPLEX_MACRO(test_app), 41 | ?A_COMPLEX_MACRO(), 42 | ?A_MACRO_FUN_USED, 43 | R#state.foo, 44 | R#state.bar, 45 | ?A_MACRO_WITH_BODY, 46 | application:get_env(other_config_from_app_src)]. 47 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/src/a_folder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/src/a_folder/a_subfolder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/src/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/test/a_folder/a_module.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/test_app/without_warnings/test/a_module_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(a_module). 2 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/a_behaviour.erl: -------------------------------------------------------------------------------- 1 | -module(a_behaviour). 2 | 3 | -callback the_magic() -> any(). 4 | -callback a_kind_of_magic(any()) -> any(). 5 | 6 | -export([the_magic/1, a_kind_of_magic/1, a_function_from_the_behaviour/1]). 7 | 8 | the_magic(M) -> 9 | M:the_magic(). 10 | 11 | a_kind_of_magic(M) -> 12 | M:a_kind_of_magic(). 13 | 14 | %% this will warn 15 | a_function_from_the_behaviour(_) -> 16 | argument_ignored. 17 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/a_behaviour_imp.erl: -------------------------------------------------------------------------------- 1 | %% @doc This module implements a local behaviour: 2 | %% The unnecessary_function_arguments rule will be ignored for all 3 | %% exported functions since we can't tell which ones are dynamic callbacks. 4 | -module(a_behaviour_imp). 5 | 6 | -behaviour(a_behaviour). 7 | 8 | -export([the_magic/0, a_kind_of_magic/1, function_with_ignored_arg/2]). 9 | 10 | the_magic() -> 11 | implemented. 12 | 13 | a_kind_of_magic(_) -> 14 | % this function won't be warned since it's a callback 15 | implemented. 16 | 17 | %% this exported function won't warn because the module implements a local behaviour 18 | function_with_ignored_arg(_, Value) -> 19 | non_exported_function_with(ignored_arg, Value). 20 | 21 | %% this should produce a warning since, being a non-exported function, it can't 22 | %% be a callback implementation. 23 | non_exported_function_with(_IgnoredArg, Value) -> 24 | Value. 25 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/clean.erl: -------------------------------------------------------------------------------- 1 | -module(clean). 2 | 3 | -define(FC(X, Y), {X, Y}). 4 | 5 | -record(r, {fc = function_call}). 6 | 7 | -export([single_fun/2, multi_fun/3]). 8 | -export([function_call/0, function_call/1, function_call/2, function_call/3]). 9 | 10 | single_fun(Arg1, Arg2) -> 11 | Arg1 * Arg2. 12 | 13 | multi_fun(_Arg1, Arg2, Arg3) when Arg2 > 1 -> 14 | [Arg2, Arg3]; 15 | multi_fun(Arg1, _, Arg3) -> 16 | [Arg1, Arg3]. 17 | 18 | function_call() -> 19 | function_call(not_a_nif). 20 | 21 | function_call(UsedArg1) -> 22 | F = function_call, 23 | F(UsedArg1, two). 24 | 25 | function_call(UsedArg1, UsedArg2) -> 26 | ?FC(UsedArg1, UsedArg2). 27 | 28 | function_call(UsedArg1, UsedArg2, vars) -> 29 | UsedArg2:UsedArg2(UsedArg1); 30 | function_call(UsedArg1, UsedArg2, macros) -> 31 | ?MODULE:function_call(UsedArg1, UsedArg2); 32 | function_call(UsedArg1, UsedArg2, records) -> 33 | (UsedArg2#r.fc)(UsedArg1); 34 | function_call(UsedArg1, UsedArg2, weird_stuff) -> 35 | begin 36 | io:format("~p~n", [UsedArg1]), 37 | function_call 38 | end(UsedArg2). 39 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/ct_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ct_SUITE). 2 | 3 | -behavior(ct_suite). 4 | 5 | -export([all/0, init_per_suite/1, end_per_suite/1]). 6 | -export([a_test/1]). 7 | 8 | all() -> 9 | [a_test]. 10 | 11 | init_per_suite(Config) -> 12 | Config. 13 | 14 | end_per_suite(_Config) -> 15 | unused. 16 | 17 | a_test(_Config) -> 18 | unused. 19 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/export_all1.erl: -------------------------------------------------------------------------------- 1 | -module(export_all1). 2 | 3 | -behaviour(ct_suite). 4 | 5 | -compile(export_all). 6 | -compile({inline, [a_test2/1]}). 7 | 8 | all() -> [a_test]. 9 | 10 | a_test(_) -> a_test2(). 11 | 12 | a_test2() -> ok. 13 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/export_all2.erl: -------------------------------------------------------------------------------- 1 | -module(export_all2). 2 | 3 | -behaviour(ct_suite). 4 | 5 | -compile([export_all]). 6 | 7 | all() -> [a_test]. 8 | 9 | a_test(_) -> ok. 10 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/gen_server_imp.erl: -------------------------------------------------------------------------------- 1 | -module(gen_server_imp). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([init/1, handle_call/3, handle_cast/2, my_function/1, my_other_function/2]). 6 | 7 | init(_) -> 8 | ignore. 9 | 10 | handle_call(_M, _From, State) -> 11 | {noreply, State}. 12 | 13 | handle_cast(_M, State) -> 14 | {noreply, State}. 15 | 16 | my_function(_) -> 17 | this_will_warn. 18 | 19 | my_other_function(this_also_will_warn, _B) -> 20 | undefined; 21 | my_other_function(A, _B) -> 22 | A. 23 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/ignore.erl: -------------------------------------------------------------------------------- 1 | -module ignore. 2 | 3 | -export([ignore_arg2/3, ignore_arg2/2, ignore_whole_func3/3, ignore_whole_func/1, 4 | ignore_whole_func/2]). 5 | 6 | -hank([{unnecessary_function_arguments, 7 | [{ignore_arg2, 3, 2}, {ignore_arg2, 2, 1}, {ignore_whole_func3, 3}, ignore_whole_func]}]). 8 | 9 | %% Arg2 is unused but ignored 10 | ignore_arg2(Arg1, _Arg2, Arg3) -> 11 | Arg1 + Arg3. 12 | 13 | %% Arg1 and Arg2 are unused but just ignoring Arg1 for `ignore_arg2/2` 14 | ignore_arg2(_Arg1, _Arg2) -> 15 | ok. 16 | 17 | %% A multi-clause function with unused 1st param 18 | ignore_whole_func3(_, _, undefined) -> 19 | ok; 20 | ignore_whole_func3(_, Arg2, _) when is_binary(Arg2) -> 21 | Arg2; 22 | ignore_whole_func3(_, _, Arg3) -> 23 | Arg3. 24 | 25 | %% Same function names with different arities 26 | ignore_whole_func(_) -> 27 | ok. 28 | 29 | ignore_whole_func(_, _) -> 30 | ok. 31 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/ignore_config.erl: -------------------------------------------------------------------------------- 1 | -module(ignore_config). 2 | 3 | -export([ignore_arg2/3, ignore_arg2/2, ignore_whole_func3/3, ignore_whole_func/1, 4 | ignore_whole_func/2]). 5 | 6 | %% Arg2 is unused but ignored 7 | ignore_arg2(Arg1, _Arg2, Arg3) -> 8 | Arg1 + Arg3. 9 | 10 | %% Arg1 and Arg2 are unused but just ignoring Arg1 for `ignore_arg2/2` 11 | ignore_arg2(_Arg1, _Arg2) -> 12 | ok. 13 | 14 | %% A multi-clause function with unused 1st param 15 | ignore_whole_func3(_, _, undefined) -> 16 | ok; 17 | ignore_whole_func3(_, Arg2, _) when is_binary(Arg2) -> 18 | Arg2; 19 | ignore_whole_func3(_, _, Arg3) -> 20 | Arg3. 21 | 22 | %% Same function names with different arities 23 | ignore_whole_func(_) -> 24 | ok. 25 | 26 | ignore_whole_func(_, _) -> 27 | ok. 28 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/macro_behaviour_imp.erl: -------------------------------------------------------------------------------- 1 | %% @doc This module is a nasty case, it implements a behaviour inside a macro 2 | -module(macro_behaviour_imp). 3 | 4 | -define(WHY_DO_YOU_DO_THIS_TO_ME, a_behaviour). 5 | 6 | -behaviour(?WHY_DO_YOU_DO_THIS_TO_ME). 7 | 8 | -export([the_magic/0, a_kind_of_magic/1]). 9 | 10 | the_magic() -> 11 | implemented. 12 | 13 | a_kind_of_magic(_) -> 14 | % this function won't be warned since it's a callback 15 | implemented. 16 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/macros.erl: -------------------------------------------------------------------------------- 1 | -module(marcors). 2 | 3 | -export([?MODULE/1]). 4 | 5 | ?MODULE(_Something) -> 6 | ?MODULE. 7 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/nifs.erl: -------------------------------------------------------------------------------- 1 | -module(nifs). 2 | 3 | -export([hank_should_ignore_this_function_unused_params/2, and_also_this_one/3, 4 | and_this_one_too/3, even_with_multiple_clauses/2]). 5 | 6 | hank_should_ignore_this_function_unused_params(_Ignore, _Me) -> 7 | erlang:nif_error(undefined). 8 | 9 | and_also_this_one(_Ignore, Me, _Too) -> 10 | mylog:info(Me), % This line should not affect anything! 11 | erlang:nif_error(undefined, []). 12 | 13 | and_this_one_too(_Ignore, _Me, Again) -> 14 | Msg = case Again of 15 | something -> 16 | "A message!"; 17 | _ -> 18 | "Another message!" 19 | end, 20 | erlang:nif_error(Msg, [ignore, Again]). 21 | 22 | even_with_multiple_clauses(A, B) -> 23 | A + B; 24 | even_with_multiple_clauses(_, _) -> 25 | erlang:nif_error(no_warnings). 26 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/not_included_behaviour.erl: -------------------------------------------------------------------------------- 1 | -module(not_included_behaviour). 2 | 3 | -callback the_not_included() -> any(). 4 | 5 | -export([the_not_included/1]). 6 | 7 | the_not_included(M) -> 8 | M:the_not_included(). 9 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/old_ct_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(old_ct_SUITE). 2 | 3 | -export([all/0, init_per_suite/1, end_per_suite/1]). 4 | -export([a_test/1]). 5 | 6 | all() -> 7 | [a_test]. 8 | 9 | init_per_suite(Config) -> 10 | Config. 11 | 12 | end_per_suite(_Config) -> 13 | unused. 14 | 15 | a_test(_Config) -> 16 | unused. 17 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/parse_transf.erl: -------------------------------------------------------------------------------- 1 | -module(parse_transf). 2 | 3 | -export([parse_transform/2]). 4 | 5 | -spec parse_transform(Forms, Options) -> Forms 6 | when Forms :: [erl_parse:abstract_form() | erl_parse:form_info()], 7 | Options :: [compile:option()]. 8 | parse_transform(Forms, _Options) -> % Options is purposefully left unused, here. 9 | Forms. 10 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/warnings_A.erl: -------------------------------------------------------------------------------- 1 | -module(warnings_A). 2 | 3 | -export([single_fun/2, multi_fun/3, 'unicode_αβåö'/1, with_nif_stub/2]). 4 | 5 | %% _Arg2 is unused 6 | single_fun(Arg1, _Arg2) -> 7 | Arg1. 8 | 9 | %% A multi-clause function with unused 3rd param 10 | multi_fun(undefined, _, _) -> 11 | ok; 12 | multi_fun(Arg1, Arg2, _Arg3) when is_binary(Arg1) -> 13 | Arg2; 14 | multi_fun(Arg1, _, _) -> 15 | Arg1. 16 | 17 | %% Unicode should be supported and not break! 18 | 'unicode_αβåö'(_Config) -> 19 | unused_param. 20 | 21 | %% NIF stub should not affect the warning 22 | with_nif_stub(_A, B) -> 23 | {only, B, is_used}; 24 | with_nif_stub(_, _) -> 25 | erlang:nif_error('A_is_unused'). 26 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/warnings_B.erl: -------------------------------------------------------------------------------- 1 | -module(warnings_B). 2 | 3 | -export([underscore/3, not_underscored/2]). 4 | 5 | %% _ is unused 6 | underscore(_, Arg2, Arg3) -> 7 | {Arg2, Arg3}. 8 | 9 | %% Arg2 is unused and not underscored (although the linter should complain) 10 | not_underscored(Arg1, Arg2) -> 11 | Arg1. 12 | -------------------------------------------------------------------------------- /test/files/unnecessary_function_arguments/weird.erl: -------------------------------------------------------------------------------- 1 | %%% @doc This module contains weird function calls that used to crash hank 2 | -module(weird). 3 | 4 | -compile([export_all]). 5 | 6 | -record(to, {a}). 7 | 8 | record(Access) -> 9 | (Access#to.a):record(field). 10 | 11 | a_case(Statement) -> 12 | using:case Statement of 13 | to -> 14 | determine; 15 | the -> 16 | function 17 | end(). 18 | -------------------------------------------------------------------------------- /test/files/unused_callbacks/clean.erl: -------------------------------------------------------------------------------- 1 | -module(clean). 2 | 3 | -callback used_callback(map()) -> boolean(). 4 | -callback used_atom(atom()) -> list(). 5 | -callback used_in_macro() -> this | one | is | used | through | the | macro. 6 | 7 | -define(MACRO_USAGE(M), M:used_in_macro()). 8 | 9 | -export([used_callback/1, used_atom/1, used_in_macro/1]). 10 | 11 | used_callback(#{module := Module, state := State}) -> 12 | Module:used_callback(State). 13 | 14 | used_atom(Module) -> 15 | erlang:apply(Module, used_atom, [another_atom]). 16 | 17 | used_in_macro(Module) -> 18 | ?MACRO_USAGE(Module). 19 | -------------------------------------------------------------------------------- /test/files/unused_callbacks/ignore.erl: -------------------------------------------------------------------------------- 1 | -module ignore. 2 | 3 | -hank([{unused_callbacks, [all_arities, {just_one, 1}]}]). 4 | 5 | -callback all_arities() -> ignored. 6 | -callback all_arities(_) -> ignored. 7 | -callback all_arities(_, _) -> ignored. 8 | -callback just_one() -> reported. 9 | -callback just_one(_) -> ignored. 10 | -callback just_one(_, _) -> reported. 11 | -------------------------------------------------------------------------------- /test/files/unused_callbacks/ignore_config.erl: -------------------------------------------------------------------------------- 1 | -module(ignore_config). 2 | 3 | -callback all_arities() -> ignored. 4 | -callback all_arities(_) -> ignored. 5 | -callback all_arities(_, _) -> ignored. 6 | -callback just_one() -> ignored. 7 | -callback just_one(_) -> ignored. 8 | -callback just_one(_, _) -> ignored. 9 | -------------------------------------------------------------------------------- /test/files/unused_callbacks/macros.erl: -------------------------------------------------------------------------------- 1 | -module(macros). 2 | 3 | -callback used_callback() -> this | one | is | used | through | the | macro. 4 | -callback unused_callback() -> this | one | isnt | used. 5 | 6 | -define(unused_callback, {this, macro, doesnt, use, the, callback}). 7 | -define(CALLBACK, used_callback). 8 | 9 | -export([using_the_callback/1, not_using_the_callback/0]). 10 | 11 | using_the_callback(Module) -> 12 | Module:?CALLBACK(). 13 | 14 | not_using_the_callback() -> 15 | ?unused_callback. 16 | -------------------------------------------------------------------------------- /test/files/unused_callbacks/warnings.erl: -------------------------------------------------------------------------------- 1 | -module(warnings). 2 | 3 | -callback unused_callback(map()) -> boolean(). 4 | -callback undetectable_unused_callback(map()) -> boolean(). 5 | 6 | -export([unused_callback/1, call_undetectable/1]). 7 | 8 | %% This function doesn't count as callback usage because 9 | %% it's just a regular module function that happens to have 10 | %% the same name as the callback definition, but it may have 11 | %% nothing to do with the callback itself like in this example 12 | %% in where `other_module:call/2` is "opaque" to us and its 13 | %% implementation may not use this module callbacks at all 14 | unused_callback(#{module := Module, state := State}) -> 15 | other_module:call(Module, State). 16 | 17 | undetectable_unused_callback(State) -> 18 | other_module:do(State). 19 | 20 | call_undetectable(#{state := State}) -> 21 | undetectable_unused_callback(State). 22 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app0-with-other-name/include/header.hrl: -------------------------------------------------------------------------------- 1 | -define(APP0_HEADER, "this is a header from app0 that might be unused"). 2 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app0-with-other-name/src/app0.app.src: -------------------------------------------------------------------------------- 1 | {application, app0, [{vsn, "1.0.1"}]}. 2 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app1/include/header.hrl: -------------------------------------------------------------------------------- 1 | -define(APP1_HEADER, "this is header from app1 that might be unused"). 2 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app1/src/app1.app.src: -------------------------------------------------------------------------------- 1 | {application, app1, [{vsn, "0"}]}. 2 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app1/src/app1_include.erl: -------------------------------------------------------------------------------- 1 | -module(app1_include). 2 | 3 | -include("header.hrl"). 4 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app1/src/app1_include_lib.erl: -------------------------------------------------------------------------------- 1 | -module(app1_include_lib). 2 | 3 | -include_lib("app1/include/header.hrl"). 4 | -include_lib("app0/include/header.hrl"). 5 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app1/src/app1_not_using_header.erl: -------------------------------------------------------------------------------- 1 | -module(app1_not_using_header). 2 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app2/src/app2.app.src: -------------------------------------------------------------------------------- 1 | {application, app2, [{vsn, "0"}]}. 2 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app2/src/app2_include.erl: -------------------------------------------------------------------------------- 1 | -module(app2_include). 2 | 3 | -include("../app1/include/header.hrl"). 4 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app2/src/app2_include_lib.erl: -------------------------------------------------------------------------------- 1 | -module(app2_include_lib). 2 | 3 | -include_lib("app1/include/header.hrl"). 4 | -include_lib("app0/include/header.hrl"). 5 | -------------------------------------------------------------------------------- /test/files/unused_hrls/lib/app2/src/app2_not_using_header.erl: -------------------------------------------------------------------------------- 1 | -module(app2_not_using_header). 2 | -------------------------------------------------------------------------------- /test/files/unused_ignores/ignore_all.erl: -------------------------------------------------------------------------------- 1 | -module(ignore_all). 2 | -hank ignore. 3 | -------------------------------------------------------------------------------- /test/files/unused_ignores/unused_ignores.erl: -------------------------------------------------------------------------------- 1 | -module(unused_ignores). 2 | 3 | -hank([{unnecessary_function_arguments, [{non_exported_function_with, 2, 1}]}]). 4 | -hank([{unused_macros, ["MACRO", "MICRO"]}]). 5 | -hank([bad_rule]). 6 | -------------------------------------------------------------------------------- /test/files/unused_macros/double.erl: -------------------------------------------------------------------------------- 1 | -module(double). 2 | 3 | -ifdef(X). 4 | 5 | -define(Y, y). 6 | 7 | -else. 8 | 9 | -define(Y, z). 10 | 11 | -endif. 12 | -------------------------------------------------------------------------------- /test/files/unused_macros/flow.erl: -------------------------------------------------------------------------------- 1 | -module(flow). 2 | 3 | -export([iif/0]). 4 | 5 | -ifndef(IFNDEF). 6 | -define(IFNDEF, true). 7 | -endif. 8 | 9 | -define(IFDEF, true). 10 | -ifdef(IFDEF). 11 | -type ifdef() :: ifdef. 12 | -export_type([ifdef/0]). 13 | -endif. 14 | 15 | -define(IF(X), not X). 16 | 17 | -if(?IF(?VALUE)). 18 | 19 | iif() -> true. 20 | 21 | -elif(not ?IF(?VALUE)). 22 | 23 | iif() -> false. 24 | 25 | -else. 26 | 27 | iif() -> wat. 28 | 29 | -endif. 30 | 31 | -define(UNDEF, undef). 32 | -undef(UNDEF). 33 | -------------------------------------------------------------------------------- /test/files/unused_macros/header.hrl: -------------------------------------------------------------------------------- 1 | %% This file contains all the macros that are used or unused in the following modules: 2 | %% - unused_macro_sample.erl 3 | %% - used_macro_sample.erl 4 | %% 5 | %% Even when those files don't include this header, Hank will only report the 6 | %% strictly unused macros as unused. It will act conservatively and not make any 7 | %% effort to actually verify where each macro that's used is defined. 8 | %% That's too hard. And if you have a project with multiple definitions of the 9 | %% same macro with the same arity... well... as long as one of them is used, 10 | %% none of them will be reported. Sorry. 11 | -define(SIMPLE_MACRO, simple_macro). 12 | -define(MACRO_USED_IN(MACRO), ??MACRO). 13 | -define(MACRO_THAT_USES(OTHER_MACRO), {OTHER_MACRO, ?MACRO_USED_IN(?OTHER_MACRO)}). 14 | -define(USED_MACROS_AS_FUNC, ?MODULE:simple_macros_func). 15 | -define(UNUSED_MACRO, unused_macro). 16 | -define(UNUSED_MACRO_WITH(), no_arguments). 17 | -define(UNUSED_MACRO_WITH(ARGUMENTS), unused_macro_with(ARGUMENTS)). 18 | -define(UNUSED_MACRO_WITH(TWO, ARGUMENTS), unused_macro_with(TWO, ARGUMENTS)). 19 | 20 | -define( UNUSED_MACRO_WITH_BROKEN( CODE ) , case ?CODE of code -> ??CODE ) . 21 | 22 | -define(UNUSED_MACRO_UNICODE_ÇØÍ, unused_macro_unicode_ÇØÍ). 23 | %% This one is actually used, since it's one of used_macro_sample macros, too. 24 | -define(macroIsAnAtom, macro_is_an_atom). 25 | -------------------------------------------------------------------------------- /test/files/unused_macros/ignore.erl: -------------------------------------------------------------------------------- 1 | -module ignore. 2 | 3 | -hank([{unused_macros, 4 | ["MACRO_ALL", {"MACRO_0", 0}, {"MACRO_1", 1}, {"MACRO_NONE", none}]}]). 5 | 6 | -define(MACRO_ALL, "this macro is always ignored"). 7 | -define(MACRO_ALL(), "regardless of its arity"). 8 | -define(MACRO_ALL(It), "is never reported as unused"). 9 | -define(MACRO_ALL(Not, Even), "if it has multiple arguments"). 10 | -define(MACRO_0, "This version of the macro should not be ignored"). 11 | -define(MACRO_0(), "This one should since it has 0 arguments"). 12 | -define(MACRO_0(And), "this one should be reported since it has one"). 13 | -define(MACRO_0(Also, This), "one, that has 2"). 14 | -define(MACRO_1, "For this macro"). 15 | -define(MACRO_1(), "there should be a report"). 16 | -define(MACRO_1(But), "not for this version with arity 1"). 17 | -define(MACRO_1(Just, For), "all the others"). 18 | -define(MACRO_NONE, "This instance should be ignored"). 19 | -define(MACRO_NONE(), "But there should be a report"). 20 | -define(MACRO_NONE(For), "each of the"). 21 | -define(MACRO_NONE(Other, Versions), ""). 22 | -------------------------------------------------------------------------------- /test/files/unused_macros/ignore_config.erl: -------------------------------------------------------------------------------- 1 | -module(ignore_config). 2 | 3 | %% @doc No warnings should be emitted for this file since 4 | %% rebar.config specifically states that all of them 5 | %% should be ignored. 6 | 7 | -define(MACRO_ALL, "this macro is always ignored"). 8 | -define(MACRO_ALL(), "regardless of its arity"). 9 | -define(MACRO_ALL(It), "is never reported as unused"). 10 | -define(MACRO_ALL(Not, Even), "if it has multiple arguments"). 11 | -define(MACRO_0(), "This one should since it has 0 arguments"). 12 | -define(MACRO_1(But), "not for this version with arity 1"). 13 | -define(MACRO_NONE, "This instance should be ignored"). 14 | -------------------------------------------------------------------------------- /test/files/unused_macros/unused_macro_sample.erl: -------------------------------------------------------------------------------- 1 | -module(unused_macro_sample). 2 | 3 | -compile(export_all). 4 | 5 | -define(UNUSED_MACRO, unused_macro). 6 | -define(UNUSED_MACRO_WITH(), no_arguments). 7 | -define(UNUSED_MACRO_WITH(ARGUMENTS), unused_macro_with(ARGUMENTS)). 8 | -define(UNUSED_MACRO_WITH(TWO, ARGUMENTS), unused_macro_with(TWO, ARGUMENTS)). 9 | 10 | %% hank won't detect this one, since it's unparseable [https://github.com/AdRoll/rebar3_hank/issues/37] 11 | -define( UNUSED_MACRO_WITH_BROKEN( CODE ) , case ?CODE of code -> ??CODE ) . 12 | 13 | %% Unicode should be supported and not break! 14 | -define(UNUSED_MACRO_UNICODE_ÇØÍ, unused_macro_unicode_ÇØÍ). 15 | -define(macroIsAnAtom, macro_is_an_atom). 16 | -------------------------------------------------------------------------------- /test/files/unused_macros/used_macro_sample.erl: -------------------------------------------------------------------------------- 1 | -module(used_macro_sample). 2 | 3 | -compile(export_all). 4 | 5 | -define(SIMPLE_MACRO, simple_macro). 6 | -define(MACRO_USED_IN(MACRO), ??MACRO). 7 | -define(MACRO_THAT_USES(OTHER_MACRO), {OTHER_MACRO, ?MACRO_USED_IN(?OTHER_MACRO)}). 8 | -define(USED_MACROS_AS_FUNC, ?MODULE:simple_macros_func). 9 | 10 | -define( USED_MACRO_WITH_BROKEN( CODE ) , case ?CODE of code -> ??CODE ) . 11 | 12 | -define(macroIsAnAtom, macro_is_an_atom). 13 | -define(macroIsAnAtom(With), {macro_is_an_atom, With}). 14 | -define(macroIsAnAtomToo(), macro_is_an_atom). 15 | 16 | using_macro(NotAMacro) -> 17 | ?MACRO_THAT_USES(NotAMacro). 18 | 19 | simple_macro() -> 20 | ?SIMPLE_MACRO. 21 | 22 | broken( ) -> ?USED_MACRO_WITH_BROKEN( SIMPLE_MACRO ) ; _ -> other end . 23 | 24 | macro_is_an_atom() -> 25 | {?macroIsAnAtom, ?macroIsAnAtom(?macroIsAnAtomToo())}. 26 | 27 | simple_macros_call() -> 28 | ?USED_MACROS_AS_FUNC(<<"">>, <<"">>). 29 | 30 | simple_macros_func(Data0, Data1) -> 31 | io:format("~p~n", [{Data0, Data1}]). 32 | -------------------------------------------------------------------------------- /test/files/unused_record_fields/header.hrl: -------------------------------------------------------------------------------- 1 | %% This file contains a record with all the fields that are used or unused 2 | %% in unused_record_field_sample.erl 3 | %% 4 | %% Even when the module doesn't include this header, Hank will only report the 5 | %% strictly unused record fields as unused. It will act conservatively and not 6 | %% make any effort to actually verify where each record field that's used is 7 | %% defined. 8 | %% That's too hard. And if you have a project with multiple definitions of the 9 | %% same record with the same fields... well... as long as one of them is used, 10 | %% none of them will be reported. Sorry. 11 | -record(a_record, 12 | {used_field, 13 | used_typed_field :: used_typed_field, 14 | used_field_with_default = used_field_with_default, 15 | used_typed_field_with_default = used_typed_field_with_default :: 16 | used_typed_field_with_default, 17 | unused_field, 18 | unused_typed_field :: unused_typed_field, 19 | unused_field_with_default = unused_field_with_default, 20 | unused_typed_field_with_default = unused_typed_field_with_default :: 21 | unused_typed_field_with_default}). 22 | -record(really_unused_record, {really_unused_field}). 23 | -------------------------------------------------------------------------------- /test/files/unused_record_fields/ignore.erl: -------------------------------------------------------------------------------- 1 | -module ignore. 2 | 3 | -compile(export_all). 4 | 5 | -hank([{unused_record_fields, 6 | [ignored_record, {a_record, ignored_field_1}, {a_record, ignored_field_2}]}]). 7 | 8 | -record(a_record, {used_field, unused_field, ignored_field_1, ignored_field_2}). 9 | -record(ignored_record, {used_field, unused_field, ignored_field_1, ignored_field_2}). 10 | 11 | usage() -> 12 | #a_record{used_field = #ignored_record{used_field = used}}. 13 | -------------------------------------------------------------------------------- /test/files/unused_record_fields/ignore_config.erl: -------------------------------------------------------------------------------- 1 | -module ignore. 2 | 3 | -compile(export_all). 4 | 5 | %% @doc No warnings should be emitted for this file since 6 | %% rebar.config specifically states that all of them 7 | %% should be ignored. 8 | 9 | -record(a_record, {used_field, ignored_field1, ignored_field2}). 10 | -record(ignored_record, {used_field, unused_field, ignored_field_1, ignored_field_2}). 11 | 12 | usage() -> 13 | #a_record{used_field = #ignored_record{used_field = used}}. 14 | -------------------------------------------------------------------------------- /test/files/unused_record_fields/macros.erl: -------------------------------------------------------------------------------- 1 | -module(macros). 2 | 3 | %% There must be a paresable record 4 | -record(a_rec, {unused_field}). 5 | %% But also an "unparseable" one 6 | -record(?MODULE, {}). 7 | -------------------------------------------------------------------------------- /test/files/unused_record_fields/unused_record_field_sample.erl: -------------------------------------------------------------------------------- 1 | -module(unused_record_field_sample). 2 | 3 | -compile(export_all). 4 | 5 | -record(a_record, 6 | {used_field, 7 | used_typed_field :: used_typed_field, 8 | used_field_with_default = used_field_with_default, 9 | used_typed_field_with_default = used_typed_field_with_default :: 10 | used_typed_field_with_default, 11 | unused_field, 12 | unused_typed_field :: unused_typed_field, 13 | unused_field_with_default = unused_field_with_default, 14 | unused_typed_field_with_default = unused_typed_field_with_default :: 15 | unused_typed_field_with_default}). 16 | %% Unicode should be supported and not break! 17 | -record('unicode_αåβö', {'attr_αåβö' :: a_type()}). 18 | 19 | %% This doesn't count as usage 20 | -type a_type() :: 21 | #a_record{unused_field :: unused_field, 22 | unused_field_with_default :: unused_field_with_default}. 23 | 24 | %% This doesn't count as usage 25 | construct() -> 26 | #a_record{used_field = used_field, used_typed_field = used_typed_field}. 27 | 28 | %% This doesn't count as usage either 29 | update(R) -> 30 | R#a_record{used_field = used_field, used_typed_field = used_typed_field}. 31 | 32 | %% This counts as usage 33 | pattern_match(#a_record{used_field = UF}, R) -> 34 | #a_record{used_field_with_default = UFWD} = R, 35 | [UF, UFWD]. 36 | 37 | %% This counts as usage 38 | index(Rs) -> 39 | lists:keysort(#a_record.used_typed_field, Rs). 40 | 41 | %% This counts as usage, too 42 | retrieve(R) -> 43 | R#a_record.used_typed_field_with_default. 44 | -------------------------------------------------------------------------------- /test/files/unused_record_fields/used_record_field_sample.erl: -------------------------------------------------------------------------------- 1 | -module(used_record_field_sample). 2 | 3 | -compile(export_all). 4 | 5 | -record(?MODULE, 6 | {this, record, is, not_used, but, it, cant, be, parsed, so, its, not_analyzed}). 7 | -record(a_record, 8 | {used_field, 9 | used_typed_field :: used_typed_field, 10 | used_field_with_default = used_field_with_default, 11 | used_typed_field_with_default = used_typed_field_with_default :: 12 | used_typed_field_with_default, 13 | another_used_field}). 14 | 15 | %% This doesn't count as usage 16 | -type a_type() :: #a_record{}. 17 | 18 | %% This doesn't count as usage 19 | construct() -> 20 | #a_record{used_field = used_field, used_typed_field = used_typed_field}. 21 | 22 | %% This doesn't count as usage either 23 | update(R) -> 24 | R#a_record{used_field = used_field, used_typed_field = used_typed_field}. 25 | 26 | %% This counts as usage 27 | pattern_match(#a_record{used_field = UF} = R) -> 28 | #a_record{used_field_with_default = UFWD} = R, 29 | [UF, UFWD]. 30 | 31 | %% This counts as usage 32 | index(Rs) -> 33 | lists:keysort(#a_record.used_typed_field, Rs). 34 | 35 | %% This counts as usage, too 36 | retrieve(R) -> 37 | R#a_record.used_typed_field_with_default. 38 | 39 | %% This counts as usage for all fields 40 | use_all_fields(R) -> 41 | R#a_record{_ = all_used}. 42 | -------------------------------------------------------------------------------- /test/global_rejector.erl: -------------------------------------------------------------------------------- 1 | %%% @doc This module is used by test_app_SUITE 2 | -module(global_rejector). 3 | 4 | -behaviour(hank_rule). 5 | 6 | -export([analyze/2, ignored/2]). 7 | 8 | %% @doc All files are wrong!! 9 | analyze(ASTs, _) -> 10 | [#{file => File, 11 | line => 1, 12 | text => "global_rejector", 13 | pattern => undefined} 14 | || {File, _} <- ASTs]. 15 | 16 | -spec ignored(hank_rule:ignore_pattern(), term()) -> boolean(). 17 | ignored(_Pattern, _IgnoreSpec) -> 18 | false. 19 | -------------------------------------------------------------------------------- /test/hank_test_utils.erl: -------------------------------------------------------------------------------- 1 | -module(hank_test_utils). 2 | 3 | -export([init_per_testcase/2, end_per_testcase/1]). 4 | -export([init/0, init/1, mock_context/2, analyze_and_sort/2, analyze_and_sort/3, 5 | set_cwd/1, abs_test_path/1]). 6 | 7 | init_per_testcase(Config, TestDirName) -> 8 | {ok, Cwd} = file:get_cwd(), % Keep the original working directory 9 | set_cwd(TestDirName), 10 | [{cwd, Cwd} | Config]. 11 | 12 | end_per_testcase(Config) -> 13 | {value, {cwd, Cwd}, NewConfig} = lists:keytake(cwd, 1, Config), 14 | ok = file:set_cwd(Cwd), 15 | NewConfig. 16 | 17 | %% @doc Initialize rebar3 to simulate running `rebar3 hank` 18 | init() -> 19 | {ok, State} = 20 | rebar3_hank:init( 21 | rebar_state:new()), 22 | State. 23 | 24 | init(AppName) when is_atom(AppName) -> 25 | RebarAppInfo = 26 | rebar_app_info:name( 27 | rebar_app_info:new(), atom_to_binary(AppName, utf8)), 28 | RebarAppInfo2 = rebar_app_info:dir(RebarAppInfo, abs_test_path(atom_to_list(AppName))), 29 | rebar_state:project_apps(init(), RebarAppInfo2). 30 | 31 | mock_context(Apps, ProjectApps) -> 32 | AppsAbs = maps:map(fun(_App, Path) -> filename:absname(Path) end, Apps), 33 | hank_context:new(AppsAbs, ProjectApps). 34 | 35 | analyze_and_sort(Files, Rules) -> 36 | analyze_and_sort(Files, Rules, mock_context(#{}, [])). 37 | 38 | analyze_and_sort(Files, Rules, Context) when is_map(Context) -> 39 | analyze_and_sort(Files, [], Rules, Context); 40 | analyze_and_sort(Files, IgnoreSpecs, Rules) -> 41 | analyze_and_sort(Files, IgnoreSpecs, Rules, mock_context(#{}, [])). 42 | 43 | analyze_and_sort(Files, IgnoreSpecs, Rules, Context) -> 44 | ParsingStyle = 45 | case erlang:monotonic_time(millisecond) rem 2 of 46 | 0 -> 47 | parallel; 48 | _ -> 49 | sequential 50 | end, 51 | #{stats := Stats, 52 | unused_ignores := UnusedIgnores, 53 | results := Results} = 54 | hank:analyze(Files, IgnoreSpecs, Rules, ParsingStyle, Context), 55 | #{parsing := Parsing, 56 | analyzing := Analyzing, 57 | total := Total} = 58 | Stats, 59 | {true, Stats} = {Parsing >= 0, Stats}, 60 | {true, Stats} = {Analyzing >= 0, Stats}, 61 | {true, Stats} = {Parsing + Analyzing =< Total, Stats}, 62 | #{unused_ignores => lists:sort(UnusedIgnores), results => lists:sort(Results)}. 63 | 64 | set_cwd(RelativePathOrFilename) -> 65 | ok = file:set_cwd(abs_test_path(RelativePathOrFilename)). 66 | 67 | abs_test_path(FilePath) -> 68 | filename:join( 69 | code:lib_dir(rebar3_hank), "test/files/" ++ FilePath). 70 | -------------------------------------------------------------------------------- /test/hidden_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Test module for the hidden files/folders functionality 2 | -module(hidden_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([hidden/1]). 6 | 7 | all() -> 8 | [hidden]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "hidden"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc No warning should be emitted for files that are hidden or that are 17 | %% included in hidden folders. 18 | hidden(_Config) -> 19 | State = hank_test_utils:init(), 20 | 21 | {ok, _} = rebar3_hank_prv:do(State), 22 | 23 | {comment, ""}. 24 | -------------------------------------------------------------------------------- /test/ignore_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Test module for the ignore functionality 2 | -module(ignore_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([rebar_config/1, hank_ignore/1, hank_individual_rules/1, rebar_config_ignore/1]). 6 | 7 | -elvis([{elvis_style, dont_repeat_yourself, disable}]). % for rebar_config_ignore/1 8 | 9 | all() -> 10 | [rebar_config, hank_ignore, hank_individual_rules, rebar_config_ignore]. 11 | 12 | init_per_testcase(_, Config) -> 13 | hank_test_utils:init_per_testcase(Config, "ignore"). 14 | 15 | end_per_testcase(_, Config) -> 16 | hank_test_utils:end_per_testcase(Config). 17 | 18 | %% @doc No warning should be emitted for files listed in the ignored part of 19 | %% hank's config at rebar.config 20 | rebar_config(_Config) -> 21 | State = hank_test_utils:init(), 22 | 23 | ct:comment("Warnings should be emitted since we're not ignoring the problematic " 24 | "files"), 25 | State1 = rebar_state:set(State, hank, []), 26 | Warnings = find_warnings(State1), 27 | 28 | ct:comment("If we ignore the problematic files, we should not get warnings " 29 | "for them"), 30 | State2 = 31 | rebar_state:set(State, 32 | hank, 33 | [{ignore, [binary_to_list(File) || #{file := File} <- Warnings]}]), 34 | {ok, _} = rebar3_hank_prv:do(State2), 35 | 36 | ct:comment("If we ignore some rules on the problematic files, we should " 37 | "not get warnings for them"), 38 | State3 = 39 | rebar_state:set(State, 40 | hank, 41 | [{ignore, 42 | [{binary_to_list(File), global_rejector} 43 | || #{file := File, text := <<" global_rejector">>} <- Warnings]}]), 44 | Warnings3 = find_warnings(State3), 45 | [] = [x || #{text := <<" global_rejector">>} <- Warnings3], 46 | true = length(Warnings3) > 0, 47 | [] = Warnings3 -- Warnings, 48 | true = length(Warnings -- Warnings3) > 0, 49 | 50 | {comment, ""}. 51 | 52 | %% @doc No warning should be emitted for files with -hank ignore 53 | hank_ignore(_Config) -> 54 | %% Initialize rebar3 state as if we run `rebar3 hank` with the default rules 55 | State = hank_test_utils:init(), 56 | 57 | ct:comment("With -hank ignore, there should be no warnings"), 58 | Warnings = find_warnings(State), 59 | [] = 60 | [Warning || Warning = #{file := File} <- Warnings, string:equal(File, "with_ignore.erl")], 61 | [] = 62 | [Warning || Warning = #{file := File} <- Warnings, string:equal(File, "with_ignore.hrl")], 63 | 64 | {comment, ""}. 65 | 66 | %% @doc No warning should be emitted for rules ignored with -hank [rule, ...] 67 | hank_individual_rules(_Config) -> 68 | State = hank_test_utils:init(), 69 | 70 | ct:comment("With -hank ignore, there should only be warnings for non-ignored " 71 | "rules"), 72 | Rules = [unused_macros, unnecessary_function_arguments, global_rejector], 73 | State1 = rebar_state:set(State, hank, [{rules, Rules}]), 74 | Warnings = find_warnings(State1), 75 | [<<" global_rejector">>] = 76 | [Text 77 | || #{file := File, text := Text} <- Warnings, string:equal(File, "specific_ignore.erl")], 78 | 79 | {comment, ""}. 80 | 81 | %% @doc No warning should be emitted for lists of ignored rules and neither should 82 | %% evaluation of the code fail 83 | rebar_config_ignore(_Config) -> 84 | Rule1 = unused_macros, 85 | Rule2 = unnecessary_function_arguments, 86 | FileErl = "rebar_config_ignore.erl", 87 | Rules = [Rule1, Rule2], 88 | 89 | State0 = hank_test_utils:init(), 90 | State1 = rebar_state:set(State0, hank, []), 91 | 92 | ct:comment("Prepare for the next test (we start with whatever worked)"), 93 | State2 = 94 | rebar_state:set(State1, 95 | hank, 96 | [{rules, Rules}, {ignore, [{FileErl, Rule1}, {FileErl, Rule2}]}]), 97 | Warnings0 = find_warnings(State2), 98 | [] = [Warning0 || Warning0 = #{file := File} <- Warnings0, string:equal(File, FileErl)], 99 | 100 | ct:comment("Test with an ignore list"), 101 | State3 = 102 | rebar_state:set(State2, hank, [{rules, Rules}, {ignore, [{FileErl, [Rule1, Rule2]}]}]), 103 | Warnings1 = find_warnings(State3), 104 | [] = [Warning1 || Warning1 = #{file := File} <- Warnings1, string:equal(File, FileErl)], 105 | 106 | {comment, ""}. 107 | 108 | find_warnings(State) -> 109 | {error, Error} = rebar3_hank_prv:do(State), 110 | <<"The following pieces of code", 111 | " are dead and should be removed:\n", 112 | ResultsBin/binary>> = 113 | iolist_to_binary(Error), 114 | Results = binary:split(ResultsBin, <<$\n>>, [global, trim]), 115 | 116 | lists:map(fun(Result) -> 117 | [File, Line | Text] = binary:split(Result, <<$:>>, [global, trim]), 118 | #{file => File, 119 | line => Line, 120 | text => iolist_to_binary(Text)} 121 | end, 122 | Results). 123 | -------------------------------------------------------------------------------- /test/single_use_hrl_attrs_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Tests for the single_use_hrl_attrs rule 2 | -module(single_use_hrl_attrs_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([hrl_in_just_one_module/1, ignore_config/1]). 6 | 7 | all() -> 8 | [hrl_in_just_one_module, ignore_config]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "single_use_hrl_attrs"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc Hank finds hrl attributes used in just one module 17 | hrl_in_just_one_module(_) -> 18 | Files = filelib:wildcard("**/*.[he]rl"), 19 | [#{file := "lib/app/include/flow.hrl", 20 | text := <<"?IFNDEF is used only at lib/app/src/flow.erl">>}, 21 | #{file := "lib/app/include/flow.hrl", 22 | text := <<"?IFDEF is used only at lib/app/src/flow.erl">>}, 23 | #{file := "lib/app/include/flow.hrl", 24 | text := <<"?IF/1 is used only at lib/app/src/flow.erl">>}, 25 | #{file := "lib/app/include/flow.hrl", 26 | text := <<"?UNDEF is used only at lib/app/src/flow.erl">>}, 27 | #{file := "lib/app/include/header1.hrl", 28 | text := <<"?APP_HEADER_1 is used only at lib/app/src/app_include_lib.erl">>}, 29 | #{file := "lib/app/include/header1.hrl", 30 | text := <<"?SOME_MACRO_1/1 is used only at lib/app/src/app_include_lib.erl">>}, 31 | #{file := "lib/app/include/header1.hrl", 32 | text := <<"?SOME_DEFINE is used only at lib/app/src/app_other.erl">>}, 33 | #{file := "lib/app/include/header2.hrl", 34 | text := <<"?APP_HEADER_2 is used only at lib/app/src/app_include.erl">>}, 35 | #{file := "lib/app/include/header2.hrl", 36 | text := <<"?SOME_MACRO_2/1 is used only at lib/app/src/app_include.erl">>}, 37 | #{file := "lib/app/include/header3.hrl", 38 | text := <<"#a_record is used only at lib/app/src/app_other.erl">>}, 39 | #{file := "lib/app/include/header3.hrl", 40 | text := <<"#another_record is used only at lib/app/src/app_other.erl">>}, 41 | #{file := "lib/app/include/header3.hrl", 42 | text := <<"#'unicode_αåβö' is used only at lib/app/src/app_other.erl"/utf8>>}, 43 | #{file := "lib/app/include/ignore.hrl", 44 | text := <<"?MACRO_0 is used only at lib/app/src/ignore.erl">>}, 45 | #{file := "lib/app/include/ignore.hrl", 46 | text := <<"?MACRO_0/1 is used only at lib/app/src/ignore.erl">>}, 47 | #{file := "lib/app/include/ignore.hrl", 48 | text := <<"?MACRO_0/2 is used only at lib/app/src/ignore.erl">>}, 49 | #{file := "lib/app/include/ignore.hrl", 50 | text := <<"?MACRO_1 is used only at lib/app/src/ignore.erl">>}, 51 | #{file := "lib/app/include/ignore.hrl", 52 | text := <<"?MACRO_1/0 is used only at lib/app/src/ignore.erl">>}, 53 | #{file := "lib/app/include/ignore.hrl", 54 | text := <<"?MACRO_1/2 is used only at lib/app/src/ignore.erl">>}, 55 | #{file := "lib/app/include/ignore.hrl", 56 | text := <<"?MACRO_NONE/0 is used only at lib/app/src/ignore.erl">>}, 57 | #{file := "lib/app/include/ignore.hrl", 58 | text := <<"?MACRO_NONE/1 is used only at lib/app/src/ignore.erl">>}, 59 | #{file := "lib/app/include/ignore.hrl", 60 | text := <<"?MACRO_NONE/2 is used only at lib/app/src/ignore.erl">>}] = 61 | analyze(Files), 62 | ok. 63 | 64 | %% @doc No warnings since rebar.config specifically states that all of them 65 | %% should be ignored. 66 | ignore_config(_) -> 67 | File = "lib/app/include/header3.hrl", 68 | Files = [File, "lib/app/src/app_include.erl"], 69 | IgnoreSpecs = [{File, single_use_hrl_attrs, ["SOME_MACRO_3", "APP_HEADER_3"]}], 70 | #{results := []} = 71 | hank_test_utils:analyze_and_sort(Files, IgnoreSpecs, [single_use_hrl_attrs]). 72 | 73 | analyze(Files) -> 74 | Apps = #{app0 => "lib/app"}, 75 | Context = hank_test_utils:mock_context(Apps, [app0]), 76 | #{results := Results, unused_ignores := []} = 77 | hank_test_utils:analyze_and_sort(Files, [single_use_hrl_attrs], Context), 78 | Results. 79 | -------------------------------------------------------------------------------- /test/single_use_hrls_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(single_use_hrls_SUITE). 2 | 3 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 4 | -export([only_hrls/1, single_use/1, respects_ignore/1, ignores_missing_files/1, 5 | alltogether/1]). 6 | 7 | all() -> 8 | [only_hrls, single_use, respects_ignore, ignores_missing_files, alltogether]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "single_use_hrls"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc Hank doesn't find anything to report for header files only 17 | only_hrls(_) -> 18 | ct:comment("If there are no Erlang files, it should not detect anything."), 19 | OnlyHrls = ["include/multi.hrl", "include/single.hrl"], 20 | [] = analyze(OnlyHrls), 21 | ok. 22 | 23 | %% @doc Hank finds header files that are only included in just one module 24 | single_use(_) -> 25 | ct:comment("It should detect include/single.hrl because it's only included " 26 | "at src/include_single.erl"), 27 | Files = 28 | ["include/multi.hrl", 29 | "include/single.hrl", 30 | "src/include_multi.erl", 31 | "src/include_single.erl"], 32 | [#{file := "include/single.hrl", 33 | text := <<"This header file is only included at: src/include_single.erl">>}] = 34 | analyze(Files), 35 | ok. 36 | 37 | %% @doc Hank respects the `ignore` attribute in a header file and at `rebar.config` 38 | respects_ignore(_) -> 39 | ct:comment("It should not detect include/ignored.hrl because is ignored " 40 | "although it's only included at src/include_ignored.erl"), 41 | Files = 42 | ["include/multi.hrl", 43 | "include/single.hrl", 44 | "include/ignored.hrl", 45 | "src/include_multi.erl", 46 | "src/include_single.erl", 47 | "src/include_ignored.erl"], 48 | [] = analyze(Files, [{"include/single.hrl", all, all}]), 49 | ok. 50 | 51 | %% @doc Hank ignores header files that aren't present in the list of analyzed files 52 | ignores_missing_files(_) -> 53 | ct:comment("It should not detect missing.hrl because it isn't present in " 54 | "the list of files analyzed by Hank, although is only included " 55 | "at src/include_missing.erl"), 56 | Files = ["include/multi.hrl", "src/include_multi.erl", "src/include_missing.erl"], 57 | [] = analyze(Files), 58 | ok. 59 | 60 | %% @doc Hank finds and ignores accordingly 61 | alltogether(_) -> 62 | ct:comment("It should detect include/single.hrl because it's only included " 63 | "at src/include_single.erl and not detect include/ignored.hrl"), 64 | Files = 65 | ["include/multi.hrl", 66 | "include/single.hrl", 67 | "include/single_unicode.hrl", 68 | "include/ignored.hrl", 69 | "src/include_multi.erl", 70 | "src/include_single.erl", 71 | "src/include_ignored.erl", 72 | "src/include_missing.erl" 73 | | filelib:wildcard("src/include_unicode_*.erl")], 74 | [#{file := "include/single.hrl", 75 | text := <<"This header file is only included at: src/include_single.erl">>}, 76 | #{file := "include/single_unicode.hrl", 77 | text := 78 | <<"This header file is only included at: src/include_unicode_"/utf8, _/binary>>}] = 79 | analyze(Files), 80 | ok. 81 | 82 | analyze(Files) -> 83 | analyze(Files, []). 84 | 85 | analyze(Files, IgnoredFiles) -> 86 | #{results := Results, unused_ignores := []} = 87 | hank_test_utils:analyze_and_sort(Files, IgnoredFiles, [single_use_hrls]), 88 | Results. 89 | -------------------------------------------------------------------------------- /test/test_app_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Test module for the app in general 2 | -module(test_app_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([with_warnings/1, without_warnings/1]). 6 | 7 | all() -> 8 | [with_warnings, without_warnings]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "test_app"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc In a project where there are things to report, hank should return error 17 | with_warnings(_Config) -> 18 | State = hank_test_utils:init(), 19 | ct:comment("With default rules, there should be warnings since " 20 | ++ "hank_rule:default_rules() should find global_rejector"), 21 | State1 = rebar_state:set(State, hank, []), 22 | find_warnings(State1), 23 | 24 | ct:comment("If we alter the equivalent to rebar.config's hank to use a " 25 | "global rejector rule, it should find the same warnings"), 26 | State2 = rebar_state:set(State, hank, [{rules, [global_rejector]}]), 27 | find_warnings(State2), 28 | 29 | {comment, ""}. 30 | 31 | %% @doc In a project where all rules run cleanly, hank should return OK 32 | without_warnings(_Config) -> 33 | State = hank_test_utils:init(), 34 | 35 | ct:comment("With no rules, there should be no warnings"), 36 | State2 = rebar_state:set(State, hank, [{rules, []}]), 37 | {ok, _} = rebar3_hank_prv:do(State2), 38 | 39 | ct:comment("Without the global rejector, there should be no warnings either"), 40 | Rules = hank_rule:default_rules() -- [global_rejector], 41 | State3 = hank_test_utils:init(test_app), 42 | IgnorePattern = 43 | {ignore, [{"sys.config", unused_configuration_options, [not_used_but_ignored]}]}, 44 | State4 = rebar_state:set(State3, hank, [{rules, Rules}, IgnorePattern]), 45 | {ok, _} = rebar3_hank_prv:do(State4), 46 | 47 | {comment, ""}. 48 | 49 | find_warnings(State) -> 50 | %% Run hank 51 | {error, Error} = rebar3_hank_prv:do(State), 52 | <<"The following pieces of code", 53 | " are dead and should be removed:\n", 54 | ResultsBin/binary>> = 55 | iolist_to_binary(Error), 56 | Results = binary:split(ResultsBin, <<$\n>>, [global, trim]), 57 | 58 | %% There are at least 8 files in the test_app folder. 59 | %% We might add more in the future and we don't want this test to fail 60 | %% just because of that. 61 | true = 8 =< length(Results), 62 | lists:foreach(fun(Result) -> 63 | %% each result looks like path/to/file:#: msg 64 | [_, <<"1">>, <<" global_rejector">>] = 65 | binary:split(Result, <<$:>>, [global, trim]) 66 | end, 67 | Results). 68 | -------------------------------------------------------------------------------- /test/unnecessary_function_arguments_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(unnecessary_function_arguments_SUITE). 2 | 3 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 4 | -export([with_warnings/1, without_warnings/1, macros/1, ignore/1, ignore_config/1, 5 | ct_suite/1, export_all/1]). 6 | 7 | all() -> 8 | [with_warnings, without_warnings, macros, ignore, ct_suite, export_all]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "unnecessary_function_arguments"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc Hank finds unused function parameters 17 | with_warnings(_Config) -> 18 | ct:comment("Should detect and display warnings for unused function parameters"), 19 | 20 | FileA = "warnings_A.erl", 21 | FileB = "warnings_B.erl", 22 | FileC = "a_behaviour.erl", 23 | FileD = "a_behaviour_imp.erl", 24 | FileE = "gen_server_imp.erl", 25 | [#{file := FileC, 26 | text := <<"a_function_from_the_behaviour/1 doesn't need its #1 argument">>}, 27 | #{file := FileD, text := <<"non_exported_function_with/2 doesn't need its #1 argument">>}, 28 | #{file := FileE, text := <<"my_function/1 doesn't need its #1 argument">>}, 29 | #{file := FileE, text := <<"my_other_function/2 doesn't need its #2 argument">>}, 30 | #{file := FileA, text := <<"single_fun/2 doesn't need its #2 argument">>}, 31 | #{file := FileA, text := <<"multi_fun/3 doesn't need its #3 argument">>}, 32 | #{file := FileA, text := <<"unicode_αβåö/1 doesn't need its #1 argument"/utf8>>}, 33 | #{file := FileA, text := <<"with_nif_stub/2 doesn't need its #1 argument">>}, 34 | #{file := FileB, text := <<"underscore/3 doesn't need its #1 argument">>}] = 35 | analyze([FileA, FileB, FileC, FileD, FileE, "macro_behaviour_imp.erl"]), 36 | ok. 37 | 38 | %% @doc Hank finds nothing! 39 | without_warnings(_Config) -> 40 | ct:comment("Should not detect anything since the files are clean from warnings"), 41 | [] = 42 | analyze(["weird.erl", 43 | "clean.erl", 44 | "nifs.erl", 45 | "macro_behaviour_imp.erl", 46 | "parse_transf.erl"]), 47 | ok. 48 | 49 | %% @doc Macros as function names should work 50 | macros(_Config) -> 51 | ct:comment("Macros as function names should not crash hank"), 52 | [#{file := "macros.erl", text := <<"?MODULE/1 doesn't need its #1 argument">>}] = 53 | analyze(["macros.erl"]), 54 | ok. 55 | 56 | %% @doc Hank should correctly ignore warnings 57 | ignore(_Config) -> 58 | ct:comment("Should correctly ignore warnings"), 59 | [#{file := "ignore.erl", text := <<"ignore_arg2/2 doesn't need its #2 argument">>}] = 60 | analyze(["ignore.erl"]), 61 | ok. 62 | 63 | %% @doc No warnings since rebar.config specifically states that all of them 64 | %% should be ignored. 65 | ignore_config(_) -> 66 | File = "ignore_config.erl", 67 | Files = [File], 68 | IgnoreSpecs = 69 | [{File, 70 | unnecessary_function_arguments, 71 | [{ignore_arg2, 3, 2}, 72 | {ignore_arg2, 2, 1}, 73 | {ignore_arg2, 2, 2}, 74 | {ignore_whole_func3, 3}, 75 | ignore_whole_func]}], 76 | #{results := [], unused_ignores := []} = 77 | hank_test_utils:analyze_and_sort(Files, IgnoreSpecs, [unnecessary_function_arguments]). 78 | 79 | %% @doc Common Test suites should be ignored 80 | ct_suite(_Config) -> 81 | ct:comment("CT suites should not generate warnings"), 82 | Files = 83 | case code:which(ct_suite) of 84 | non_existing -> % OTP < 23.2 85 | ["old_ct_SUITE.erl"]; 86 | _ -> 87 | ["ct_SUITE.erl"] 88 | end, 89 | [] = analyze(Files). 90 | 91 | %% @doc Compiler flag "export_all" should be supported 92 | export_all(_Config) -> 93 | [] = analyze(["export_all1.erl"]), 94 | [] = analyze(["export_all2.erl"]). 95 | 96 | analyze(Files) -> 97 | #{results := Results, unused_ignores := []} = 98 | hank_test_utils:analyze_and_sort(Files, [unnecessary_function_arguments]), 99 | Results. 100 | -------------------------------------------------------------------------------- /test/unused_callbacks_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(unused_callbacks_SUITE). 2 | 3 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 4 | -export([with_warnings/1, with_macros/1, without_warnings/1, ignore/1, ignore_config/1]). 5 | 6 | all() -> 7 | [with_warnings, with_macros, without_warnings, ignore, ignore_config]. 8 | 9 | init_per_testcase(_, Config) -> 10 | hank_test_utils:init_per_testcase(Config, "unused_callbacks"). 11 | 12 | end_per_testcase(_, Config) -> 13 | hank_test_utils:end_per_testcase(Config). 14 | 15 | %% @doc Hank finds unused callbacks 16 | with_warnings(_Config) -> 17 | ct:comment("Should detect and display warnings for unused callbacks"), 18 | 19 | File = "warnings.erl", 20 | [#{file := File, 21 | text := <<"Callback unused_callback/1 is not used anywhere in the module">>}] = 22 | analyze([File]), 23 | {comment, ""}. 24 | 25 | %% @doc Hank finds unused callbacks with macros 26 | with_macros(_Config) -> 27 | ct:comment("Should detect and display warnings for unused callbacks with macros"), 28 | 29 | File = "macros.erl", 30 | [#{file := File, 31 | text := <<"Callback unused_callback/0 is not used anywhere in the module">>}] = 32 | analyze([File]), 33 | {comment, ""}. 34 | 35 | %% @doc Hank finds nothing! 36 | without_warnings(_Config) -> 37 | ct:comment("Should not detect anything since the file is clean from warnings"), 38 | [] = analyze(["clean.erl"]), 39 | {comment, ""}. 40 | 41 | %% @doc Hank ignores some callbacks 42 | ignore(_Config) -> 43 | ct:comment("Should only detect the callbacks that are not ignored"), 44 | [#{file := "ignore.erl", 45 | text := <<"Callback just_one/0 is not used anywhere in the module">>}, 46 | #{file := "ignore.erl", 47 | text := <<"Callback just_one/2 is not used anywhere in the module">>}] = 48 | analyze(["ignore.erl"]), 49 | {comment, ""}. 50 | 51 | %% @doc No warnings since rebar.config specifically states that all of them 52 | %% should be ignored. 53 | ignore_config(_) -> 54 | File = "ignore_config.erl", 55 | Files = [File], 56 | IgnoreSpecs = 57 | [{File, unused_callbacks, [all_arities, {just_one, 0}, {just_one, 1}, {just_one, 2}]}], 58 | #{results := [], unused_ignores := []} = 59 | hank_test_utils:analyze_and_sort(Files, IgnoreSpecs, [unused_callbacks]). 60 | 61 | analyze(Files) -> 62 | #{results := Results, unused_ignores := []} = 63 | hank_test_utils:analyze_and_sort(Files, [unused_callbacks]), 64 | Results. 65 | -------------------------------------------------------------------------------- /test/unused_hrls_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Tests for the unused_hrls rule 2 | -module(unused_hrls_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([unused/1, local_include/1, remote_include/1, local_include_lib/1, 6 | remote_include_lib/1, versioned_include_lib/1]). 7 | 8 | all() -> 9 | [unused, 10 | local_include, 11 | remote_include, 12 | local_include_lib, 13 | remote_include_lib, 14 | versioned_include_lib]. 15 | 16 | init_per_testcase(_, Config) -> 17 | hank_test_utils:init_per_testcase(Config, "unused_hrls"). 18 | 19 | end_per_testcase(_, Config) -> 20 | hank_test_utils:end_per_testcase(Config). 21 | 22 | %% @doc Hank finds unused header files 23 | unused(_) -> 24 | ct:comment("If there are no Erlang files, all hrls should be unused."), 25 | OnlyHrls = ["lib/app0-with-other-name/include/header.hrl", "lib/app1/include/header.hrl"], 26 | [#{file := "lib/app0-with-other-name/include/header.hrl", text := "This file is unused"}, 27 | #{file := "lib/app1/include/header.hrl", text := "This file is unused"}] = 28 | analyze(OnlyHrls), 29 | 30 | ct:comment("If there are Erlang files that don't include the hrls, all " 31 | "hrls should be unused."), 32 | WithErls = 33 | ["lib/app0-with-other-name/include/header.hrl", 34 | "lib/app1/include/header.hrl", 35 | "lib/app1/src/app1_not_using_header.erl", 36 | "lib/app2/src/app2_not_using_header.erl"], 37 | [#{file := "lib/app0-with-other-name/include/header.hrl"}, 38 | #{file := "lib/app1/include/header.hrl"}] = 39 | analyze(WithErls), 40 | 41 | {comment, ""}. 42 | 43 | %% @doc Hank detects that a header file is used with an include 44 | local_include(_) -> 45 | ct:comment("lib/app1/include/header.hrl should not be marked as unused " 46 | "since it is used locally"), 47 | Apps1And0 = 48 | ["lib/app1/include/header.hrl", 49 | "lib/app1/src/app1_not_using_header.erl", 50 | "lib/app1/src/app1_include.erl"], 51 | [] = analyze(Apps1And0), 52 | 53 | ct:comment("include/header.hrl should not be marked as unused since it " 54 | "is used locally"), 55 | ok = 56 | file:set_cwd( 57 | filename:join( 58 | code:lib_dir(rebar3_hank), "test/files/unused_hrls/lib/app1")), 59 | OnlyApp1 = 60 | ["include/header.hrl", "src/app1_not_using_header.erl", "src/app1_include.erl"], 61 | [] = analyze(OnlyApp1), 62 | 63 | {comment, ""}. 64 | 65 | remote_include(_) -> 66 | ct:comment("lib/app1/include/header.hrl should not be marked as unused " 67 | "since it is used from app2"), 68 | Apps1And0 = 69 | ["lib/app0-with-other-name/include/header.hrl", 70 | "lib/app1/include/header.hrl", 71 | "lib/app2/src/app2_not_using_header.erl", 72 | "lib/app2/src/app2_include.erl"], 73 | [#{file := "lib/app0-with-other-name/include/header.hrl", 74 | text := "This file is unused"}] = 75 | analyze(Apps1And0), 76 | 77 | {comment, ""}. 78 | 79 | local_include_lib(_) -> 80 | ct:comment("include/header.hrl should not be marked as unused since it " 81 | "is used in app1_include_lib"), 82 | Apps = 83 | #{app0 => "lib/app0-with-other-name", 84 | app1 => "lib/app1", 85 | app2 => "lib/app2"}, 86 | Context = hank_test_utils:mock_context(Apps, maps:keys(Apps)), 87 | hank_test_utils:set_cwd("unused_hrls/lib/app1"), 88 | OnlyApp1 = 89 | ["include/header.hrl", "src/app1_not_using_header.erl", "src/app1_include_lib.erl"], 90 | [] = analyze(OnlyApp1, Context), 91 | 92 | {comment, ""}. 93 | 94 | remote_include_lib(_) -> 95 | ct:comment("No header file should be marked as unused since they're both " 96 | "used in app2_include_lib"), 97 | Apps1And0 = 98 | ["lib/app0-with-other-name/include/header.hrl", 99 | "lib/app1/include/header.hrl", 100 | "lib/app2/src/app2_not_using_header.erl", 101 | "lib/app2/src/app2_include_lib.erl"], 102 | [] = analyze(Apps1And0), 103 | 104 | {comment, ""}. 105 | 106 | versioned_include_lib(_) -> 107 | ct:comment("No header file should be marked as unused since they're both " 108 | "used in app1_include_lib"), 109 | Apps1And0 = 110 | ["lib/app0-with-other-name/include/header.hrl", 111 | "lib/app1/include/header.hrl", 112 | "lib/app1/src/app1_not_using_header.erl", 113 | "lib/app1/src/app1_include_lib.erl"], 114 | [] = analyze(Apps1And0), 115 | 116 | {comment, ""}. 117 | 118 | analyze(Files) -> 119 | Apps = 120 | #{app0 => "lib/app0-with-other-name", 121 | app1 => "lib/app1", 122 | app2 => "lib/app2"}, 123 | Context = hank_test_utils:mock_context(Apps, maps:keys(Apps)), 124 | analyze(Files, Context). 125 | 126 | analyze(Files, Context) -> 127 | #{results := Results, unused_ignores := []} = 128 | hank_test_utils:analyze_and_sort(Files, [unused_hrls], Context), 129 | Results. 130 | -------------------------------------------------------------------------------- /test/unused_ignores_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Tests for the unused ignore warnings 2 | -module(unused_ignores_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([unused_ignores/1]). 6 | 7 | all() -> 8 | [unused_ignores]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "unused_ignores"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc Hank finds unused record files 17 | unused_ignores(_) -> 18 | Files = filelib:wildcard("*.?rl"), 19 | IgnoreSpecs = [{"ignore_config.erl", unused_macros, ["MACRO_FROM_CONFIG"]}], 20 | #{results := [], 21 | unused_ignores := 22 | [{"ignore_config.erl", unused_macros, ["MACRO_FROM_CONFIG"]}, 23 | {"unused_ignores.erl", bad_rule, all}, 24 | {"unused_ignores.erl", 25 | unnecessary_function_arguments, 26 | [{non_exported_function_with, 2, 1}]}, 27 | {"unused_ignores.erl", unused_macros, ["MACRO", "MICRO"]}]} = 28 | hank_test_utils:analyze_and_sort(Files, IgnoreSpecs, [unused_macros]). 29 | -------------------------------------------------------------------------------- /test/unused_macros_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Tests for the unused_macros rule 2 | -module(unused_macros_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([unused_macros/1]). 6 | 7 | all() -> 8 | [unused_macros]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "unused_macros"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc Hank finds unused record files 17 | unused_macros(_) -> 18 | Files = filelib:wildcard("*.?rl"), 19 | IgnoreSpecs = 20 | [{"ignore_config.erl", 21 | unused_macros, 22 | ["MACRO_ALL", {"MACRO_0", 0}, {"MACRO_1", 1}, {"MACRO_NONE", none}]}], 23 | #{results := 24 | [#{file := "double.erl", text := <<"?Y is unused">>}, 25 | #{file := "double.erl", text := <<"?Y is unused">>}, 26 | #{file := "header.hrl", text := <<"?UNUSED_MACRO is unused">>}, 27 | #{file := "header.hrl", text := <<"?UNUSED_MACRO_WITH/0 is unused">>}, 28 | #{file := "header.hrl", text := <<"?UNUSED_MACRO_WITH/1 is unused">>}, 29 | #{file := "header.hrl", text := <<"?UNUSED_MACRO_WITH/2 is unused">>}, 30 | #{file := "header.hrl", text := <<"?UNUSED_MACRO_UNICODE_ÇØÍ is unused"/utf8>>}, 31 | #{file := "ignore.erl", text := <<"?MACRO_0 is unused">>}, 32 | #{file := "ignore.erl", text := <<"?MACRO_0/1 is unused">>}, 33 | #{file := "ignore.erl", text := <<"?MACRO_0/2 is unused">>}, 34 | #{file := "ignore.erl", text := <<"?MACRO_1 is unused">>}, 35 | #{file := "ignore.erl", text := <<"?MACRO_1/0 is unused">>}, 36 | #{file := "ignore.erl", text := <<"?MACRO_1/2 is unused">>}, 37 | #{file := "ignore.erl", text := <<"?MACRO_NONE/0 is unused">>}, 38 | #{file := "ignore.erl", text := <<"?MACRO_NONE/1 is unused">>}, 39 | #{file := "ignore.erl", text := <<"?MACRO_NONE/2 is unused">>}, 40 | #{file := "unused_macro_sample.erl", text := <<"?UNUSED_MACRO is unused">>}, 41 | #{file := "unused_macro_sample.erl", text := <<"?UNUSED_MACRO_WITH/0 is unused">>}, 42 | #{file := "unused_macro_sample.erl", text := <<"?UNUSED_MACRO_WITH/1 is unused">>}, 43 | #{file := "unused_macro_sample.erl", text := <<"?UNUSED_MACRO_WITH/2 is unused">>}, 44 | #{file := "unused_macro_sample.erl", 45 | text := <<"?UNUSED_MACRO_UNICODE_ÇØÍ is unused"/utf8>>}, 46 | #{file := "unused_macro_sample.erl", text := <<"?macroIsAnAtom is unused">>}], 47 | unused_ignores := []} = 48 | hank_test_utils:analyze_and_sort(Files, IgnoreSpecs, [unused_macros]). 49 | -------------------------------------------------------------------------------- /test/unused_record_fields_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Tests for the unused_record_fields rule 2 | -module(unused_record_fields_SUITE). 3 | 4 | -export([all/0, init_per_testcase/2, end_per_testcase/2]). 5 | -export([unused_record_fields/1]). 6 | 7 | all() -> 8 | [unused_record_fields]. 9 | 10 | init_per_testcase(_, Config) -> 11 | hank_test_utils:init_per_testcase(Config, "unused_record_fields"). 12 | 13 | end_per_testcase(_, Config) -> 14 | hank_test_utils:end_per_testcase(Config). 15 | 16 | %% @doc Hank finds unused record files 17 | unused_record_fields(_) -> 18 | Files = filelib:wildcard("*.?rl"), 19 | IgnoreSpecs = 20 | [{"ignore_config.erl", 21 | unused_record_fields, 22 | [ignored_record, {a_record, ignored_field1}, {a_record, ignored_field2}]}], 23 | #{results := 24 | [#{file := "header.hrl", 25 | text := <<"Field really_unused_field in record really_unused_record is unused">>}, 26 | #{file := "ignore.erl", text := <<"Field unused_field in record a_record is unused">>}, 27 | #{file := "macros.erl", text := <<"Field unused_field in record a_rec is unused">>}, 28 | #{file := "unused_record_field_sample.erl", 29 | text := <<"Field unused_field in record a_record is unused">>}, 30 | #{file := "unused_record_field_sample.erl", 31 | text := <<"Field unused_typed_field in record a_record is unused">>}, 32 | #{file := "unused_record_field_sample.erl", 33 | text := <<"Field unused_field_with_default in record a_record is unused">>}, 34 | #{file := "unused_record_field_sample.erl", 35 | text := 36 | <<"Field unused_typed_field_with_default in record a_record is " 37 | "unused">>}, 38 | #{file := "unused_record_field_sample.erl", 39 | text := <<"Field 'attr_αåβö' in record 'unicode_αåβö' is unused"/utf8>>}], 40 | unused_ignores := []} = 41 | hank_test_utils:analyze_and_sort(Files, IgnoreSpecs, [unused_record_fields]). 42 | --------------------------------------------------------------------------------