├── .credo.exs ├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── coveralls.json ├── examples └── my_code │ ├── .credo.exs │ ├── .formatter.exs │ ├── .gitignore │ ├── .recode.exs │ ├── README.md │ ├── config.exs │ ├── config │ └── alias_order.exs │ ├── lib │ ├── my_code.ex │ └── my_code │ │ ├── alias_expansion.ex │ │ ├── alias_order.ex │ │ ├── deep.ex │ │ ├── empty.ex │ │ ├── everything_ok.ex │ │ ├── fun.ex │ │ ├── happy.ex │ │ ├── multi.ex │ │ ├── pipe_fun_one.ex │ │ ├── raise_task.ex │ │ ├── same_line.ex │ │ ├── single_pipe.ex │ │ ├── tags.ex │ │ └── trailing_comma.ex │ ├── mix.exs │ ├── mix.lock │ ├── scripts │ ├── backup.bin │ └── backup.exs │ └── test │ ├── my_code_test.ex │ └── test_helper.exs ├── lib ├── mix │ └── tasks │ │ ├── recode.ex │ │ ├── recode.gen.config.ex │ │ ├── recode.help.ex │ │ └── recode.update.config.ex ├── recode.ex └── recode │ ├── application.ex │ ├── ast.ex │ ├── cli_formatter.ex │ ├── config.ex │ ├── context.ex │ ├── event_manager.ex │ ├── formatter.ex │ ├── formatter_plugin.ex │ ├── issue.ex │ ├── runner.ex │ ├── runner │ └── impl.ex │ ├── task.ex │ └── task │ ├── alias_expansion.ex │ ├── alias_order.ex │ ├── dbg.ex │ ├── enforce_line_length.ex │ ├── filter_count.ex │ ├── format.ex │ ├── io_inspect.ex │ ├── locals_without_parens.ex │ ├── moduledoc.ex │ ├── nesting.ex │ ├── pipe_fun_one.ex │ ├── single_pipe.ex │ ├── specs.ex │ ├── tag_fixme.ex │ ├── tag_todo.ex │ ├── tags.ex │ ├── test_file_ext.ex │ ├── unnecessary_if_unless.ex │ └── unused_variable.ex ├── mix.exs ├── mix.lock ├── recode.exs ├── test ├── fixtures │ ├── config.exs │ ├── context │ │ ├── doc_and_spec.ex │ │ ├── impl.ex │ │ ├── nested.ex │ │ ├── simple.ex │ │ ├── use_import_etc.ex │ │ ├── vars.ex │ │ ├── vars.ex.output │ │ ├── vars.exs │ │ ├── vars.exs.output │ │ └── when.ex │ ├── invalid_task_config.exs │ ├── rename │ │ ├── config.exs │ │ ├── exp │ │ │ ├── as.ex │ │ │ ├── bar.ex │ │ │ ├── baz.ex │ │ │ ├── call.ex │ │ │ ├── capture.ex │ │ │ ├── definition.ex │ │ │ ├── import.ex │ │ │ ├── import_no_change.ex │ │ │ ├── no_debug_info.ex │ │ │ ├── other_definition.ex │ │ │ ├── setup_do.exs │ │ │ ├── use.ex │ │ │ └── with_arity.ex │ │ └── lib │ │ │ ├── as.ex │ │ │ ├── bar.ex │ │ │ ├── baz.ex │ │ │ ├── call.ex │ │ │ ├── capture.ex │ │ │ ├── definition.ex │ │ │ ├── import.ex │ │ │ ├── import_no_change.ex │ │ │ ├── no_debug_info.ex │ │ │ ├── other_definition.ex │ │ │ ├── setup_do.exs │ │ │ ├── use.ex │ │ │ └── with_arity.ex │ └── runner │ │ ├── config.exs │ │ ├── lib │ │ └── foo.ex │ │ └── test │ │ └── foo_test.exs ├── mix │ └── tasks │ │ ├── recode.gen.config_test.exs │ │ ├── recode.help_test.exs │ │ ├── recode.update.config_test.exs │ │ └── recode_test.exs ├── recode │ ├── ast_test.exs │ ├── cli_formatter_test.exs │ ├── config_test.exs │ ├── context_test.exs │ ├── formatter_plugin_test.exs │ ├── formatter_test.exs │ ├── issue_test.exs │ ├── runner │ │ └── impl_test.exs │ ├── task │ │ ├── alias_expansion_test.exs │ │ ├── alias_order_test.exs │ │ ├── dbg_test.exs │ │ ├── enforce_line_length_test.exs │ │ ├── filter_count_test.exs │ │ ├── format_test.exs │ │ ├── io_inspect_test.exs │ │ ├── locals_without_parens.exs │ │ ├── moduledoc_test.exs │ │ ├── nesting_test.exs │ │ ├── pipe_fun_one_test.exs │ │ ├── single_pipe_test.exs │ │ ├── specs_test.exs │ │ ├── tag_fixme_test.exs │ │ ├── tag_todo_test.exs │ │ ├── tags_test.exs │ │ ├── test_file_ext_test.exs │ │ ├── unnecessary_if_unless_test.exs │ │ └── unused_variable_test.exs │ └── task_test.exs ├── recode_test.exs ├── support │ ├── fake_plugin.ex │ ├── plts │ │ └── .keep │ ├── recode_case.ex │ └── strip.ex └── test_helper.exs └── tmp └── .keep /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | ~r|lib/mix/tasks/recode.ex:.*:no_return| 3 | ] 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [assert: 1] 5 | ] 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Created with GitHubActions version 0.3.0 2 | name: CI 3 | env: 4 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 5 | on: 6 | - pull_request 7 | - push 8 | jobs: 9 | linux: 10 | name: Test on Ubuntu (Elixir ${{ matrix.elixir }}, OTP ${{ matrix.otp }}) 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | include: 15 | - elixir: '1.18.1' 16 | otp: '27.2' 17 | - elixir: '1.18.1' 18 | otp: '26.2' 19 | - elixir: '1.18.1' 20 | otp: '25.3' 21 | - elixir: '1.17.3' 22 | otp: '25.3' 23 | - elixir: '1.16.3' 24 | otp: '24.3' 25 | - elixir: '1.15.8' 26 | otp: '24.3' 27 | - elixir: '1.14.5' 28 | otp: '23.3' 29 | - elixir: '1.13.4' 30 | otp: '22.3' 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Elixir 35 | id: setup-beam 36 | uses: erlef/setup-beam@v1 37 | with: 38 | elixir-version: ${{ matrix.elixir }} 39 | otp-version: ${{ matrix.otp }} 40 | - name: Restore deps 41 | uses: actions/cache@v4 42 | with: 43 | path: deps 44 | key: deps-${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}-${{ steps.setup-beam.outputs.setup-beam-version }} 45 | - name: Restore _build 46 | uses: actions/cache@v4 47 | with: 48 | path: _build 49 | key: _build-${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}-${{ steps.setup-beam.outputs.setup-beam-version }} 50 | - name: Restore test/support/plts 51 | if: ${{ matrix.elixir == '1.18.1' && matrix.otp == '27.2' }} 52 | uses: actions/cache@v4 53 | with: 54 | path: test/support/plts 55 | key: test/support/plts-${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}-${{ steps.setup-beam.outputs.setup-beam-version }} 56 | - name: Get dependencies 57 | run: mix deps.get 58 | - name: Compile dependencies 59 | run: MIX_ENV=test mix deps.compile 60 | - name: Compile project 61 | run: MIX_ENV=test mix compile --warnings-as-errors 62 | - name: Check unused dependencies 63 | if: ${{ matrix.elixir == '1.18.1' && matrix.otp == '27.2' }} 64 | run: mix deps.unlock --check-unused 65 | - name: Check code format 66 | if: ${{ matrix.elixir == '1.18.1' && matrix.otp == '27.2' }} 67 | run: mix format --check-formatted 68 | - name: Lint code 69 | if: ${{ matrix.elixir == '1.18.1' && matrix.otp == '27.2' }} 70 | run: mix credo --strict 71 | - name: Run tests 72 | if: ${{ !(matrix.elixir == '1.18.1' && matrix.otp == '27.2') }} 73 | run: mix test 74 | - name: Run tests with coverage 75 | if: ${{ matrix.elixir == '1.18.1' && matrix.otp == '27.2' }} 76 | run: mix coveralls.github 77 | - name: Static code analysis 78 | if: ${{ matrix.elixir == '1.18.1' && matrix.otp == '27.2' }} 79 | run: mix dialyzer --format github --force-check 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | recode-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/* 27 | !/tmp/.keep 28 | 29 | # dialyzer plt 30 | /test/support/plts/*.plt 31 | /test/support/plts/*.plt.hash 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.7.3 - 2024/07/25 4 | 5 | + Add config for preformatter. 6 | + Add `Recode.Task.UnnecessaryIfUnless` 7 | + Add `Recode.Task.LocalsWithoutParens` 8 | + Add `Recode.Task.Moduledoc` 9 | + Fix `Recode.Task.Tags` 10 | 11 | ## 0.7.2 - 2024/02/10 12 | 13 | + Fix AST.alias_info fot `alias __MODULE__, as: MyModule`. 14 | 15 | ## 0.7.1 - 2024/01/09 16 | 17 | + Fix bug in `Recode.Task.Tags`. 18 | 19 | ## 0.7.0 - 2024/01/07 20 | 21 | + Refactor formatter and use `Escape`. 22 | + Add switch `--color` to mix task recode. 23 | + Add option `color` to config. 24 | + Run recode tasks async. 25 | + Remove `Recode.StopWatch` 26 | + Refactor `Recode.FormatterPlugin` 27 | + Improve config validation. 28 | 29 | ## 0.6.5 - 2023/10/09 30 | 31 | + Start `:recode` appliations in `mix recode` task. 32 | 33 | ## 0.6.4 - 2023/09/15 34 | 35 | + Fix `exclude_plugins` arg. 36 | 37 | ## 0.6.3 - 2023/09/15 38 | 39 | + Fix `Recode.FormatterPlugin`. 40 | + Add switch `--debug` (for now undocumented). 41 | 42 | ## 0.6.2 - 2023/09/04 43 | 44 | + Fix runner impl for `mix format`. 45 | + Fix typos. 46 | 47 | ## 0.6.1 - 2023/08/27 48 | 49 | + Use `rewrite` version `~> 0.8`. 50 | 51 | ## 0.6.0 - 2023/08/26 52 | 53 | + Add `Recode.Task.Dbg`. 54 | + Add `Recode.Task.FilterCount`. 55 | + Add `Recode.Task.IOInspect`. 56 | + Add `Recode.Task.Nesting`. 57 | + Add `Recode.Task.TagFIXME`. 58 | + Add `Recode.Task.TagTODO`. 59 | + Add mix task `recode.help`. 60 | + Add mix task `recode.update.config`. 61 | + Use switch `--task` multiple times. 62 | + Refactor `RecodeCase` 63 | + Add some minor fixes for `Recode.Task.AliasOrder`. 64 | + Fix file count output. 65 | + Add callback `init/1` to `Recode.Task`. 66 | + Add validation of `task` and `config` in `Mix.Tasks.Recode` 67 | 68 | ## 0.5.2 - 2023/07/17 69 | 70 | + Bump `rewrite` to 0.7.0. 71 | 72 | ## 0.5.1 - 2023/05/19 73 | 74 | + Fix `Recode.Task.AliasExpansion` 75 | 76 | ## 0.5.0 - 2023/05/05 77 | 78 | + Add `Recode.FormatterPlugin` 79 | 80 | ## 0.4.4 - 2023/03/17 81 | 82 | + Refactor `Recode.Task.EnforceLineLength`. 83 | + Add `Recode.Runner.run/3`. 84 | + Fix `Recode.Task.AliasOrder` 85 | + Add dir `apps` to the default config. 86 | 87 | ## 0.4.3 - 2023/02/04 88 | 89 | + Refactor recode formatter task. 90 | 91 | ## 0.4.2 - 2022/12/10 92 | 93 | + Fixing file exclusion. 94 | 95 | ## 0.4.1 - 2022/11/05 96 | 97 | + Remove unnecessary compile call 98 | + Fix handling of multiple input files 99 | 100 | ## 0.4.0 - 2022/09/09 101 | 102 | + Add option `-` to `mix recode` to read from stdin. 103 | + Add `Recode.Task.UnusedVariable`. 104 | + Update `Recode.Task.SinglePipe`. Some false positives are fixed. 105 | + Update `Recode.Task.PipeFunOne`. Some false positives are fixed. 106 | + The modules `Recode.Project`, `Recode.Source`, and etc moving to the package 107 | [`rewrite`](https://github.com/hrzndhrn/rewrite). 108 | + Catch exceptions raised in tasks and output a warning for each exception. 109 | + Remove `mix` task `recode.rename`. `Recode` gets a focus on linting and 110 | autocorrection with this change. The refactoring functionality will move to 111 | another package. 112 | 113 | 114 | ## 0.3.0 - 2022/08/28 115 | 116 | + Rename `Recode.Taks.SameLine` to `Recode.Task.EnforceLineLength`. 117 | 118 | ## 0.2.0 - 2022/08/21 119 | 120 | + Refactor config. 121 | + Add `Recode.Task.SameLine`. 122 | + Add flag `--task` to `mix recode`. 123 | 124 | ## 0.1.3 - 2022/07/26 125 | 126 | + Fix `Recode.Task.Rename` 127 | 128 | ## 0.1.3 - 2022/07/24 129 | 130 | + Fix `Recode.Task.SinglePipe`. 131 | + Fix bugs in `Recode.Context`. 132 | 133 | ## 0.1.2 - 2022/07/13 134 | 135 | + Add options `:macros` to `Recode.Task.Specs`. 136 | 137 | ## 0.1.1 - 2022/07/06 138 | 139 | + Bug fixes. 140 | + Added `recode.exs` to run `recode` with `recode`. 141 | + Changes to run `mix recode --dry --config recode.exs` without any update or 142 | issue. 143 | 144 | ## 0.1.0 - 2022/07/04 145 | 146 | + The very first version. 147 | -------------------------------------------------------------------------------- /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 contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empaty towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esurk.sucram@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marcus Kruse 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 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "treat_no_relevant_lines_as_covered": true, 4 | "minimum_coverage": 80 5 | }, 6 | "skip_files": [ 7 | "lib/recode/task.ex" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/my_code/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | # plugins: [], 3 | # plugins: [Recode.FormatterPlugin], 4 | # plugins: [FreedomFormatter], 5 | # plugins: [FreedomFormatter, Recode.FormatterPlugin], 6 | # trailing_comma: true, 7 | # recode: [ 8 | # tasks: [ 9 | # {Recode.Task.PipeFunOne, []}, 10 | # {Recode.Task.SinglePipe, []} 11 | # ] 12 | # ], 13 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 14 | locals_without_parens: [noop: 1] 15 | ] 16 | -------------------------------------------------------------------------------- /examples/my_code/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | my_code-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /examples/my_code/.recode.exs: -------------------------------------------------------------------------------- 1 | [ 2 | version: "0.7.2", 3 | # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. 4 | autocorrect: true, 5 | # With "--dry" no changes will be written to the files. 6 | # Can also be set/reset with `--dry`/`--no-dry`. 7 | # If dry is true then verbose is also active. 8 | dry: false, 9 | # Enables or disables color in the output. 10 | color: true, 11 | # Can also be set/reset with `--verbose`/`--no-verbose`. 12 | verbose: false, 13 | # Can be overwritten by calling `mix recode "lib/**/*.ex"`. 14 | inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"], 15 | formatters: [Recode.CLIFormatter], 16 | tasks: [ 17 | # Tasks could be added by a tuple of the tasks module name and an options 18 | # keyword list. A task can be deactivated by `active: false`. The execution of 19 | # a deactivated task can be forced by calling `mix recode --task ModuleName`. 20 | # {Recode.Task.Format, []}, 21 | # {Recode.Task.Format, config: [formatter: :sourceror]}, 22 | # {Recode.Task.Format, config: [formatter: :elixir]}, 23 | # {Recode.Task.Format, active: false}, 24 | {Recode.Task.AliasExpansion, []}, 25 | {Recode.Task.AliasOrder, []}, 26 | {Recode.Task.Dbg, [autocorrect: false]}, 27 | {Recode.Task.EnforceLineLength, [active: false]}, 28 | {Recode.Task.FilterCount, []}, 29 | {Recode.Task.IOInspect, [autocorrect: false]}, 30 | {Recode.Task.Nesting, []}, 31 | {Recode.Task.PipeFunOne, []}, 32 | {Recode.Task.SinglePipe, []}, 33 | {Recode.Task.Specs, [exclude: ["test/**/*.{ex,exs}", "mix.exs"], config: [only: :visible]]}, 34 | {Recode.Task.TagFIXME, [exit_code: 2]}, 35 | {Recode.Task.TagTODO, [exit_code: 4]}, 36 | {Recode.Task.TestFileExt, []}, 37 | {Recode.Task.UnusedVariable, [active: false]} 38 | ] 39 | ] 40 | -------------------------------------------------------------------------------- /examples/my_code/README.md: -------------------------------------------------------------------------------- 1 | # MyCode 2 | 3 | A Project with some nonsense code to test `recode`. 4 | 5 | The project provides two scripts 6 | - `mix backup` to backup the source code 7 | - `mix backup.restore` to write the backup back 8 | -------------------------------------------------------------------------------- /examples/my_code/config.exs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /examples/my_code/config/alias_order.exs: -------------------------------------------------------------------------------- 1 | alias Recode.Task 2 | 3 | [ 4 | # Can also be set/reset with "--autocorrect"/"--no-autocorrect". 5 | autocorrect: true, 6 | # With "--dry" no changes will be written to the files. 7 | # Can also be set/reset with "--dry"/"--no-dry". 8 | # If dry is true then verbose is also active. 9 | dry: false, 10 | # Can also be set/reset with "--verbose"/"--no-verbose". 11 | verbose: false, 12 | inputs: ["{config,lib,test}/**/*.{ex,exs}"], 13 | formatter: {Recode.Formatter, []}, 14 | tasks: [ 15 | {Task.AliasOrder, []} 16 | ] 17 | ] 18 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode do 2 | @moduledoc """ 3 | Documentation for `MyCode`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> MyCode.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/alias_expansion.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.AliasExpansion do 2 | alias MyCode.{PipeFunOne, SinglePipe} 3 | 4 | def foo(x) do 5 | SinglePipe.double(x) + PipeFunOne.double(x) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/alias_order.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.Echo do 2 | @moduledoc false 3 | 4 | def say, do: :echo 5 | end 6 | 7 | defmodule MyCode.Foxtrot do 8 | @moduledoc false 9 | 10 | def say, do: :foxtrot 11 | end 12 | 13 | defmodule Mycode.AliasOrder do 14 | alias MyCode.SinglePipe 15 | alias MyCode.PipeFunOne 16 | alias MyCode.{Foxtrot, Echo} 17 | 18 | @doc false 19 | def foo do 20 | {SinglePipe.double(2), PipeFunOne.double(3)} 21 | end 22 | 23 | @doc false 24 | def echo_foxtrot, do: {Echo.say(), Foxtrot.say()} 25 | end 26 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/deep.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.Deep do 2 | def abbys(stare) do 3 | if stare do 4 | cond do 5 | stare -> 6 | case stare do 7 | true -> :stare 8 | false -> :error 9 | end 10 | 11 | true -> 12 | :error 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/empty.ex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrzndhrn/recode/42042cfbb7dd594c21cd12eb58eb38e6c11f70af/examples/my_code/lib/my_code/empty.ex -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/everything_ok.ex: -------------------------------------------------------------------------------- 1 | defmodule MyApp.EverythingOk do 2 | @moduledoc false 3 | 4 | def ok, do: :ok 5 | end 6 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/fun.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.Fun do 2 | @moduledoc false 3 | 4 | def noop(x), do: x 5 | end 6 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/happy.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.Happy do 2 | @moduledoc """ 3 | Don't worry, be happy 4 | """ 5 | 6 | @doc """ 7 | Returns always `:happy`. 8 | """ 9 | @spec mood :: :happy 10 | def mood, do: :happy 11 | end 12 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/multi.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.Multi do 2 | 3 | import MyCode.Fun 4 | 5 | def double(x), do: x + x 6 | 7 | def pipe(x) do 8 | x |> double |> double() |> dbg() 9 | end 10 | 11 | def single(x) do 12 | x |> double() 13 | end 14 | 15 | def without_parens(x) do 16 | noop x 17 | end 18 | 19 | def my_count(list) do 20 | list 21 | |> Enum.filter(fn x -> rem(x, 2) == 0 end) 22 | |> Enum.count() 23 | |> IO.inspect() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/pipe_fun_one.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.PipeFunOne do 2 | @moduledoc false 3 | 4 | def double(x), do: x + x 5 | 6 | def pipe(x) do 7 | x |> double |> double() 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/raise_task.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.RaiseTask do 2 | @moduledoc false 3 | 4 | use Recode.Task, corrector: true 5 | 6 | @impl Recode.Task 7 | def run(_source, _opts) do 8 | raise "Ups" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/same_line.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.SameLine do 2 | def foo(x) 3 | when is_integer(x) do 4 | { 5 | :foo, 6 | x 7 | } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/single_pipe.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.SinglePipe do 2 | @moduledoc false 3 | 4 | def double(x), do: x + x 5 | 6 | def single_pipe(x) do 7 | x |> double() 8 | end 9 | 10 | def reverse(a), do: a |> Enum.reverse() 11 | end 12 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/tags.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.Tags do 2 | @moduledoc """ 3 | TODO: add docs 4 | """ 5 | 6 | # FIXME: add more functions 7 | def fun, do: :foo 8 | end 9 | -------------------------------------------------------------------------------- /examples/my_code/lib/my_code/trailing_comma.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCode.TrailingComma do 2 | @moduledoc false 3 | 4 | def list do 5 | [ 6 | 100_000, 7 | 200_000, 8 | 300_000, 9 | 400_000, 10 | 500_000, 11 | 600_000, 12 | 700_000, 13 | 800_000, 14 | 900_000, 15 | 1_000_000, 16 | 2_000_000, 17 | ] |> Enum.reverse() 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/my_code/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MyCode.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :my_code, 7 | version: "0.1.0", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | aliases: aliases(), 12 | ] 13 | end 14 | 15 | def application do 16 | [ 17 | extra_applications: [:logger], 18 | mod: {Recode.Application, []}, 19 | ] 20 | end 21 | 22 | defp aliases do 23 | [ 24 | backup: ["cmd elixir ./scripts/backup.exs"], 25 | "backup.restore": ["cmd elixir ./scripts/backup.exs restore"], 26 | ] 27 | end 28 | 29 | defp deps do 30 | [ 31 | {:recode, path: "../.."}, 32 | # dev/test 33 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 34 | {:freedom_formatter, "~> 2.1", only: :dev}, 35 | ] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /examples/my_code/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"}, 4 | "escape": {:hex, :escape, "0.1.0", "548edab75e6e6938b1e199ef59cb8e504bcfd3bcf83471d4ae9a3c7a7a3c7d45", [:mix], [], "hexpm", "a5d8e92db4677155df54bc1306d401b5233875d570d474201db03cb3047491cd"}, 5 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 6 | "freedom_formatter": {:hex, :freedom_formatter, "2.1.0", "0578dce24ee370cfcd84306166fbf121b65e51482e7be7e96f4062982f929bfa", [:mix], [], "hexpm", "6522fd7f78214c48cfe7ff6d269a56819ec004a09cd1c08809a3ab895504ee01"}, 7 | "glob_ex": {:hex, :glob_ex, "0.1.6", "3a311ade50f6b71d638af660edcc844c3ab4eb2a2c816cfebb73a1d521bb2f9d", [:mix], [], "hexpm", "fda1e90e10f6029bd72967fef0c9891d0d14da89ca7163076e6028bfcb2c42fa"}, 8 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 9 | "rewrite": {:hex, :rewrite, "0.10.0", "5d756b6dc67679e7156ff6055f9654be02dbaeb177aaf1ff6af7ee8da8718248", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.13", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "68d7808cf549e7bf51b0119a8edc14d50970bad479115249030baa580c1d7b50"}, 10 | "sourceror": {:hex, :sourceror, "0.14.1", "c6fb848d55bd34362880da671debc56e77fd722fa13b4dcbeac89a8998fc8b09", [:mix], [], "hexpm", "8b488a219e4c4d7d9ff29d16346fd4a5858085ccdd010e509101e226bbfd8efc"}, 11 | } 12 | -------------------------------------------------------------------------------- /examples/my_code/scripts/backup.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrzndhrn/recode/42042cfbb7dd594c21cd12eb58eb38e6c11f70af/examples/my_code/scripts/backup.bin -------------------------------------------------------------------------------- /examples/my_code/scripts/backup.exs: -------------------------------------------------------------------------------- 1 | defmodule Backup do 2 | @inputs "{config,lib,test}/**/*" 3 | @backup "scripts/backup.bin" 4 | 5 | def run([]) do 6 | @inputs 7 | |> Path.wildcard() 8 | |> Enum.reject(&File.dir?/1) 9 | |> Enum.into(%{}, fn path -> {path, File.read!(path)} end) 10 | |> backup() 11 | end 12 | 13 | def run(["restore"]) do 14 | IO.puts("restoring form backup #{@backup}") 15 | 16 | Enum.each(["lib", "test", "config"], fn dir -> File.rm_rf!(dir) end) 17 | 18 | files = @backup |> File.read!() |> :erlang.binary_to_term() 19 | 20 | Enum.each(files, fn {path, data} -> 21 | IO.puts("restoring #{path}") 22 | path |> Path.dirname() |> File.mkdir_p!() 23 | File.write!(path, data) 24 | end) 25 | end 26 | 27 | def run(argv) do 28 | raise "Unknown args #{inspect(argv)}" 29 | end 30 | 31 | defp backup(files) do 32 | data = :erlang.term_to_binary(files, compressed: 9) 33 | File.write!(@backup, data) 34 | IO.puts("backup saved to #{@backup}") 35 | end 36 | end 37 | 38 | Backup.run(System.argv()) 39 | -------------------------------------------------------------------------------- /examples/my_code/test/my_code_test.ex: -------------------------------------------------------------------------------- 1 | defmodule MyCodeTest do 2 | use ExUnit.Case 3 | doctest MyCode 4 | 5 | test "greets the world" do 6 | assert MyCode.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /examples/my_code/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/mix/tasks/recode.gen.config.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Recode.Gen.Config do 2 | @shortdoc "Generates a new config for Recode" 3 | 4 | @moduledoc """ 5 | #{@shortdoc}. Writes the file `.recode.exs` in the root directory of the mix 6 | project. 7 | 8 | The default config: 9 | ```elixir 10 | #{Recode.Config.to_string()} 11 | ``` 12 | """ 13 | 14 | use Mix.Task 15 | 16 | @impl true 17 | def run([]) do 18 | Mix.Generator.create_file(".recode.exs", Recode.Config.to_string()) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mix/tasks/recode.help.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Recode.Help do 2 | @shortdoc "Lists recode tasks" 3 | 4 | @moduledoc """ 5 | Lists all availbale recode tasks with a short description or prints the 6 | documentation for a given recode task. 7 | 8 | To print the documentation of a task run `mix recode.help {task-name}`. As a 9 | task name the module name (e.g. `Nesting`) or the full module name (e.g. 10 | `Recode.Task.Nesting`) is accepted. 11 | """ 12 | 13 | use Mix.Task 14 | 15 | @task_namespace "Recode.Task." 16 | 17 | @impl Mix.Task 18 | def run([]) do 19 | :recode 20 | |> Application.spec(:modules) 21 | |> Enum.filter(&task?/1) 22 | |> Enum.group_by(&category/1, &info/1) 23 | |> print() 24 | end 25 | 26 | def run([task]) do 27 | module = 28 | :recode |> Application.spec(:modules) |> Enum.find(fn module -> task?(module, task) end) 29 | 30 | if module do 31 | IEx.Introspection.h(module) 32 | :ok 33 | else 34 | Mix.raise(""" 35 | The recode task #{task} could not be found. \ 36 | Run "mix recode.help" for a list of recode tasks.\ 37 | """) 38 | end 39 | end 40 | 41 | def run(_opts) do 42 | Mix.raise(""" 43 | recode.help does not support this command. For more information run "mix help recode.help"\ 44 | """) 45 | end 46 | 47 | defp category(module), do: Recode.Task.category(module) 48 | 49 | defp task?(module) do 50 | task? = module |> inspect() |> String.starts_with?(@task_namespace) 51 | 52 | task? and not is_nil(Recode.Task.shortdoc(module)) 53 | end 54 | 55 | defp task?(module, name) do 56 | with true <- task?(module) do 57 | inspect(module) == name or 58 | module |> inspect() |> String.trim_leading(@task_namespace) == name 59 | end 60 | end 61 | 62 | defp info(module) do 63 | name = module |> inspect() |> String.trim_leading(@task_namespace) 64 | {name, Recode.Task.shortdoc(module), Recode.Task.corrector?(module)} 65 | end 66 | 67 | defp print(info) do 68 | max = max_name_length(info) 69 | print("Design tasks:", info.design, max) 70 | print("Readability tasks:", info.readability, max) 71 | print("Refactor tasks:", info.refactor, max) 72 | print("Warning tasks:", info.warning, max) 73 | end 74 | 75 | defp print(section, tasks, max) do 76 | IO.puts(section) 77 | 78 | Enum.each(tasks, fn {task, doc, corrector?} -> 79 | type = if corrector?, do: "Corrector -", else: "Checker -" 80 | IO.puts(String.pad_trailing(task, max) <> " # #{type} #{doc}") 81 | end) 82 | end 83 | 84 | defp max_name_length(info) when is_map(info) do 85 | info 86 | |> Map.values() 87 | |> List.flatten() 88 | |> Enum.reduce(0, fn {name, _shortdoc, _corrector?}, max -> 89 | max(byte_size(name), max) 90 | end) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/mix/tasks/recode.update.config.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Recode.Update.Config do 2 | @shortdoc "Updates an existing config for Recode" 3 | 4 | @moduledoc """ 5 | #{@shortdoc}. 6 | 7 | The task merges the exsiting config into the actual config and updates the 8 | version. Using this task preserves changes in the actual config and adds new values. 9 | 10 | The acutal default config: 11 | ```elixir 12 | #{Recode.Config.to_string()} 13 | ``` 14 | """ 15 | 16 | use Mix.Task 17 | 18 | @config_filename ".recode.exs" 19 | @deprecated_configs [:formatter] 20 | 21 | @doc false 22 | def run([]), do: do_run(false) 23 | def run(["--force"]), do: do_run(true) 24 | def run(opts), do: Mix.raise("get unknown options: #{inspect(opts)}") 25 | 26 | defp do_run(force) do 27 | if File.exists?(@config_filename) do 28 | config = 29 | @config_filename 30 | |> Code.eval_file() 31 | |> elem(0) 32 | |> delete(@deprecated_configs) 33 | |> Recode.Config.merge() 34 | 35 | Mix.Generator.create_file(@config_filename, Recode.Config.to_string(config), force: force) 36 | else 37 | Mix.raise(~s|config file #{@config_filename} not found, run "mix recode.gen.config"|) 38 | end 39 | end 40 | 41 | defp delete(config, deprecated_configs) do 42 | Enum.reduce(deprecated_configs, config, fn key, config -> Keyword.delete(config, key) end) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/recode.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode do 2 | @moduledoc """ 3 | A linter with autocorrection and a refactoring tool. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /lib/recode/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | @impl true 7 | def start(_type, _args) do 8 | children = [{Task.Supervisor, name: Recode.TaskSupervisor, max_restarts: 10}] 9 | 10 | opts = [strategy: :one_for_one, name: Recode.Supervisor] 11 | 12 | Supervisor.start_link(children, opts) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/recode/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Config do 2 | @moduledoc """ 3 | Functions to read and merge the `Recode` configuration. 4 | """ 5 | 6 | alias Recode.Task.Format 7 | 8 | @type config :: keyword() 9 | 10 | @config_filename ".recode.exs" 11 | 12 | @config_version "0.7.3" 13 | 14 | # The minimum version of the config to run recode. This version marks the last 15 | # breaking change for handle the config. 16 | @config_min_version "0.7.1" 17 | 18 | @config_keys [:version, :autocorrect, :dry, :verbose, :inputs, :formatters, :tasks, :color] 19 | 20 | # The default configuration used by mix tasks recode.gen.config and 21 | # recode.update.config. 22 | @config_default [ 23 | version: @config_version, 24 | autocorrect: true, 25 | dry: false, 26 | color: true, 27 | verbose: false, 28 | inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"], 29 | formatters: [Recode.CLIFormatter], 30 | tasks: [ 31 | {Recode.Task.AliasExpansion, []}, 32 | {Recode.Task.AliasOrder, []}, 33 | {Recode.Task.Dbg, [autocorrect: false]}, 34 | {Recode.Task.EnforceLineLength, [active: false]}, 35 | {Recode.Task.FilterCount, []}, 36 | {Recode.Task.IOInspect, [autocorrect: false]}, 37 | {Recode.Task.LocalsWithoutParens, []}, 38 | {Recode.Task.Moduledoc, []}, 39 | {Recode.Task.Nesting, []}, 40 | {Recode.Task.PipeFunOne, []}, 41 | {Recode.Task.SinglePipe, []}, 42 | {Recode.Task.Specs, [exclude: ["test/**/*.{ex,exs}", "mix.exs"], config: [only: :visible]]}, 43 | {Recode.Task.TagFIXME, exit_code: 2}, 44 | {Recode.Task.TagTODO, exit_code: 4}, 45 | {Recode.Task.TestFileExt, []}, 46 | {Recode.Task.UnnecessaryIfUnless, []}, 47 | {Recode.Task.UnusedVariable, [active: false]} 48 | ] 49 | ] 50 | 51 | @doc """ 52 | Returns the default configuration. 53 | """ 54 | @spec default() :: config() 55 | def default, do: @config_default 56 | 57 | @doc """ 58 | Returns the given config as a formatted string with comments. 59 | """ 60 | @spec to_string(config()) :: String.t() 61 | def to_string(config \\ default()) do 62 | config = Keyword.validate!(config, @config_keys) 63 | 64 | template = """ 65 | [ 66 | version: "<%= @config[:version] %>", 67 | # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. 68 | autocorrect: <%= @config[:autocorrect] %>, 69 | # With "--dry" no changes will be written to the files. 70 | # Can also be set/reset with `--dry`/`--no-dry`. 71 | # If dry is true then verbose is also active. 72 | dry: <%= @config[:dry] %>, 73 | # Enables or disables color in the output. 74 | color: <%= @config[:color] %>, 75 | # Can also be set/reset with `--verbose`/`--no-verbose`. 76 | verbose: <%= @config[:verbose] %>, 77 | # Can be overwritten by calling `mix recode "lib/**/*.ex"`. 78 | inputs: <%= inspect @config[:inputs] %>, 79 | formatters: <%= inspect @config[:formatters] %>, 80 | tasks: [ 81 | # Tasks could be added by a tuple of the tasks module name and an options 82 | # keyword list. A task can be deactivated by `active: false`. The execution of 83 | # a deactivated task can be forced by calling `mix recode --task ModuleName`. 84 | <%= for task <- @config[:tasks] do %><%= inspect task %>, 85 | <% end %> 86 | ] 87 | ] 88 | """ 89 | 90 | template 91 | |> EEx.eval_string(assigns: [config: config]) 92 | |> Code.format_string!() 93 | |> IO.iodata_to_binary() 94 | end 95 | 96 | @doc """ 97 | Merges two configs into one. 98 | 99 | The merge will do a deep merge. The merge will do a deep merge. The merge 100 | takes the version from the `right` config. 101 | 102 | ## Examples 103 | 104 | iex> new = [version: "0.0.2", verbose: false, autocorrect: true] 105 | ...> old = [version: "0.0.1", verbose: true] 106 | iex> Recode.Config.merge(new ,old) |> Enum.sort() 107 | [autocorrect: true, verbose: true, version: "0.0.2"] 108 | """ 109 | @spec merge(config, config) :: config 110 | def merge(left \\ default(), right) do 111 | Keyword.merge(left, right, fn 112 | :version, version, _ -> version 113 | :tasks, left, right -> merge_tasks(left, right) 114 | _, _, value -> value 115 | end) 116 | end 117 | 118 | defp merge_tasks(left, right) do 119 | left 120 | |> Keyword.merge(right, fn 121 | _key, left, [] -> left 122 | _key, left, right -> merge_task_config(left, right) 123 | end) 124 | |> Enum.sort() 125 | end 126 | 127 | defp merge_task_config(left, right) do 128 | Keyword.merge(left, right, fn 129 | :config, left, right -> left |> Keyword.merge(right) |> Enum.sort() 130 | _key, _left, right -> right 131 | end) 132 | end 133 | 134 | @doc """ 135 | Reads the `Recode` cofiguration from the given `path`. 136 | """ 137 | @spec read(Path.t()) :: {:ok, config()} | {:error, :not_found} 138 | def read(path \\ @config_filename) when is_binary(path) do 139 | case File.exists?(path) do 140 | true -> 141 | config = 142 | path 143 | |> Code.eval_file() 144 | |> elem(0) 145 | |> default_tasks() 146 | |> update_inputs() 147 | 148 | {:ok, config} 149 | 150 | false -> 151 | {:error, :not_found} 152 | end 153 | end 154 | 155 | @doc """ 156 | Validates the config version and tasks. 157 | """ 158 | @spec validate(config()) :: :ok | {:error, :out_of_date | :no_tasks} 159 | def validate(config) do 160 | with :ok <- validate_version(config) do 161 | validate_tasks(config) 162 | end 163 | end 164 | 165 | defp validate_version(config) do 166 | cmp = 167 | config 168 | |> Keyword.get(:version, @config_min_version) 169 | |> Version.compare(@config_min_version) 170 | 171 | if cmp == :lt do 172 | {:error, :out_of_date} 173 | else 174 | :ok 175 | end 176 | end 177 | 178 | defp validate_tasks(config) do 179 | if Keyword.has_key?(config, :tasks), do: :ok, else: {:error, :no_tasks} 180 | end 181 | 182 | defp default_tasks(config) do 183 | if has_task?(config, Format) do 184 | config 185 | else 186 | Keyword.update!(config, :tasks, fn tasks -> [{Format, []} | tasks] end) 187 | end 188 | end 189 | 190 | defp update_inputs(config) do 191 | Keyword.update(config, :inputs, [], fn inputs -> 192 | inputs |> List.wrap() |> Enum.map(fn input -> GlobEx.compile!(input) end) 193 | end) 194 | end 195 | 196 | defp has_task?(config, module) do 197 | config 198 | |> Keyword.fetch!(:tasks) 199 | |> Keyword.has_key?(module) 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /lib/recode/event_manager.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.EventManager do 2 | @moduledoc false 3 | 4 | def start_link do 5 | DynamicSupervisor.start_link(strategy: :one_for_one) 6 | end 7 | 8 | def stop(event_manager) do 9 | for {_, pid, _, _} <- DynamicSupervisor.which_children(event_manager) do 10 | GenServer.stop(pid, :normal, :infinity) 11 | end 12 | 13 | DynamicSupervisor.stop(event_manager) 14 | end 15 | 16 | def add_handler(event_manager, handler, opts) do 17 | DynamicSupervisor.start_child(event_manager, %{ 18 | id: GenServer, 19 | start: {GenServer, :start_link, [handler, opts]}, 20 | restart: :temporary 21 | }) 22 | end 23 | 24 | def notify(event_manager, message) do 25 | for {_, pid, _, _} <- Supervisor.which_children(event_manager) do 26 | GenServer.cast(pid, message) 27 | end 28 | 29 | :ok 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/recode/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Formatter do 2 | @moduledoc """ 3 | Helper functions for formatting and the formatting protocols. 4 | 5 | Formatters are `GenServer`s specified during `Recode` configuration that 6 | receive a series of events as casts. 7 | 8 | The following events are possible: 9 | 10 | * `{:prepared, %Rewrite{} = project, time}` - 11 | all files have been read. 12 | 13 | * `{:task_started, %Rewrite.Source{} = source, task}` - 14 | a task has started. 15 | 16 | * `{:task_finished, %Rewrite.Source{} = source, task, time}` . 17 | a task has finished. 18 | 19 | * `{:tasks_finished, %Rewrite{} = project, time}` - 20 | all tasks are finished. 21 | 22 | * `{:finished, %Rewrite{} = project, time}` - 23 | the Recode run has finished. 24 | 25 | The full Recode configuration is passed as the argument to GenServer.init/1 26 | callback when the formatters are started. 27 | 28 | All `time` variables are integers and representing microseconds. 29 | """ 30 | 31 | @doc """ 32 | Formats the given `microseconds` to a string representing the time in seconds. 33 | 34 | ## Examples 35 | 36 | iex> Recode.Formatter.format_time(1234) 37 | "0.00" 38 | iex> Recode.Formatter.format_time(12345) 39 | "0.01" 40 | iex> Recode.Formatter.format_time(123456) 41 | "0.1" 42 | iex> Recode.Formatter.format_time(1234567) 43 | "1.2" 44 | iex> Recode.Formatter.format_time(12345678) 45 | "12.3" 46 | """ 47 | @spec format_time(integer) :: String.t() 48 | def format_time(microseconds), do: format_time(microseconds, :second) 49 | 50 | @doc """ 51 | Formats the given `microseconds` to a string representing the time in the 52 | given `time_unit`. 53 | 54 | ## Examples 55 | 56 | iex> Recode.Formatter.format_time(1234, :second) 57 | "0.00" 58 | iex> Recode.Formatter.format_time(12345, :second) 59 | "0.01" 60 | iex> Recode.Formatter.format_time(123456, :second) 61 | "0.1" 62 | iex> Recode.Formatter.format_time(1234567, :second) 63 | "1.2" 64 | iex> Recode.Formatter.format_time(12345678, :second) 65 | "12.3" 66 | 67 | iex> Recode.Formatter.format_time(1234, :millisecond) 68 | "1.234" 69 | iex> Recode.Formatter.format_time(12345, :millisecond) 70 | "12.345" 71 | """ 72 | @spec format_time(integer, time_unit :: :second | :millisecond) :: String.t() 73 | def format_time(microseconds, :second) do 74 | time = div(microseconds, 10_000) 75 | 76 | if time < 10 do 77 | "0.0#{time}" 78 | else 79 | time = div(time, 10) 80 | "#{div(time, 10)}.#{rem(time, 10)}" 81 | end 82 | end 83 | 84 | def format_time(microseconds, :millisecond) do 85 | "#{div(microseconds, 1_000)}.#{rem(microseconds, 1_000)}" 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/recode/formatter_plugin.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.FormatterPlugin.Config do 2 | @moduledoc false 3 | end 4 | 5 | defmodule Recode.FormatterPlugin do 6 | @moduledoc """ 7 | Defines Recode formatter plugin for `mix format`. 8 | 9 | Since Elixir 1.13, it is possible to define custom formatter plugins. This 10 | plugin allows you to run Recode autocorrecting tasks together when executing 11 | `mix format`. 12 | 13 | To use this formatter, simply add `Recode.FormatterPlugin` to your 14 | `.formatter.exs` plugins: 15 | 16 | ``` 17 | [ 18 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 19 | plugins: [Recode.FormatterPlugin] 20 | ] 21 | ``` 22 | 23 | By default it uses the `.recode.exs` configuration file. 24 | 25 | If your project does not have a `.recode.exs` configuration file or if you 26 | want to overwrite the configuration, you can pass the configuration using the 27 | `recode` option: 28 | 29 | ``` 30 | [ 31 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 32 | plugins: [Recode.FormatterPlugin], 33 | recode: [ 34 | tasks: [ 35 | {Recode.Task.AliasExpansion, []}, 36 | {Recode.Task.EnforceLineLength, []}, 37 | {Recode.Task.SinglePipe, []} 38 | ] 39 | ] 40 | ] 41 | ``` 42 | """ 43 | 44 | @behaviour Mix.Tasks.Format 45 | 46 | alias Recode.Config 47 | alias Recode.Runner 48 | 49 | @persistent_term_key {__MODULE__, :config} 50 | 51 | @impl true 52 | def features(_opts) do 53 | [extensions: [".ex", ".exs"]] 54 | end 55 | 56 | @impl true 57 | def format(content, formatter_opts) do 58 | file = formatter_opts |> Keyword.get(:file, "source.ex") |> Path.relative_to_cwd() 59 | 60 | formatter_opts = 61 | Keyword.update(formatter_opts, :plugins, [], fn plugins -> 62 | Enum.reject(plugins, fn plugin -> plugin == Recode.FormatterPlugin end) 63 | end) 64 | 65 | config = 66 | formatter_opts 67 | |> config() 68 | |> Keyword.put(:dot_formatter_opts, Keyword.delete(formatter_opts, :recode)) 69 | 70 | Runner.run(content, config, file) 71 | end 72 | 73 | defp config(opts) do 74 | with nil <- :persistent_term.get(@persistent_term_key, nil) do 75 | config = init_config(opts[:recode]) 76 | :ok = :persistent_term.put(@persistent_term_key, config) 77 | config 78 | end 79 | end 80 | 81 | @config_error """ 82 | No configuration for `Recode.FormatterPlugin` found. Run \ 83 | `mix recode.gen.config` to create a config file or add config in \ 84 | `.formatter.exs` under the key `:recode`. 85 | """ 86 | defp init_config(nil) do 87 | case Config.read() do 88 | {:error, :not_found} -> Mix.raise(@config_error) 89 | {:ok, config} -> init_config(config) 90 | end 91 | end 92 | 93 | defp init_config(recode) do 94 | recode 95 | |> Keyword.merge( 96 | dry: false, 97 | verbose: false, 98 | autocorrect: true, 99 | check: false 100 | ) 101 | |> validate_config!() 102 | end 103 | 104 | defp validate_config!(config) do 105 | case Config.validate(config) do 106 | :ok -> 107 | config 108 | 109 | {:error, :out_of_date} -> 110 | Mix.raise("The config is out of date. Run `mix recode.update.config` to update.") 111 | 112 | {:error, :no_tasks} -> 113 | Mix.raise("No `:tasks` key found in configuration.") 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/recode/issue.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Issue do 2 | @moduledoc """ 3 | An `Issue` struct to track findings by the chechers. 4 | """ 5 | 6 | alias Recode.Issue 7 | 8 | defstruct [:reporter, :message, :line, :column, :meta] 9 | 10 | @type t :: %Issue{ 11 | reporter: module(), 12 | message: String.t() | nil, 13 | line: non_neg_integer() | nil, 14 | column: non_neg_integer() | nil, 15 | meta: term() 16 | } 17 | 18 | @doc """ 19 | Creates a new `%Issue{}` 20 | 21 | ## Examples 22 | 23 | iex> Recode.Issue.new(Test, "kaput", line: 1, column: 1) 24 | %Recode.Issue{reporter: Test, message: "kaput", line: 1, column: 1, meta: nil} 25 | 26 | iex> Recode.Issue.new(Test, foo: "bar") 27 | %Recode.Issue{reporter: Test, message: nil, line: nil, column: nil, meta: [foo: "bar"]} 28 | """ 29 | @spec new(module(), String.t() | term() | nil, keyword(), term()) :: Issue.t() 30 | def new(reporter, message, info \\ [], meta \\ nil) 31 | 32 | def new(reporter, message, info, meta) when is_binary(message) do 33 | line = Keyword.get(info, :line) 34 | column = Keyword.get(info, :column) 35 | struct!(Issue, reporter: reporter, message: message, line: line, column: column, meta: meta) 36 | end 37 | 38 | def new(reporter, meta, info, nil) do 39 | line = Keyword.get(info, :line) 40 | column = Keyword.get(info, :column) 41 | message = Keyword.get(meta, :message) 42 | 43 | struct!(Issue, reporter: reporter, line: line, column: column, meta: meta, message: message) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/recode/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Runner do 2 | @moduledoc false 3 | 4 | alias Recode.Runner 5 | 6 | @type config :: keyword() 7 | @type opts :: keyword() 8 | @type task :: {module(), opts()} 9 | 10 | @callback run(config) :: {:ok, integer()} | {:error, :no_source} 11 | @callback run(String.t(), config) :: String.t() 12 | @callback run(String.t(), config, Path.t()) :: String.t() 13 | 14 | def run(config) when is_list(config) do 15 | impl().run(config) 16 | end 17 | 18 | def run(content, config, path \\ "source.ex") when is_list(config) do 19 | impl().run(content, config, path) 20 | end 21 | 22 | defp impl, do: Application.get_env(:recode, :runner, Runner.Impl) 23 | end 24 | -------------------------------------------------------------------------------- /lib/recode/task.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task do 2 | @moduledoc """ 3 | The behaviour for a `recode` task. 4 | """ 5 | 6 | alias Rewrite.Source 7 | 8 | @type config :: keyword() 9 | @type message :: String.t() 10 | @type task :: module() 11 | @type category :: atom() 12 | 13 | @doc """ 14 | Applies a task with the given `source` and `opts`. 15 | """ 16 | @callback run(source :: Source.t(), opts :: Keyword.t()) :: Source.t() 17 | 18 | @doc """ 19 | Sets a callback to check and manipulate `config` before any recode task runs. 20 | 21 | When `init` returns an error tuple, the `mix recode` task raises an exception 22 | with the returned `message`. 23 | """ 24 | @callback init(config()) :: {:ok, config} | {:error, message()} 25 | 26 | # a callback for mox 27 | @doc false 28 | @callback __attributes__ :: any 29 | 30 | @optional_callbacks init: 1 31 | 32 | @doc """ 33 | Returns `true` if the given `task` provides a check for sources. 34 | """ 35 | @spec checker?(task()) :: boolean() 36 | def checker?(task) when is_atom(task), do: attribute(task, :checker) 37 | 38 | @doc """ 39 | Returns `true` if the given `task` provides a correction functionality for 40 | sources. 41 | """ 42 | @spec corrector?(task()) :: boolean() 43 | def corrector?(task) when is_atom(task), do: attribute(task, :corrector) 44 | 45 | @doc """ 46 | Returns the category for the given `task`. 47 | """ 48 | @spec category(task()) :: category() 49 | def category(task) when is_atom(task), do: attribute(task, :category) 50 | 51 | @doc """ 52 | Returns the shortdoc for the given `task`. 53 | 54 | Returns `nil` if `@shortdoc` is not available for the `task`. 55 | """ 56 | @spec shortdoc(task()) :: String.t() | nil 57 | def shortdoc(task) when is_atom(task), do: attribute(task, :shortdoc) 58 | 59 | defp attribute(task, key) when key in [:corrector, :checker, :category] do 60 | task.__attributes__() 61 | |> Keyword.fetch!(:__recode_task_config__) 62 | |> Keyword.fetch!(key) 63 | end 64 | 65 | defp attribute(task, key) do 66 | task.__attributes__() 67 | |> Keyword.get(key, []) 68 | |> unwrap() 69 | end 70 | 71 | defp unwrap([]), do: nil 72 | 73 | defp unwrap([value]), do: value 74 | 75 | defmacro __using__(opts) do 76 | config = 77 | Keyword.validate!(opts, 78 | checker: true, 79 | corrector: false, 80 | category: nil 81 | ) 82 | 83 | quote do 84 | @behaviour Recode.Task 85 | 86 | @__recode_task_config__ unquote(config) 87 | 88 | Module.register_attribute(__MODULE__, :__recode_task_config__, persist: true) 89 | Module.register_attribute(__MODULE__, :shortdoc, persist: true) 90 | 91 | @impl Recode.Task 92 | def init(config), do: {:ok, config} 93 | 94 | @impl Recode.Task 95 | def __attributes__, do: __MODULE__.__info__(:attributes) 96 | 97 | defoverridable init: 1 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/recode/task/alias_expansion.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.AliasExpansion do 2 | @shortdoc "Exapnds multi aliases to separate aliases." 3 | 4 | @moduledoc """ 5 | Multi aliases makes module uses harder to search for in large code bases. 6 | 7 | # preferred 8 | alias Module.Foo 9 | alias Module.Bar 10 | 11 | # not preferred 12 | alias Module.{Foo, Bar} 13 | 14 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 15 | """ 16 | 17 | use Recode.Task, corrector: true, category: :readability 18 | 19 | alias Recode.Issue 20 | alias Recode.Task.AliasExpansion 21 | alias Rewrite.Source 22 | alias Sourceror.Zipper 23 | 24 | @impl Recode.Task 25 | def run(source, opts) do 26 | {zipper, issues} = 27 | source 28 | |> Source.get(:quoted) 29 | |> Zipper.zip() 30 | |> Zipper.traverse([], fn zipper, issues -> 31 | expand_alias(zipper, issues, opts[:autocorrect]) 32 | end) 33 | 34 | case opts[:autocorrect] do 35 | true -> 36 | Source.update(source, AliasExpansion, :quoted, Zipper.root(zipper)) 37 | 38 | false -> 39 | Source.add_issues(source, issues) 40 | end 41 | end 42 | 43 | defp expand_alias(%Zipper{node: {:alias, meta, _args} = ast} = zipper, issues, true) do 44 | newlines = newlines(meta) 45 | 46 | zipper = 47 | case extract(ast) do 48 | {:ok, {base, segments, alias_meta, call_meta}} -> 49 | segments 50 | |> segments_to_alias(base) 51 | |> put_leading_comments(alias_meta) 52 | |> put_trailing_comments(call_meta, newlines) 53 | |> insert(zipper) 54 | 55 | :error -> 56 | zipper 57 | end 58 | 59 | {zipper, issues} 60 | end 61 | 62 | defp expand_alias(%Zipper{node: {:alias, meta, _args} = ast} = zipper, issues, false) do 63 | issues = 64 | case extract(ast) do 65 | {:ok, _data} -> 66 | message = "Avoid multi aliases." 67 | issue = Issue.new(AliasExpansion, message, meta) 68 | [issue | issues] 69 | 70 | :error -> 71 | issues 72 | end 73 | 74 | {zipper, issues} 75 | end 76 | 77 | defp expand_alias(zipper, issues, _autocorrect), do: {zipper, issues} 78 | 79 | defp newlines(meta), do: get_in(meta, [:end_of_expression, :newlines]) || 1 80 | 81 | defp extract(tree) do 82 | case tree do 83 | {:alias, alias_meta, [{{:., _meta, [base, :{}]}, call_meta, segments}]} -> 84 | {:ok, {base(base), segments, alias_meta, call_meta}} 85 | 86 | _tree -> 87 | :error 88 | end 89 | end 90 | 91 | defp base({:__MODULE__, meta, nil}), do: {:__MODULE__, meta, [:__MODULE__]} 92 | 93 | defp base({:__MODULE__, meta, args}), do: {:__MODULE__, meta, [:__MODULE__ | args]} 94 | 95 | defp base(base), do: base 96 | 97 | defp insert(aliases, zipper) do 98 | aliases 99 | |> Enum.reduce(zipper, fn alias, zip -> Zipper.insert_left(zip, alias) end) 100 | |> Zipper.remove() 101 | end 102 | 103 | defp segments_to_alias(segments, {_name, _meta, base_segments}) when is_list(segments) do 104 | Enum.map(segments, fn {_name, meta, segments} -> 105 | {:alias, meta, [{:__aliases__, [], base_segments ++ segments}]} 106 | end) 107 | end 108 | 109 | defp put_leading_comments([first | rest], meta) do 110 | comments = meta[:leading_comments] || [] 111 | [Sourceror.prepend_comments(first, comments) | rest] 112 | end 113 | 114 | defp put_trailing_comments(list, meta, newlines) do 115 | comments = meta[:trailing_comments] || [] 116 | 117 | case List.pop_at(list, -1) do 118 | {nil, list} -> 119 | list 120 | 121 | {last, list} -> 122 | last = 123 | {:__block__, 124 | [ 125 | trailing_comments: comments, 126 | end_of_expression: [newlines: newlines] 127 | ], [last]} 128 | 129 | list ++ [last] 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/recode/task/alias_order.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.AliasOrder do 2 | @shortdoc "Checks if aliases are sorted alphabetically." 3 | 4 | @moduledoc """ 5 | Alphabetically sorted lists are easier to read. 6 | 7 | # preferred 8 | 9 | alias Alpha 10 | alias Bravo 11 | alias Delta.{Echo, Foxtrot} 12 | 13 | # not preferred 14 | 15 | alias Delta.{Foxtrot, Echo} 16 | alias Alpha 17 | alias Bravo 18 | """ 19 | 20 | use Recode.Task, corrector: true, category: :readability 21 | 22 | alias Recode.AST 23 | alias Recode.Issue 24 | alias Recode.Task.AliasOrder 25 | alias Rewrite.Source 26 | alias Sourceror.Zipper 27 | 28 | @impl Recode.Task 29 | def run(source, opts) do 30 | source 31 | |> Source.get(:quoted) 32 | |> Zipper.zip() 33 | |> do_run(source, opts[:autocorrect]) 34 | end 35 | 36 | defp do_run(zipper, source, false) do 37 | {_zipper, groups} = 38 | Zipper.traverse_while(zipper, [[]], fn zipper, acc -> 39 | alias_groups(zipper, acc) 40 | end) 41 | 42 | Source.add_issues(source, issues(groups)) 43 | end 44 | 45 | defp do_run(zipper, source, true) do 46 | {zipper, []} = 47 | Zipper.traverse_while(zipper, [], fn zipper, acc -> 48 | alias_order(zipper, acc) 49 | end) 50 | 51 | Source.update(source, AliasOrder, :quoted, Zipper.root(zipper)) 52 | end 53 | 54 | defp alias_groups(%Zipper{node: {:alias, _meta, _args} = ast} = zipper, [group | groups]) do 55 | group = [ast | group] 56 | 57 | if AST.get_newlines(ast) > 1 || !Zipper.skip(zipper) do 58 | {:skip, zipper, [[], Enum.reverse(group) | groups]} 59 | else 60 | {:skip, zipper, [group | groups]} 61 | end 62 | end 63 | 64 | defp alias_groups(zipper, [[] | _groups] = acc) do 65 | {:cont, zipper, acc} 66 | end 67 | 68 | defp alias_groups(zipper, [group | groups]) do 69 | {:cont, zipper, [[], Enum.reverse(group) | groups]} 70 | end 71 | 72 | defp issues([[] | groups]) do 73 | Enum.concat( 74 | Enum.flat_map(groups, &issues_in_group/1), 75 | Enum.flat_map(groups, &issues_in_multi/1) 76 | ) 77 | end 78 | 79 | defp issues_in_group(group) do 80 | group 81 | |> unordered() 82 | |> Enum.map(&issue/1) 83 | end 84 | 85 | defp issues_in_multi(group) do 86 | Enum.reduce(group, [], fn 87 | {:alias, _meta1, [{{:., _meta2, _args2}, _meta3, multi}]}, acc -> 88 | multi 89 | |> unordered() 90 | |> Enum.map(&issue/1) 91 | |> Enum.concat(acc) 92 | 93 | _ast, acc -> 94 | acc 95 | end) 96 | end 97 | 98 | defp issue({:__aliases__, meta, args}) do 99 | Issue.new( 100 | AliasOrder, 101 | "The alias `#{AST.name(args)}` is not alphabetically ordered among its multi group", 102 | meta 103 | ) 104 | end 105 | 106 | defp issue({:alias, meta, _args} = ast) do 107 | {name, _multi, _as} = AST.alias_info(ast) 108 | 109 | Issue.new( 110 | AliasOrder, 111 | "The alias `#{AST.name(name)}` is not alphabetically ordered among its group", 112 | meta 113 | ) 114 | end 115 | 116 | defp unordered(group) do 117 | group = 118 | Enum.reject(group, fn 119 | {:alias, _meta1, [{:unquote, _meta2, _args} | _rest]} -> true 120 | _alias -> false 121 | end) 122 | 123 | sorted = Enum.sort(group, &sort/2) 124 | 125 | sorted 126 | |> List.myers_difference(group) 127 | |> Enum.reduce([], fn 128 | {:del, alias}, acc -> Enum.concat(alias, acc) 129 | _eq_ins, acc -> acc 130 | end) 131 | end 132 | 133 | defp alias_order(%Zipper{node: {:alias, _meta, _args} = ast} = zipper, acc) do 134 | acc = [ast | acc] 135 | 136 | if AST.get_newlines(ast) > 1 || !Zipper.skip(zipper) do 137 | zipper = update(zipper, acc) 138 | {:skip, zipper, []} 139 | else 140 | {:skip, zipper, acc} 141 | end 142 | end 143 | 144 | defp alias_order(zipper, []) do 145 | {:cont, zipper, []} 146 | end 147 | 148 | defp alias_order(zipper, acc) do 149 | zipper = update(zipper, acc) 150 | 151 | {:cont, zipper, []} 152 | end 153 | 154 | defp update(zipper, acc) do 155 | acc = Enum.reverse(acc) 156 | sorted = acc |> Enum.map(&sort_multi/1) |> Enum.sort(&sort/2) 157 | 158 | case acc == sorted do 159 | true -> 160 | zipper 161 | 162 | false -> 163 | zipper 164 | |> rewind(hd(acc)) 165 | |> do_update(sorted) 166 | end 167 | end 168 | 169 | defp do_update(zipper, [ast]) do 170 | do_update(zipper, ast, 2) 171 | end 172 | 173 | defp do_update(zipper, [ast | sorted]) do 174 | zipper 175 | |> do_update(ast, 1) 176 | |> do_update(sorted) 177 | end 178 | 179 | defp do_update(zipper, ast, newlines) do 180 | zipper 181 | |> Zipper.update(fn _ast -> AST.put_newlines(ast, newlines) end) 182 | |> skip() 183 | end 184 | 185 | defp sort({:alias, _meta1, _args1} = alias1, {:alias, _meta2, _args2} = alias2) do 186 | {module1, multi1, _as} = AST.alias_info(alias1) 187 | {module2, multi2, _as} = AST.alias_info(alias2) 188 | 189 | case module1 == module2 do 190 | true -> length(multi1) < length(multi2) 191 | false -> lower_than?(module1, module2) 192 | end 193 | end 194 | 195 | defp sort({:__aliases__, _meta1, args1}, {:__aliases__, _meta2, args2}) do 196 | lower_than?(args1, args2) 197 | end 198 | 199 | defp lower_than?([value1], [value2]), do: lower_than?(value1, value2) 200 | 201 | defp lower_than?([_value], []), do: false 202 | 203 | defp lower_than?([], [_value]), do: true 204 | 205 | defp lower_than?([value | rest1], [value | rest2]), do: lower_than?(rest1, rest2) 206 | 207 | defp lower_than?([value1 | _], [value2 | _]), do: lower_than?(value1, value2) 208 | 209 | defp lower_than?(value1, value2) when is_atom(value1) and is_atom(value2) do 210 | String.upcase(to_string(value1)) < String.upcase(to_string(value2)) 211 | end 212 | 213 | defp sort_multi({:alias, meta1, [{{:., meta2, [aliases, opts]}, meta3, multi}]}) do 214 | multi = 215 | Enum.sort(multi, fn multi1, multi2 -> 216 | AST.aliases_concat(multi1) < AST.aliases_concat(multi2) 217 | end) 218 | 219 | {:alias, meta1, [{{:., meta2, [aliases, opts]}, meta3, multi}]} 220 | end 221 | 222 | defp sort_multi(ast), do: ast 223 | 224 | defp rewind(zipper, ast) do 225 | Zipper.find(zipper, :prev, fn item -> item == ast end) 226 | end 227 | 228 | defp skip(zipper) do 229 | with nil <- Zipper.right(zipper) do 230 | Zipper.next(zipper) 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /lib/recode/task/dbg.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.Dbg do 2 | @shortdoc "There should be no calls to dbg." 3 | 4 | @moduledoc """ 5 | Calls to `dbg/2` should only appear in debug sessions. 6 | 7 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 8 | """ 9 | 10 | use Recode.Task, corrector: true, category: :warning 11 | 12 | alias Recode.Issue 13 | alias Recode.Task.Dbg 14 | alias Rewrite.Source 15 | alias Sourceror.Zipper 16 | 17 | @impl Recode.Task 18 | def run(source, opts) do 19 | source 20 | |> Source.get(:quoted) 21 | |> Zipper.zip() 22 | |> Zipper.traverse([], fn zipper, issues -> 23 | traverse(zipper, issues, opts[:autocorrect]) 24 | end) 25 | |> update(source, opts[:autocorrect]) 26 | end 27 | 28 | defp update({zipper, _issues}, source, true) do 29 | Source.update(source, Dbg, :quoted, Zipper.root(zipper)) 30 | end 31 | 32 | defp update({_zipper, []}, source, false), do: source 33 | 34 | defp update({_zipper, issues}, source, false) do 35 | Source.add_issues(source, issues) 36 | end 37 | 38 | defp traverse( 39 | %Zipper{ 40 | node: {:|>, _, [arg, {{:., _, [{:__aliases__, _, [:Kernel]}, :dbg]}, _, _}]} 41 | } = 42 | zipper, 43 | issues, 44 | true 45 | ) do 46 | {Zipper.replace(zipper, arg), issues} 47 | end 48 | 49 | defp traverse( 50 | %Zipper{node: {:|>, _, [arg, {:dbg, _, _}]}} = zipper, 51 | issues, 52 | true 53 | ) do 54 | {Zipper.replace(zipper, arg), issues} 55 | end 56 | 57 | defp traverse( 58 | %Zipper{ 59 | node: {{:., _, [{:__aliases__, meta, [:Kernel]}, :dbg]}, _, _} 60 | } = zipper, 61 | issues, 62 | autocorrect 63 | ) do 64 | handle(zipper, issues, meta, autocorrect) 65 | end 66 | 67 | defp traverse(%Zipper{node: {:dbg, meta, args}} = zipper, issues, autocorrect) 68 | when is_list(args) do 69 | handle(zipper, issues, meta, autocorrect) 70 | end 71 | 72 | defp traverse( 73 | %Zipper{node: {:&, meta, [{:/, _, [{:dbg, _, _}, _]}]}} = zipper, 74 | issues, 75 | autocorrect 76 | ) do 77 | handle_up(zipper, issues, meta, autocorrect) 78 | end 79 | 80 | defp traverse(zipper, issues, _autocorrect) do 81 | {zipper, issues} 82 | end 83 | 84 | defp handle(zipper, issues, _meta, true) do 85 | {Zipper.remove(zipper), issues} 86 | end 87 | 88 | defp handle(zipper, issues, meta, false) do 89 | issue = Issue.new(Dbg, @shortdoc, meta) 90 | {zipper, [issue | issues]} 91 | end 92 | 93 | defp handle_up(zipper, issues, meta, true) do 94 | up = Zipper.up(zipper) 95 | upup = Zipper.up(up) 96 | 97 | case upup do 98 | %Zipper{node: {:|>, _, [arg, _]}} -> {Zipper.replace(upup, arg), issues} 99 | _else -> handle(up, issues, meta, true) 100 | end 101 | end 102 | 103 | defp handle_up(zipper, issues, meta, false) do 104 | handle(zipper, issues, meta, false) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/recode/task/enforce_line_length.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.EnforceLineLength do 2 | @shortdoc "Forces expressions to one line." 3 | 4 | @moduledoc """ 5 | The `EnforceLineLength` task writes multiline expressions into one line if 6 | they do not exceed the maximum line length. 7 | 8 | ## Options 9 | 10 | * `:skip` - specifies expressions to skip. 11 | * `:ignore` - specifies expressions to ignore. 12 | 13 | ## Examples 14 | 15 | The following code is not changed by the Elixir Formatter. 16 | ```Elixir 17 | fn 18 | x -> 19 | { 20 | :ok, 21 | x 22 | } 23 | end 24 | ``` 25 | The `EnforceLineLength` task rewrite this to 26 | ```Elixir 27 | fn x -> {:ok, x} end 28 | ``` 29 | and with the option `[ignore: :fn]` to 30 | ```Elixir 31 | fn 32 | x -> {:ok, x} 33 | end 34 | ``` 35 | and with the option `skip: :fn` the code keeps unchanged. 36 | """ 37 | 38 | use Recode.Task, corrector: true, category: :readability 39 | 40 | alias Recode.AST 41 | alias Recode.Task.EnforceLineLength 42 | alias Rewrite.Source 43 | alias Sourceror.Zipper 44 | 45 | @impl Recode.Task 46 | def run(source, opts) do 47 | opts = validate(opts) 48 | 49 | zipper = 50 | source 51 | |> Source.get(:quoted) 52 | |> Zipper.zip() 53 | |> Zipper.traverse_while(fn zipper -> same_line(zipper, opts) end) 54 | 55 | Source.update(source, EnforceLineLength, :quoted, Zipper.root(zipper)) 56 | end 57 | 58 | defp same_line(%Zipper{node: {:with, _meta, _args}} = zipper, _opts) do 59 | {:cont, zipper} 60 | end 61 | 62 | defp same_line(%Zipper{node: {name, _meta, args}} = zipper, opts) when is_list(args) do 63 | cond do 64 | name in opts[:skip] -> {:skip, zipper} 65 | name in opts[:ignore] -> {:cont, zipper} 66 | true -> do_same_line(zipper) 67 | end 68 | end 69 | 70 | defp same_line(zipper, _opts), do: {:cont, zipper} 71 | 72 | defp do_same_line(zipper) do 73 | case zipper |> Zipper.node() |> AST.multiline?() do 74 | true -> {:cont, Zipper.update(zipper, &AST.to_same_line/1)} 75 | false -> {:cont, zipper} 76 | end 77 | end 78 | 79 | defp validate(opts) do 80 | opts 81 | |> Keyword.update(:skip, [], fn skip -> List.wrap(skip) end) 82 | |> Keyword.update(:ignore, [], fn ignore -> List.wrap(ignore) end) 83 | |> Keyword.validate!([:skip, :ignore, :autocorrect]) 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/recode/task/filter_count.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.FilterCount do 2 | @shortdoc "Checks calls like Enum.filter(...) |> Enum.count()." 3 | 4 | @moduledoc """ 5 | `Enum.count/2` is more efficient than `Enum.filter/2 |> Enum.count/1`. 6 | 7 | 8 | # this should be refactored 9 | [1, 2, 3, 4, 5] 10 | |> Enum.filter(fn x -> rem(x, 3) == 0 end) 11 | |> Enum.count() 12 | 13 | # to look like this 14 | Enum.count([1, 2, 3, 4, 5], fn x -> rem(x, 3) == 0 end) 15 | 16 | The reason for this is performance, because the two separate calls to 17 | `Enum.filter/2` and `Enum.count/1` require two iterations whereas 18 | `Enum.count/2` performs the same work in one pass. 19 | 20 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 21 | """ 22 | 23 | use Recode.Task, corrector: true, category: :refactor 24 | 25 | alias Recode.Issue 26 | alias Recode.Task.FilterCount 27 | alias Rewrite.Source 28 | alias Sourceror.Zipper 29 | 30 | @impl Recode.Task 31 | def run(source, opts) do 32 | source 33 | |> Source.get(:quoted) 34 | |> Zipper.zip() 35 | |> Zipper.traverse([], fn zipper, issues -> 36 | filter_count(zipper, issues, opts[:autocorrect]) 37 | end) 38 | |> update(source, opts[:autocorrect]) 39 | end 40 | 41 | defp update({zipper, _issues}, source, true) do 42 | Source.update(source, FilterCount, :quoted, Zipper.root(zipper)) 43 | end 44 | 45 | defp update({_zipper, issues}, source, false) do 46 | Source.add_issues(source, issues) 47 | end 48 | 49 | defp filter_count( 50 | %Zipper{ 51 | node: 52 | {:|>, _, 53 | [ 54 | {{:., _, [{:__aliases__, _, [:Enum]}, :filter]}, _, [arg, fun]}, 55 | {{:., meta, [{:__aliases__, _, [:Enum]}, :count]}, _, []} 56 | ]} 57 | } = zipper, 58 | issues, 59 | autocorrect 60 | ) do 61 | if autocorrect do 62 | {Zipper.replace(zipper, quoted_count(fun, arg)), issues} 63 | else 64 | {zipper, [issue(meta) | issues]} 65 | end 66 | end 67 | 68 | defp filter_count( 69 | %Zipper{ 70 | node: 71 | {:|>, meta, 72 | [ 73 | {:|>, _, 74 | [ 75 | {expr, _, _} = arg, 76 | {{:., issue_meta, [{:__aliases__, _, [:Enum]}, :filter]}, _, [fun]} 77 | ]}, 78 | {{:., _, [{:__aliases__, _, [:Enum]}, :count]}, _, []} 79 | ]} 80 | } = zipper, 81 | issues, 82 | autocorrect 83 | ) do 84 | if autocorrect do 85 | arg = purne_meta(arg) 86 | fun = purne_meta(fun) 87 | 88 | quoted = 89 | case expr do 90 | :|> -> 91 | quoted_count_pipe(fun, meta, arg) 92 | 93 | _else -> 94 | zipper 95 | |> in_pipe?() 96 | |> quoted_count(fun, meta, arg) 97 | end 98 | 99 | {Zipper.replace(zipper, quoted), issues} 100 | else 101 | {zipper, [issue(issue_meta) | issues]} 102 | end 103 | end 104 | 105 | defp filter_count( 106 | %Zipper{ 107 | node: 108 | {{:., _, [{:__aliases__, _, [:Enum]}, :count]}, _, 109 | [ 110 | {:|>, meta, 111 | [{expr, _, _} = arg, {{:., _, [{:__aliases__, _, [:Enum]}, :filter]}, _, [fun]}]} 112 | ]} 113 | } = zipper, 114 | issues, 115 | autocorrect 116 | ) do 117 | if autocorrect do 118 | quoted = 119 | case expr do 120 | :|> -> quoted_count_pipe(fun, meta, arg) 121 | _else -> quoted_count(fun, arg) 122 | end 123 | 124 | {Zipper.replace(zipper, quoted), issues} 125 | else 126 | {zipper, [issue(meta) | issues]} 127 | end 128 | end 129 | 130 | defp filter_count( 131 | %Zipper{ 132 | node: 133 | {{:., meta, [{:__aliases__, _, [:Enum]}, :count]}, _, 134 | [ 135 | {{:., _, [{:__aliases__, _, [:Enum]}, :filter]}, _, [arg, fun]} 136 | ]} 137 | } = zipper, 138 | issues, 139 | autocorrect 140 | ) do 141 | if autocorrect do 142 | {Zipper.replace(zipper, quoted_count(fun, arg)), issues} 143 | else 144 | {zipper, [issue(meta) | issues]} 145 | end 146 | end 147 | 148 | defp filter_count(zipper, issues, _autocorrect), do: {zipper, issues} 149 | 150 | defp quoted_count(true = _in_pipe?, fun, meta, arg), do: quoted_count_pipe(fun, meta, arg) 151 | defp quoted_count(false = _in_pipe?, fun, _meta, arg), do: quoted_count(fun, arg) 152 | 153 | defp quoted_count(fun, arg) do 154 | {{:., [], [{:__aliases__, [], [:Enum]}, :count]}, [], [arg, fun]} 155 | end 156 | 157 | defp quoted_count_pipe(fun, meta, arg) do 158 | {:|>, meta, [arg, {{:., [], [{:__aliases__, [], [:Enum]}, :count]}, [], [fun]}]} 159 | end 160 | 161 | defp purne_meta({expr, _meta, args}), do: {expr, [], args} 162 | 163 | defp in_pipe?(zipper) do 164 | match?({:|>, _, _}, zipper |> Zipper.up() |> Zipper.node()) 165 | end 166 | 167 | defp issue(meta) do 168 | Issue.new( 169 | FilterCount, 170 | "`Enum.count/2` is more efficient than `Enum.filter/2 |> Enum.count/1`", 171 | meta 172 | ) 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/recode/task/format.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.Format do 2 | @shortdoc "Does the same as `mix format`." 3 | 4 | @moduledoc """ 5 | This task runs the Elixir formatter. 6 | 7 | This task runs as first task by any `mix recode` call. 8 | """ 9 | 10 | # The task can be configured like other tasks. This configuration is just for 11 | # debuging and not part of the documentation. 12 | # 13 | # The default, formats code with Sourceror: 14 | # {Recode.Task.Format, []}, 15 | # 16 | # formats code with Sourceror: 17 | # {Recode.Task.Format, config: [formatter: :sourceror]}, 18 | # 19 | # Formats code with the Elixir formatter: 20 | # {Recode.Task.Format, config: [formatter: :elixir]}, 21 | # 22 | # Deactivates the task: 23 | # {Recode.Task.Format, active: false}, 24 | 25 | use Recode.Task, corrector: true, category: :readability 26 | 27 | alias Recode.Issue 28 | alias Recode.Task.Format 29 | alias Rewrite.Source 30 | 31 | @default_config [formatter: :sourceror] 32 | @valid_formatters [:elixir, :sourceror] 33 | 34 | @impl Recode.Task 35 | def run(source, opts) do 36 | opts = Keyword.merge(opts, @default_config) 37 | 38 | source 39 | |> Source.Ex.merge_formatter_opts(exclude_plugins: [Recode.FormatterPlugin]) 40 | |> execute(opts[:autocorrect], opts[:formatter]) 41 | end 42 | 43 | defp execute(source, true, formatter) do 44 | Source.update(source, Format, :content, format(source, formatter)) 45 | end 46 | 47 | defp execute(source, false, formatter) do 48 | case Source.get(source, :content) == format(source, formatter) do 49 | true -> 50 | source 51 | 52 | false -> 53 | Source.add_issue(source, Issue.new(Format, "The file is not formatted.")) 54 | end 55 | end 56 | 57 | defp format(source, :sourceror) do 58 | Source.Ex.format(source) 59 | end 60 | 61 | defp format(source, :elixir) do 62 | opts = Keyword.get(source.filetype.opts, :formatter_opts, []) 63 | 64 | source 65 | |> Source.get(:content) 66 | |> Code.format_string!(opts) 67 | |> IO.iodata_to_binary() 68 | end 69 | 70 | @impl Recode.Task 71 | def init([]), do: {:ok, @default_config} 72 | 73 | def init(config) do 74 | with {:error, reason} <- validate(config) do 75 | case reason do 76 | {:unknown, keys} -> 77 | {:error, 78 | """ 79 | Unknown options: #{inspect(keys)}.\ 80 | """} 81 | 82 | {:invalid_formatter, formatter} -> 83 | {:error, 84 | """ 85 | The option formatter expects :elixir or :sourceror, got: #{inspect(formatter)}.\ 86 | """} 87 | end 88 | end 89 | end 90 | 91 | defp validate(config) do 92 | with {:ok, config} <- validate_keys(config) do 93 | if config[:formatter] in @valid_formatters do 94 | {:ok, config} 95 | else 96 | {:error, {:invalid_formatter, config[:formatter]}} 97 | end 98 | end 99 | end 100 | 101 | defp validate_keys(config) do 102 | with {:error, keys} <- Keyword.validate(config, [:autocorrect] ++ @default_config) do 103 | {:error, {:unknown, keys}} 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/recode/task/io_inspect.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.IOInspect do 2 | @shortdoc "There should be no calls to IO.inspect." 3 | 4 | @moduledoc """ 5 | Calls to `IO.inspect/2` should only appear in debug sessions. 6 | 7 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 8 | """ 9 | 10 | use Recode.Task, corrector: true, category: :warning 11 | 12 | alias Recode.Issue 13 | alias Recode.Task.IOInspect 14 | alias Rewrite.Source 15 | alias Sourceror.Zipper 16 | 17 | @impl Recode.Task 18 | def run(source, opts) do 19 | source 20 | |> Source.get(:quoted) 21 | |> Zipper.zip() 22 | |> Zipper.traverse([], fn zipper, issues -> 23 | traverse(zipper, issues, opts[:autocorrect]) 24 | end) 25 | |> update(source, opts[:autocorrect]) 26 | end 27 | 28 | defp update({zipper, _issues}, source, true) do 29 | Source.update(source, IOInspect, :quoted, Zipper.root(zipper)) 30 | end 31 | 32 | defp update({_zipper, []}, source, false), do: source 33 | 34 | defp update({_zipper, issues}, source, false) do 35 | Source.add_issues(source, issues) 36 | end 37 | 38 | defp traverse( 39 | %Zipper{ 40 | node: {:|>, _, [arg, {{:., _, [{:__aliases__, _, [:IO]}, :inspect]}, _, _}]} 41 | } = zipper, 42 | issues, 43 | true 44 | ) do 45 | {Zipper.replace(zipper, arg), issues} 46 | end 47 | 48 | defp traverse( 49 | %Zipper{node: {{:., _, [{:__aliases__, meta, [:IO]}, :inspect]}, _, args}} = zipper, 50 | issues, 51 | autocorrect 52 | ) 53 | when is_list(args) do 54 | handle(zipper, issues, meta, autocorrect) 55 | end 56 | 57 | defp traverse( 58 | %Zipper{ 59 | node: {:&, meta, [{:/, _, [{{:., _, [{:__aliases__, _, [:IO]}, :inspect]}, _, _}, _]}]} 60 | } = zipper, 61 | issues, 62 | autocorrect 63 | ) do 64 | handle_up(zipper, issues, meta, autocorrect) 65 | end 66 | 67 | defp traverse(zipper, issues, _autocorrect) do 68 | {zipper, issues} 69 | end 70 | 71 | defp handle(zipper, issues, _meta, true) do 72 | {Zipper.remove(zipper), issues} 73 | end 74 | 75 | defp handle(zipper, issues, meta, false) do 76 | issue = Issue.new(IOInspect, @shortdoc, meta) 77 | {zipper, [issue | issues]} 78 | end 79 | 80 | defp handle_up(zipper, issues, meta, true) do 81 | up = Zipper.up(zipper) 82 | upup = Zipper.up(up) 83 | 84 | case upup do 85 | %Zipper{node: {:|>, _, [arg, _]}} -> {Zipper.replace(upup, arg), issues} 86 | _else -> handle(up, issues, meta, true) 87 | end 88 | end 89 | 90 | defp handle_up(zipper, issues, meta, false) do 91 | zipper |> Zipper.next() |> Zipper.next() |> handle(issues, meta, false) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/recode/task/locals_without_parens.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.LocalsWithoutParens do 2 | @shortdoc "Removes parens from locals without parens." 3 | 4 | @moduledoc """ 5 | Don't use parens for functions that don't need them. 6 | 7 | # preferred 8 | assert true == true 9 | 10 | # not preferred 11 | assert(true == true) 12 | 13 | The task uses the `:locals_without_parens` from the formatter config in `.formatter.exs`. 14 | See also: ["Importing-dependencies-configuration"](https://hexdocs.pm/mix/Mix.Tasks.Format.html#module-importing-dependencies-configuration) 15 | in the docs for [`mix format`](https://hexdocs.pm/mix/Mix.Tasks.Format.html#content). 16 | 17 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 18 | """ 19 | 20 | use Recode.Task, corrector: true, category: :readability 21 | 22 | alias Mix.Tasks.Format 23 | alias Recode.Issue 24 | alias Recode.Task.RemoveParens 25 | alias Rewrite.Source 26 | alias Sourceror.Zipper 27 | 28 | @impl Recode.Task 29 | def run(source, opts) do 30 | {_formatter, formatter_opts} = Format.formatter_for_file(source.path || "nofile") 31 | locals_without_parens = Keyword.get(formatter_opts, :locals_without_parens, []) 32 | 33 | {zipper, issues} = 34 | source 35 | |> Source.get(:quoted) 36 | |> Zipper.zip() 37 | |> Zipper.traverse([], fn zipper, issues -> 38 | remove_parens(locals_without_parens, zipper, issues, opts[:autocorrect]) 39 | end) 40 | 41 | case opts[:autocorrect] do 42 | true -> 43 | Source.update(source, RemoveParens, :quoted, Zipper.root(zipper)) 44 | 45 | false -> 46 | Source.add_issues(source, issues) 47 | end 48 | end 49 | 50 | defp remove_parens( 51 | locals_without_parens, 52 | %Zipper{node: {fun, meta, args}} = zipper, 53 | issues, 54 | autocorrect? 55 | ) do 56 | if local_without_parens?(locals_without_parens, fun, args) do 57 | if autocorrect? do 58 | node = {fun, Keyword.delete(meta, :closing), args} 59 | {Zipper.replace(zipper, node), issues} 60 | else 61 | issue = Issue.new(RemoveParens, "Unncecessary parens") 62 | {zipper, [issue | issues]} 63 | end 64 | else 65 | {zipper, issues} 66 | end 67 | end 68 | 69 | defp remove_parens(_, zipper, issues, _) do 70 | {zipper, issues} 71 | end 72 | 73 | defp local_without_parens?(locals_without_parens, fun, [_ | _] = args) do 74 | arity = length(args) 75 | 76 | Enum.any?(locals_without_parens, fn 77 | {^fun, :*} -> true 78 | {^fun, ^arity} -> true 79 | _other -> false 80 | end) 81 | end 82 | 83 | defp local_without_parens?(_locals_without_parens, _fun, _args), do: false 84 | end 85 | -------------------------------------------------------------------------------- /lib/recode/task/moduledoc.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.Moduledoc do 2 | @shortdoc "There should be a @moduledoc in any module." 3 | 4 | @moduledoc """ 5 | Any module should contain a `@moudledoc` attribute. 6 | 7 | For a public module, comprehensive documentation should be available. The 8 | module documentation helps the user of your package, contributors, and your 9 | future self understand what the module is for. 10 | 11 | For private modules, it is also okay to set `@moduled false`. Modules marked 12 | in this way are not displayed in the documentation. 13 | 14 | ## Options 15 | 16 | * `ignore_names` - accepts a regex or a list of regexes to recognize modules 17 | that this task ignores. 18 | """ 19 | 20 | use Recode.Task, corrector: false, category: :readability 21 | 22 | alias Recode.AST 23 | alias Recode.Issue 24 | alias Rewrite.Source 25 | 26 | @default_config [ignore_names: []] 27 | @error_message """ 28 | The config for the Recode.Task.Moduledoc is wrong. The task excepts the option \ 29 | :ignore_names with a regexp or a list of regexps. 30 | """ 31 | 32 | @impl Recode.Task 33 | def run(source, config) do 34 | ignore_names = Keyword.fetch!(config, :ignore_names) 35 | 36 | source 37 | |> Source.get(:quoted) 38 | |> check() 39 | |> update(source, ignore_names) 40 | end 41 | 42 | @impl Recode.Task 43 | def init([]), do: {:ok, @default_config} 44 | 45 | def init(config) do 46 | with {:ok, config} <- validate_keys(config) do 47 | config = 48 | Keyword.update!(config, :ignore_names, fn ignore_names -> List.wrap(ignore_names) end) 49 | 50 | if validate_config(config) do 51 | {:ok, config} 52 | else 53 | {:error, @error_message} 54 | end 55 | end 56 | end 57 | 58 | defp check(ast) do 59 | AST.reduce_while(ast, [], fn 60 | {:defmodule, meta, [aliases, args]}, acc -> 61 | module = {AST.module(aliases), meta} 62 | block = AST.block(args) 63 | acc = [check_module([module], block) | acc] 64 | {:skip, acc} 65 | 66 | _ast, acc -> 67 | {:cont, acc} 68 | end) 69 | end 70 | 71 | defp check_module(modules, ast) do 72 | AST.reduce_while(ast, modules, fn 73 | {:@, _, [{:moduledoc, _, args} | _]}, [module | modules] when not is_nil(module) -> 74 | {:cont, [check_moduledoc(args, module) | modules]} 75 | 76 | {:defmodule, meta, [aliases, args]}, acc -> 77 | module = {AST.module(aliases), meta} 78 | block = AST.block(args) 79 | acc = [acc | check_module([module], block)] 80 | {:skip, acc} 81 | 82 | _ast, acc -> 83 | {:skip, acc} 84 | end) 85 | end 86 | 87 | defp check_moduledoc([{:__block__, _meta, [text]}], {module, meta}) when is_binary(text) do 88 | {module, Keyword.merge(meta, exist: true, empty: String.trim(text) == "")} 89 | end 90 | 91 | defp check_moduledoc(_args, {module, meta}) do 92 | {module, Keyword.merge(meta, exist: true, empty: false)} 93 | end 94 | 95 | defp update([], source, _ignore_names), do: source 96 | 97 | defp update(result, source, ignore_names) do 98 | result 99 | |> List.flatten() 100 | |> Enum.reduce(source, fn module, source -> issue(module, source, ignore_names) end) 101 | end 102 | 103 | defp issue({module, meta}, source, ignore_names) do 104 | cond do 105 | ignore?(AST.name(module), ignore_names) -> 106 | source 107 | 108 | Keyword.get(meta, :empty, false) -> 109 | add_issue(source, meta, """ 110 | The @moudledoc attribute for moudle #{module} has no content.\ 111 | """) 112 | 113 | Keyword.get(meta, :exist, false) -> 114 | source 115 | 116 | true -> 117 | add_issue(source, meta, """ 118 | The moudle #{module} is missing @moduledoc.\ 119 | """) 120 | end 121 | end 122 | 123 | defp add_issue(source, meta, message) do 124 | Source.add_issue(source, Issue.new(Moduledoc, message, meta)) 125 | end 126 | 127 | defp ignore?(_name, []), do: false 128 | 129 | defp ignore?(name, [ignore_name | ignore_names]) do 130 | with false <- Regex.match?(ignore_name, name) do 131 | ignore?(name, ignore_names) 132 | end 133 | end 134 | 135 | defp validate_keys(config) do 136 | with {:error, unknown} <- Keyword.validate(config, @default_config) do 137 | {:error, "#{@error_message}. Unknown keys: #{inspect(unknown)}"} 138 | end 139 | end 140 | 141 | defp validate_config(config) do 142 | case Keyword.fetch!(config, :ignore_names) do 143 | [_ | _] = list -> Enum.all?(list, fn regex -> is_struct(regex, Regex) end) 144 | %Regex{} -> true 145 | _invalid -> false 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/recode/task/nesting.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.Nesting do 2 | @moduledoc """ 3 | Code should be not nested to deep in functions and macros. 4 | 5 | Developers should refactor a too-deeply nested function/macro by extracting 6 | branches from the code to separate functions to separate different loops and 7 | conditions. 8 | 9 | ## Options 10 | 11 | * `:max_depth` - the maximum allowable depth, defaults to `2`. 12 | 13 | """ 14 | 15 | @shortdoc "Checks code nesting depth in functions and macros." 16 | 17 | use Recode.Task, category: :refactor 18 | 19 | alias Recode.Issue 20 | alias Recode.Task.Nesting 21 | alias Rewrite.Source 22 | alias Sourceror.Zipper 23 | 24 | @def_ops [:def, :defp, :defmacro, :defmacrop] 25 | @depth_ops [:if, :unless, :case, :cond, :fn] 26 | @max_depth 2 27 | 28 | @impl Recode.Task 29 | def run(source, opts) do 30 | source 31 | |> Source.get(:quoted) 32 | |> Zipper.zip() 33 | |> Zipper.traverse_while([], traverse_defs(opts[:max_depth])) 34 | |> update(source) 35 | end 36 | 37 | @impl Recode.Task 38 | def init(opts) do 39 | {:ok, Keyword.put_new(opts, :max_depth, @max_depth)} 40 | end 41 | 42 | defp update({_zipper, issues}, source) do 43 | Source.add_issues(source, issues) 44 | end 45 | 46 | defp traverse_defs(max_depth) do 47 | fn zipper, issues -> traverse_defs(zipper, issues, max_depth) end 48 | end 49 | 50 | defp traverse_defs(%Zipper{node: {op, _, _}} = zipper, issues, max_depth) 51 | when op in @def_ops do 52 | {:skip, zipper, check_depth(zipper, issues, max_depth, 0)} 53 | end 54 | 55 | defp traverse_defs(zipper, issues, _max_depth), do: {:cont, zipper, issues} 56 | 57 | defp traverse_depth(%Zipper{node: {op, _, args}} = zipper, {issues, depth}, max_depth) 58 | when op in @depth_ops and depth < max_depth do 59 | issues = args |> Zipper.zip() |> check_depth(issues, max_depth, depth + 1) 60 | {:skip, zipper, {issues, 0}} 61 | end 62 | 63 | defp traverse_depth(%Zipper{node: {op, meta, _}} = zipper, {issues, _depth}, max_depth) 64 | when op in @depth_ops do 65 | issue = Issue.new(Nesting, "The body is nested too deep (max depth: #{max_depth}).", meta) 66 | {:skip, zipper, {[issue | issues], 0}} 67 | end 68 | 69 | defp traverse_depth(zipper, acc, _max_depth), do: {:cont, zipper, acc} 70 | 71 | defp check_depth(zipper, issues, max_depth, depth) do 72 | {_zipper, {issues, _depth}} = 73 | Zipper.traverse_while(zipper, {issues, depth}, fn zipper, acc -> 74 | traverse_depth(zipper, acc, max_depth) 75 | end) 76 | 77 | issues 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/recode/task/pipe_fun_one.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.PipeFunOne do 2 | @shortdoc "Add parentheses to one-arity functions." 3 | 4 | @moduledoc """ 5 | Add parentheses to one-arity functions. 6 | 7 | # preferred 8 | some_string |> String.downcase() |> String.trim() 9 | 10 | # not preferred 11 | some_string |> String.downcase |> String.trim 12 | 13 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 14 | """ 15 | 16 | use Recode.Task, corrector: true, category: :readability 17 | 18 | alias Recode.Issue 19 | alias Recode.Task.PipeFunOne 20 | alias Rewrite.Source 21 | alias Sourceror.Zipper 22 | 23 | @defs [:def, :defp, :defmacro, :defmacrop, :defdelegate] 24 | 25 | @impl Recode.Task 26 | def run(source, opts) do 27 | {zipper, issues} = 28 | source 29 | |> Source.get(:quoted) 30 | |> Zipper.zip() 31 | |> Zipper.traverse([], fn zipper, issues -> 32 | pipe_fun_one(zipper, issues, opts[:autocorrect]) 33 | end) 34 | 35 | case opts[:autocorrect] do 36 | true -> Source.update(source, PipeFunOne, :quoted, Zipper.root(zipper)) 37 | false -> Source.add_issues(source, issues) 38 | end 39 | end 40 | 41 | defp pipe_fun_one(%Zipper{node: {def, _meta, _args}} = zipper, issues, _autocorrect) 42 | when def in @defs do 43 | {Zipper.next(zipper), issues} 44 | end 45 | 46 | defp pipe_fun_one(%Zipper{node: {:|>, _meta, _tree}} = zipper, issues, true) do 47 | {Zipper.update(zipper, &update/1), issues} 48 | end 49 | 50 | defp pipe_fun_one(%Zipper{node: {:|>, meta, _tree} = ast} = zipper, issues, false) do 51 | case issue?(ast) do 52 | true -> 53 | issue = Issue.new(PipeFunOne, "Use parentheses for one-arity functions in pipes.", meta) 54 | 55 | {zipper, [issue | issues]} 56 | 57 | false -> 58 | {zipper, issues} 59 | end 60 | end 61 | 62 | defp pipe_fun_one(zipper, issues, _autocorrect), do: {zipper, issues} 63 | 64 | defp issue?({:|>, _meta1, [_a, {_name, _meta2, nil}]}), do: true 65 | 66 | defp issue?({:|>, _meta1, [_a, {_name, meta, []}]}), do: Keyword.get(meta, :no_parens, false) 67 | 68 | defp issue?(_ast), do: false 69 | 70 | defp update({:|>, meta, [a, b]}) do 71 | {:|>, meta, [a, update(b)]} 72 | end 73 | 74 | defp update({name, meta, nil}) do 75 | {name, meta, []} 76 | end 77 | 78 | defp update({name, meta, []}) do 79 | {name, Keyword.delete(meta, :no_parens), []} 80 | end 81 | 82 | defp update(tree), do: tree 83 | end 84 | -------------------------------------------------------------------------------- /lib/recode/task/single_pipe.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.SinglePipe do 2 | @shortdoc "Pipes should only be used when piping data through multiple calls." 3 | 4 | @moduledoc """ 5 | Pipes (`|>`) should only be used when piping data through multiple calls. 6 | 7 | # preferred 8 | some_string |> String.downcase() |> String.trim() 9 | Enum.reverse(some_enum) 10 | 11 | # not preferred 12 | some_enum |> Enum.reverse() 13 | 14 | `SinglePipe` does not change a single `|>` that starts with a none zero arity 15 | function. 16 | 17 | # will not be changed 18 | one(:a) |> two() 19 | 20 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 21 | """ 22 | 23 | use Recode.Task, corrector: true, category: :readability 24 | 25 | alias Recode.Issue 26 | alias Recode.Task.SinglePipe 27 | alias Rewrite.Source 28 | alias Sourceror.Zipper 29 | 30 | @defs [:def, :defp, :defmacro, :defmacrop, :defdelegate] 31 | 32 | @impl Recode.Task 33 | def run(source, opts) do 34 | {zipper, issues} = 35 | source 36 | |> Source.get(:quoted) 37 | |> Zipper.zip() 38 | |> Zipper.traverse([], fn zipper, issues -> 39 | single_pipe(zipper, issues, opts[:autocorrect]) 40 | end) 41 | 42 | case opts[:autocorrect] do 43 | true -> 44 | Source.update(source, SinglePipe, :quoted, Zipper.root(zipper)) 45 | 46 | false -> 47 | Source.add_issues(source, issues) 48 | end 49 | end 50 | 51 | defp single_pipe(%Zipper{node: {def, _meta, _args}} = zipper, issues, _autocorrect) 52 | when def in @defs do 53 | {Zipper.next(zipper), issues} 54 | end 55 | 56 | defp single_pipe( 57 | %Zipper{node: {:|>, _meta1, [{:|>, _meta2, _args}, _ast]}} = zipper, 58 | issues, 59 | _autocorrect 60 | ) do 61 | {skip(zipper), issues} 62 | end 63 | 64 | defp single_pipe(%Zipper{node: {:|>, _meta, _ast}} = zipper, issues, true) do 65 | zipper = zipper |> Zipper.update(&update/1) |> skip() 66 | 67 | {zipper, issues} 68 | end 69 | 70 | defp single_pipe(%Zipper{node: {:|>, meta, _ast}} = zipper, issues, false) do 71 | issue = 72 | Issue.new( 73 | SinglePipe, 74 | "Use a function call when a pipeline is only one function long.", 75 | meta 76 | ) 77 | 78 | {zipper, [issue | issues]} 79 | end 80 | 81 | defp single_pipe(zipper, issues, _autocorrect), do: {zipper, issues} 82 | 83 | defp skip(%Zipper{node: {:|>, _meta, _ast}} = zipper) do 84 | zipper |> Zipper.next() |> skip() 85 | end 86 | 87 | defp skip(zipper), do: zipper 88 | 89 | defp update({:|>, _meta1, [{_name, _meta2, nil} = arg, {fun, meta, args}]}) do 90 | {fun, meta, [arg | args]} 91 | end 92 | 93 | defp update({:|>, _meta1, [{_name, _meta2, []} = arg, {fun, meta, args}]}) do 94 | {fun, meta, [arg | args]} 95 | end 96 | 97 | defp update({:|>, _meta1, [{:__block__, _meta2, [_arg]} = block, {fun, meta, args}]}) do 98 | {fun, meta, [block | args]} 99 | end 100 | 101 | defp update({:|>, _meta1, [{:%{}, _meta2, _args} = map, {fun, meta, args}]}) do 102 | {fun, meta, [map | args]} 103 | end 104 | 105 | # Single pipes with two function calls are not changed. 106 | # e.g. `foo(1) |> bar(2)` 107 | # Because we do not want: `bar(2, foo(1))`. Some other check should expand 108 | # this to `1 |> foo() |> bar(2)`. 109 | defp update(ast), do: ast 110 | end 111 | -------------------------------------------------------------------------------- /lib/recode/task/specs.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.Specs do 2 | @shortdoc "Checks for specs." 3 | 4 | @moduledoc """ 5 | Function, macros and callbacks should have typespecs. 6 | 7 | The check only considers whether the specification is present. It doesn't 8 | perform any actual type checking. 9 | 10 | ## Options 11 | 12 | * `:only` - `:public`, `:visible` or `:all`, defaults to `:all`. 13 | * `:macros` - when `true`, macros are also checked, defaults to `false`. 14 | """ 15 | 16 | use Recode.Task, category: :readability 17 | 18 | alias Recode.Context 19 | alias Recode.Issue 20 | alias Recode.Task.Specs 21 | alias Rewrite.Source 22 | alias Sourceror.Zipper 23 | 24 | @impl Recode.Task 25 | def run(source, opts) do 26 | include = Keyword.get(opts, :only, :all) 27 | macros = Keyword.get(opts, :macros, false) 28 | 29 | issues = check_specs(source, {include, macros}) 30 | 31 | Source.add_issues(source, issues) 32 | end 33 | 34 | defp check_specs(source, opts) do 35 | source 36 | |> Source.get(:quoted) 37 | |> Zipper.zip() 38 | |> Context.traverse({[], nil}, fn zipper, context, acc -> 39 | check_specs(zipper, context, acc, opts) 40 | end) 41 | |> result() 42 | end 43 | 44 | defp check_specs(zipper, context, {issues, last_def}, opts) do 45 | case context.definition != last_def do 46 | true -> 47 | issues = check_spec(opts, context, issues) 48 | {zipper, context, {issues, context.definition}} 49 | 50 | false -> 51 | {zipper, context, {issues, last_def}} 52 | end 53 | end 54 | 55 | defp check_spec(_opts, %Context{definition: nil}, issues) do 56 | issues 57 | end 58 | 59 | defp check_spec(_opts, %Context{definition: {{:defmacro, :__using__, _args}, _body}}, issues) do 60 | issues 61 | end 62 | 63 | defp check_spec({_only, false}, %Context{definition: {{kind, _name, _args}, _body}}, issues) 64 | when kind in [:defmacro, :defmacrop] do 65 | issues 66 | end 67 | 68 | defp check_spec({:all, _macros}, context, issues) do 69 | case not Context.spec?(context) and not Context.impl?(context) do 70 | true -> [issue(context) | issues] 71 | false -> issues 72 | end 73 | end 74 | 75 | defp check_spec({only, _macros}, context, issues) do 76 | case Context.definition?(context, only) and 77 | not Context.spec?(context) and 78 | not Context.impl?(context) do 79 | true -> [issue(context) | issues] 80 | false -> issues 81 | end 82 | end 83 | 84 | defp issue(%Context{definition: {_definition, meta}}) do 85 | message = "Functions should have a @spec type specification." 86 | Issue.new(Specs, message, meta) 87 | end 88 | 89 | defp result({_zipper, {issues, _seen}}), do: issues 90 | end 91 | -------------------------------------------------------------------------------- /lib/recode/task/tag_fixme.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.TagFIXME do 2 | @shortdoc "Checks if there are FIXME tags in the sources." 3 | 4 | @moduledoc """ 5 | #{@shortdoc} 6 | 7 | FIXME tags in comments and docs are used as a reminder and should be handeld in 8 | the near future. 9 | 10 | ## Examples 11 | 12 | # FIXME: this function returns a wrong value 13 | def fun do 14 | # ... 15 | end 16 | 17 | ## Options 18 | 19 | * `:include_docs` - includes `@doc`, `@moduledoc` and `@shortdoc` to the 20 | check when set to `true`. Defaults to `true`. 21 | 22 | """ 23 | 24 | use Recode.Task, category: :design 25 | 26 | alias Recode.Task.Tags 27 | 28 | @impl Recode.Task 29 | def init(opts) do 30 | Tags.init(Keyword.merge([tag: "FIXME", reporter: Recode.Task.TagFIXME], opts)) 31 | end 32 | 33 | @impl Recode.Task 34 | defdelegate run(source, opts), to: Tags 35 | end 36 | -------------------------------------------------------------------------------- /lib/recode/task/tag_todo.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.TagTODO do 2 | @shortdoc "Checks if there are TODO tags in the sources." 3 | 4 | @moduledoc """ 5 | #{@shortdoc} 6 | 7 | TODO tags in comments and docs are used as a reminder and should be handeld in 8 | the near future. 9 | 10 | ## Examples 11 | 12 | # TODO: refactor this function 13 | def fun do 14 | # ... 15 | end 16 | 17 | ## Options 18 | 19 | * `:include_docs` - includes `@doc`, `@moduledoc` and `@shortdoc` to the 20 | check when set to `true`. Defaults to `true`. 21 | 22 | """ 23 | 24 | use Recode.Task, category: :design 25 | 26 | alias Recode.Task.Tags 27 | 28 | @impl Recode.Task 29 | def init(opts) do 30 | Tags.init(Keyword.merge([tag: "TODO", reporter: Recode.Task.TagTODO], opts)) 31 | end 32 | 33 | @impl Recode.Task 34 | defdelegate run(source, opts), to: Tags 35 | end 36 | -------------------------------------------------------------------------------- /lib/recode/task/tags.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.Tags do 2 | @moduledoc false 3 | 4 | use Recode.Task, category: :design 5 | 6 | alias Recode.Issue 7 | alias Rewrite.Source 8 | alias Sourceror.Zipper 9 | 10 | @defaults include_docs: true 11 | 12 | @impl Recode.Task 13 | def run(source, opts) do 14 | source 15 | |> Source.get(:quoted) 16 | |> Zipper.zip() 17 | |> Zipper.traverse([], fn zipper, issues -> 18 | check_tags(zipper, issues, opts) 19 | end) 20 | |> update(source) 21 | end 22 | 23 | @impl Recode.Task 24 | def init(opts) do 25 | cond do 26 | not Keyword.has_key?(opts, :tag) -> 27 | {:error, 28 | """ 29 | Recode.Task.Tags needs a configuration entry for :tag (e.g. tag: "TODO")\ 30 | """} 31 | 32 | not Keyword.has_key?(opts, :reporter) -> 33 | {:error, 34 | """ 35 | Recode.Task.Tags needs a configuration entry for :reporter \ 36 | (e.g. reporter: "Recode.Task.TagTODO")\ 37 | """} 38 | 39 | true -> 40 | {:ok, Keyword.merge(@defaults, opts)} 41 | end 42 | end 43 | 44 | defp update({_zipper, issues}, source) do 45 | Source.add_issues(source, issues) 46 | end 47 | 48 | defp check_tags(%Zipper{node: {:@, _, [{doc, _, args}]}} = zipper, issues, opts) 49 | when doc in [:moduledoc, :doc, :shortdoc] do 50 | issues = 51 | if opts[:include_docs] && doc?(args) do 52 | args 53 | |> doc() 54 | |> find_tags(opts) 55 | |> Enum.concat(issues) 56 | else 57 | issues 58 | end 59 | 60 | {zipper, issues} 61 | end 62 | 63 | defp check_tags(%Zipper{node: {_op, meta, _args}} = zipper, issues, opts) do 64 | issues = 65 | meta 66 | |> comments() 67 | |> Enum.flat_map(fn comment -> find_tags(comment, opts) end) 68 | |> Enum.concat(issues) 69 | 70 | {zipper, issues} 71 | end 72 | 73 | defp check_tags(zipper, issues, _opts), do: {zipper, issues} 74 | 75 | defp doc?([{:__block__, _meta, [text]}]), do: is_binary(text) 76 | defp doc?(_ast), do: false 77 | 78 | defp doc([{:__block__, meta, [text]}]) do 79 | doc = Keyword.put(meta, :text, text) 80 | 81 | case String.length(doc[:delimiter] || "") do 82 | 3 -> Keyword.update!(doc, :line, fn line -> line + 1 end) 83 | _ -> doc 84 | end 85 | end 86 | 87 | defp comments(meta) do 88 | Keyword.get(meta, :leading_comments, []) ++ Keyword.get(meta, :trailing_comments, []) 89 | end 90 | 91 | defp find_tags(meta, opts) when is_map(meta) do 92 | find_tags(Enum.into(meta, []), opts) 93 | end 94 | 95 | defp find_tags(meta, opts) do 96 | meta 97 | |> Keyword.get(:text) 98 | |> split_lines() 99 | |> find_tags(~r/^#?\s*#{opts[:tag]}/, 0, []) 100 | |> issues(meta, opts) 101 | end 102 | 103 | defp find_tags([], _regex, _line_number, acc), do: acc 104 | 105 | defp find_tags([line | lines], regex, line_number, acc) do 106 | case Regex.match?(regex, line) do 107 | true -> find_tags(lines, regex, line_number + 1, [{line_number, line} | acc]) 108 | false -> find_tags(lines, regex, line_number + 1, acc) 109 | end 110 | end 111 | 112 | defp issues(tags, meta, opts) do 113 | Enum.map(tags, fn {line, text} -> 114 | text = Regex.replace(~r/^[#\s]*/, text, "") 115 | Issue.new(opts[:reporter], "Found a tag: #{text}", line: line + meta[:line]) 116 | end) 117 | end 118 | 119 | defp split_lines(nil), do: [] 120 | defp split_lines(string) when is_binary(string), do: String.split(string, "\n") 121 | end 122 | -------------------------------------------------------------------------------- /lib/recode/task/test_file_ext.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.TestFileExt do 2 | @shortdoc "Checks the file extension of test files." 3 | 4 | @moduledoc """ 5 | Tests must be in a file with the extension `*_test.exs`. 6 | 7 | This module searches for `*_test.ex` to rename the file and/or report an 8 | issue. 9 | """ 10 | 11 | use Recode.Task, corrector: true, category: :warning 12 | 13 | alias Recode.Issue 14 | alias Recode.Task.TestFileExt 15 | alias Rewrite.Source 16 | 17 | @impl Recode.Task 18 | def run(source, opts) do 19 | test_file_ext(source, opts[:autocorrect]) 20 | end 21 | 22 | defp test_file_ext(%Source{path: path} = source, autocrecct) do 23 | case update_path(path) do 24 | ^path -> source 25 | updated_path -> update_source(source, updated_path, autocrecct) 26 | end 27 | end 28 | 29 | defp update_path(path) when is_binary(path) do 30 | String.replace(path, ~r/_test\.ex$/, "_test.exs") 31 | end 32 | 33 | defp update_source(source, path, true) do 34 | Source.update(source, TestFileExt, :path, path) 35 | end 36 | 37 | defp update_source(source, path, false) do 38 | message = "The file must be renamed to #{path} so that ExUnit can find it." 39 | Source.add_issue(source, Issue.new(TestFileExt, message)) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/recode/task/unnecessary_if_unless.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.UnnecessaryIfUnless do 2 | @shortdoc "Removes redundant booleans" 3 | 4 | @moduledoc """ 5 | Redudant booleans make code needlesly verbose. 6 | 7 | # preferred 8 | foo == bar 9 | 10 | # not preferred 11 | if foo == bar do 12 | true 13 | else 14 | false 15 | end 16 | 17 | This task rewrites the code when `mix recode` runs with `autocorrect: true`. 18 | """ 19 | 20 | use Recode.Task, corrector: true, category: :readability 21 | 22 | alias Recode.Issue 23 | alias Recode.Task.UnnecessaryIfUnless 24 | alias Rewrite.Source 25 | alias Sourceror.Zipper 26 | 27 | @impl Recode.Task 28 | def run(source, opts) do 29 | {zipper, issues} = 30 | source 31 | |> Source.get(:quoted) 32 | |> Zipper.zip() 33 | |> Zipper.traverse([], fn zipper, issues -> 34 | remove_unecessary_if_unless(zipper, issues, opts[:autocorrect]) 35 | end) 36 | 37 | case opts[:autocorrect] do 38 | true -> 39 | Source.update(source, UnnecessaryIfUnless, :quoted, Zipper.root(zipper)) 40 | 41 | false -> 42 | Source.add_issues(source, issues) 43 | end 44 | end 45 | 46 | defp remove_unecessary_if_unless( 47 | %Zipper{node: {conditional, meta, body}} = zipper, 48 | issues, 49 | true 50 | ) 51 | when conditional in [:if, :unless] do 52 | case extract(body, conditional) do 53 | {:ok, expr} -> 54 | expr = put_leading_comments(expr, meta) 55 | 56 | {Zipper.replace(zipper, expr), issues} 57 | 58 | :error -> 59 | {zipper, issues} 60 | end 61 | end 62 | 63 | defp remove_unecessary_if_unless( 64 | %Zipper{node: {conditional, meta, body}} = zipper, 65 | issues, 66 | false 67 | ) 68 | when conditional in [:if, :unless] do 69 | issues = 70 | case extract(body, conditional) do 71 | {:ok, _expr} -> 72 | message = "Avoid unnecessary `if` and `unless`" 73 | issue = Issue.new(UnnecessaryIfUnless, message, meta) 74 | [issue | issues] 75 | 76 | :error -> 77 | issues 78 | end 79 | 80 | {zipper, issues} 81 | end 82 | 83 | defp remove_unecessary_if_unless(zipper, issues, _autocorrect), do: {zipper, issues} 84 | 85 | defp extract( 86 | [ 87 | expr, 88 | [ 89 | {{:__block__, _, [:do]}, {:__block__, _, [left]}}, 90 | {{:__block__, _, [:else]}, {:__block__, _, [right]}} 91 | ] 92 | ], 93 | conditional 94 | ) 95 | when is_boolean(left) and is_boolean(right) do 96 | case {conditional, left, right} do 97 | {:if, true, false} -> {:ok, expr} 98 | {:if, false, true} -> {:ok, {:not, [], [expr]}} 99 | {:unless, true, false} -> {:ok, {:not, [], [expr]}} 100 | {:unless, false, true} -> {:ok, expr} 101 | end 102 | end 103 | 104 | defp extract(_, _), do: :error 105 | 106 | defp put_leading_comments(expr, meta) do 107 | comments = meta[:leading_comments] || [] 108 | Sourceror.append_comments(expr, comments) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/recode/task/unused_variable.ex: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.UnusedVariable do 2 | @shortdoc "Checks if unused variables occur." 3 | 4 | @moduledoc """ 5 | Prepend unused variables with `_`. 6 | """ 7 | 8 | use Recode.Task, corrector: true, category: :warning 9 | 10 | alias Recode.Issue 11 | alias Rewrite.Source 12 | alias Sourceror.Zipper 13 | 14 | @impl Recode.Task 15 | def run(source, opts) do 16 | {zipper, issues} = 17 | source 18 | |> Source.get(:quoted) 19 | |> Zipper.zip() 20 | |> Zipper.traverse([], fn zipper, issues -> 21 | prepend_unused_variable_with_underscore(zipper, issues) 22 | end) 23 | 24 | case opts[:autocorrect] do 25 | true -> 26 | Source.update(source, __MODULE__, :quoted, Zipper.root(zipper)) 27 | 28 | false -> 29 | Source.add_issues(source, issues) 30 | end 31 | end 32 | 33 | defp process_unused({var_name, var_meta, nil} = var, search_zipper, zipper, issues) do 34 | if unused?(search_zipper, var) do 35 | zipper = add_underscore(zipper, var) 36 | 37 | issue = Issue.new(__MODULE__, "Unused variable: #{var_name}", var_meta) 38 | 39 | {zipper, [issue | issues]} 40 | else 41 | {zipper, issues} 42 | end 43 | end 44 | 45 | defp process_unused(_var, _search_zipper, zipper, issues) do 46 | {zipper, issues} 47 | end 48 | 49 | defp prepend_unused_variable_with_underscore( 50 | %Zipper{node: {:=, _meta, [var, _other]}} = zipper, 51 | issues 52 | ) do 53 | process_unused(var, Zipper.top(zipper), zipper, issues) 54 | end 55 | 56 | defp prepend_unused_variable_with_underscore( 57 | %Zipper{node: {:def, _meta, [{_fun_name, _fun_meta, nil}, _content]}} = zipper, 58 | issues 59 | ) do 60 | {zipper, issues} 61 | end 62 | 63 | defp prepend_unused_variable_with_underscore( 64 | %Zipper{node: {:def, _meta, [{_fun_name, _fun_meta, params}, _content]}} = zipper, 65 | issues 66 | ) do 67 | {fzipper, fissues} = 68 | Enum.reduce(params, {zipper, issues}, fn var, {z, i} -> 69 | process_unused(var, zipper, z, i) 70 | end) 71 | 72 | {fzipper, fissues} 73 | end 74 | 75 | defp prepend_unused_variable_with_underscore( 76 | %Zipper{node: {:defp, _meta, [{_fun_name, _fun_meta, nil}, _content]}} = zipper, 77 | issues 78 | ) do 79 | {zipper, issues} 80 | end 81 | 82 | defp prepend_unused_variable_with_underscore( 83 | %Zipper{node: {:defp, _meta, [{_fun_name, _fun_meta, params}, _content]}} = zipper, 84 | issues 85 | ) do 86 | {fzipper, fissues} = 87 | Enum.reduce(params, {zipper, issues}, fn var, {z, i} -> 88 | process_unused(var, zipper, z, i) 89 | end) 90 | 91 | {fzipper, fissues} 92 | end 93 | 94 | defp prepend_unused_variable_with_underscore(zipper, issues), do: {zipper, issues} 95 | 96 | defp add_underscore(zipper, var) do 97 | zipper 98 | |> Zipper.find(fn node -> node == var end) 99 | |> Zipper.update(fn 100 | {name, meta, value} -> 101 | {:"_#{name}", meta, value} 102 | 103 | other -> 104 | other 105 | end) 106 | end 107 | 108 | defp unused?(zipper, {name, metadata, _value}) do 109 | if String.starts_with?(Atom.to_string(name), "_") do 110 | false 111 | else 112 | {_zipper, references} = 113 | Zipper.traverse(zipper, [], fn 114 | %Zipper{node: {^name, ref_metadata, _value} = ref} = zipper, references 115 | when metadata != ref_metadata -> 116 | {zipper, [ref | references]} 117 | 118 | zipper, references -> 119 | {zipper, references} 120 | end) 121 | 122 | references == [] 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.7.3" 5 | @source_url "https://github.com/hrzndhrn/recode" 6 | 7 | def project do 8 | [ 9 | app: :recode, 10 | version: @version, 11 | elixir: "~> 1.13", 12 | name: "Recode", 13 | description: description(), 14 | elixirc_paths: elixirc_paths(), 15 | docs: docs(), 16 | source_url: @source_url, 17 | start_permanent: Mix.env() == :prod, 18 | dialyzer: dialyzer(), 19 | test_coverage: [tool: ExCoveralls], 20 | preferred_cli_env: preferred_cli_env(), 21 | deps: deps(), 22 | package: package(), 23 | aliases: aliases() 24 | ] 25 | end 26 | 27 | def application do 28 | [ 29 | extra_applications: [:logger, :mix, :ex_unit, :crypto, :iex, :eex], 30 | mod: {Recode.Application, []} 31 | ] 32 | end 33 | 34 | defp description do 35 | "An experimental linter with autocorrection." 36 | end 37 | 38 | defp elixirc_paths do 39 | case Mix.env() do 40 | :test -> ["lib", "test/support"] 41 | _env -> ["lib"] 42 | end 43 | end 44 | 45 | defp docs do 46 | [ 47 | source_ref: "v#{@version}", 48 | formatters: ["html"], 49 | groups_for_modules: [ 50 | Tasks: [ 51 | Recode.Task.AliasExpansion, 52 | Recode.Task.AliasOrder, 53 | Recode.Task.Dbg, 54 | Recode.Task.EnforceLineLength, 55 | Recode.Task.FilterCount, 56 | Recode.Task.Format, 57 | Recode.Task.IOInspect, 58 | Recode.Task.LocalsWithoutParens, 59 | Recode.Task.Moduledoc, 60 | Recode.Task.Nesting, 61 | Recode.Task.PipeFunOne, 62 | Recode.Task.SinglePipe, 63 | Recode.Task.Specs, 64 | Recode.Task.TagFIXME, 65 | Recode.Task.TagTODO, 66 | Recode.Task.TestFileExt, 67 | Recode.Task.UnnecessaryIfUnless, 68 | Recode.Task.UnusedVariable 69 | ] 70 | ] 71 | ] 72 | end 73 | 74 | defp dialyzer do 75 | [ 76 | ignore_warnings: ".dialyzer_ignore.exs", 77 | plt_file: {:no_warn, "test/support/plts/dialyzer.plt"}, 78 | flags: [:unmatched_returns] 79 | ] 80 | end 81 | 82 | def preferred_cli_env do 83 | [ 84 | carp: :test, 85 | coveralls: :test, 86 | "coveralls.detail": :test, 87 | "coveralls.post": :test, 88 | "coveralls.html": :test, 89 | "coveralls.github": :test 90 | ] 91 | end 92 | 93 | defp aliases do 94 | [ 95 | carp: "test --seed 0 --max-failures 1" 96 | ] 97 | end 98 | 99 | defp deps do 100 | [ 101 | {:escape, "~> 0.1"}, 102 | {:glob_ex, "~> 0.1"}, 103 | {:rewrite, "~> 0.9"}, 104 | # dev/test 105 | {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, 106 | {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, 107 | {:ex_doc, "~> 0.25", only: :dev, runtime: false}, 108 | {:excoveralls, "~> 0.15", only: :test}, 109 | {:mox, "~> 1.0", only: :test} 110 | ] 111 | end 112 | 113 | defp package do 114 | [ 115 | maintainers: ["Marcus Kruse"], 116 | licenses: ["MIT"], 117 | links: %{"GitHub" => @source_url} 118 | ] 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 4 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, 6 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 7 | "escape": {:hex, :escape, "0.2.0", "346fced73c28087a42288e164df56ca5ee2966ce2a355b8c6a507a8dbe077353", [:mix], [], "hexpm", "a6fee4fd8c42c85379187edc4b6c69f027fed828ea14283b162bf0e124f6bc8e"}, 8 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, 9 | "excoveralls": {:hex, :excoveralls, "0.18.4", "70f70dc37b9bd90cf66868c12778d2f60b792b79e9e12aed000972a8046dc093", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cda8bb587d9deaa0da6158cf0a18929189abbe53bf42b10fe70016d5f4f5d6a9"}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, 12 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 13 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 16 | "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, 17 | "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.1", "f41275a0354c736db4b1d255b5d2a27c91028e55c21ea3145b938e22649ffa3f", [:mix], [], "hexpm", "605e44204998f138d6e13be366c8e81af860e726c8177caf50067e1b618fe522"}, 19 | "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, 20 | "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, 21 | } 22 | -------------------------------------------------------------------------------- /recode.exs: -------------------------------------------------------------------------------- 1 | [ 2 | version: "0.7.2", 3 | # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. 4 | autocorrect: true, 5 | # With "--dry" no changes will be written to the files. 6 | # Can also be set/reset with `--dry`/`--no-dry`. 7 | # If dry is true then verbose is also active. 8 | dry: false, 9 | # Enables or disables color in the output. 10 | color: true, 11 | # Can also be set/reset with `--verbose`/`--no-verbose`. 12 | verbose: false, 13 | # Can be overwritten by calling `mix recode "lib/**/*.ex"`. 14 | inputs: ["{mix,.formatter}.exs", "{apps,config,lib}/**/*.{ex,exs}"], 15 | formatters: [Recode.CLIFormatter], 16 | tasks: [ 17 | # Tasks could be added by a tuple of the tasks module name and an options 18 | # keyword list. A task can be deactivated by `active: false`. The execution of 19 | # a deactivated task can be forced by calling `mix recode --task ModuleName`. 20 | {Recode.Task.AliasExpansion, []}, 21 | {Recode.Task.AliasOrder, []}, 22 | {Recode.Task.Dbg, [autocorrect: false]}, 23 | {Recode.Task.EnforceLineLength, [active: false]}, 24 | {Recode.Task.FilterCount, []}, 25 | {Recode.Task.IOInspect, [autocorrect: false]}, 26 | {Recode.Task.Nesting, []}, 27 | {Recode.Task.PipeFunOne, []}, 28 | {Recode.Task.SinglePipe, []}, 29 | {Recode.Task.Specs, [exclude: ["test/**/*.{ex,exs}", "mix.exs"], config: [only: :visible]]}, 30 | {Recode.Task.TagFIXME, [exit_code: 2]}, 31 | {Recode.Task.TagTODO, [exit_code: 4]}, 32 | {Recode.Task.TestFileExt, []}, 33 | {Recode.Task.UnusedVariable, [active: false]} 34 | ] 35 | ] 36 | -------------------------------------------------------------------------------- /test/fixtures/config.exs: -------------------------------------------------------------------------------- 1 | [ 2 | version: "0.7.2", 3 | # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. 4 | autocorrect: true, 5 | # With "--dry" no changes will be written to the files. 6 | # Can also be set/reset with `--dry`/`--no-dry`. 7 | # If dry is true then verbose is also active. 8 | dry: false, 9 | # Can also be set/reset with `--verbose`/`--no-verbose`. 10 | verbose: false, 11 | # Can be overwritten by calling `mix recode "lib/**/*.ex"`. 12 | inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"], 13 | formatters: [Recode.CLIFormatter], 14 | tasks: [ 15 | # Tasks could be added by a tuple of the tasks module name and an options 16 | # keyword list. A task can be deactivated by `active: false`. The execution of 17 | # a deactivated task can be forced by calling `mix recode --task ModuleName`. 18 | {Recode.Task.AliasExpansion, []}, 19 | {Recode.Task.AliasOrder, []}, 20 | {Recode.Task.Dbg, [autocorrect: false]}, 21 | {Recode.Task.EnforceLineLength, [active: false]}, 22 | {Recode.Task.FilterCount, []}, 23 | {Recode.Task.PipeFunOne, []}, 24 | {Recode.Task.SinglePipe, []}, 25 | {Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]}, 26 | {Recode.Task.TestFileExt, []}, 27 | {Recode.Task.UnusedVariable, [active: false]} 28 | ] 29 | ] 30 | -------------------------------------------------------------------------------- /test/fixtures/context/doc_and_spec.ex: -------------------------------------------------------------------------------- 1 | defmodule Traverse.Simple do 2 | @moduledoc """ 3 | Doc for module simple 4 | """ 5 | end 6 | 7 | defmodule Traverse.Simpler do 8 | @doc """ 9 | Doc for foo 10 | """ 11 | 12 | @ignore "me" 13 | 14 | @spec foo(integer() | nil) :: integer() | nil 15 | def foo(nil), do: nil 16 | 17 | def foo(x) do 18 | x * 2 19 | end 20 | 21 | def baz, do: :baz 22 | end 23 | -------------------------------------------------------------------------------- /test/fixtures/context/impl.ex: -------------------------------------------------------------------------------- 1 | defmodule Traverse.Foo do 2 | use Traverse.Something 3 | 4 | @impl true 5 | def foo(x), do: {:foo, x} 6 | 7 | @impl Traverse.Something 8 | def baz, do: :baz 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/context/nested.ex: -------------------------------------------------------------------------------- 1 | defmodule Traverse.SomeModule do 2 | def add(a, b) do 3 | a + b 4 | end 5 | 6 | def call, do: :call 7 | end 8 | 9 | defmodule Traverse.Imp do 10 | def foo(a) do 11 | a + a 12 | end 13 | 14 | def bar(a, b) do 15 | a + b 16 | end 17 | end 18 | 19 | defmodule Traverse.Asterix do 20 | defmacro __using__(_opts) do 21 | end 22 | end 23 | 24 | # a comment 25 | 26 | defmodule Traverse.Simple do 27 | alias Traverse.SomeModule 28 | import Traverse.Imp, only: [foo: 1, bar: 2] 29 | use Traverse.Asterix 30 | 31 | defmodule Nested do 32 | def bar(x) do 33 | foo(x) + bar(2, x) 34 | end 35 | end 36 | 37 | def foo(x) do 38 | SomeModule.call() 39 | x * 2 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/fixtures/context/simple.ex: -------------------------------------------------------------------------------- 1 | defmodule Traverse.Simple do 2 | def foo(x) do 3 | x * 2 4 | end 5 | 6 | def baz, do: :baz 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/context/use_import_etc.ex: -------------------------------------------------------------------------------- 1 | defmodule Traverse.Obelix do 2 | defmacro __using__(_opts) do 3 | quote do 4 | end 5 | end 6 | end 7 | 8 | defmodule Traverse.Pluto do 9 | end 10 | 11 | defmodule Traverse.Mouse do 12 | def micky(x), do: x + x 13 | end 14 | 15 | defmodule Traverse.Foo do 16 | use Traverse.Obelix, app: Traverse 17 | 18 | alias Traverse.Nested.Simple 19 | alias Donald.Duck, as: Goofy 20 | alias Foo.{Bar, Baz} 21 | 22 | import Traverse.Pluto 23 | import Traverse.Mouse, only: [micky: 1] 24 | import Traverse.{Gladstone, Gander} 25 | 26 | require Logger 27 | require Traverse.Pluto, as: Animal 28 | 29 | def foo, do: Simple.foo() 30 | 31 | def xyz do 32 | Bar.x() + Baz.y() + Goofy.z() 33 | end 34 | 35 | def mouse do 36 | micky(:entenhausen) 37 | end 38 | end 39 | 40 | defmodule Traverse.Timer do 41 | import :timer 42 | end 43 | 44 | defmodule Traverse.RequireAlias do 45 | require alias Traverse.Pluto 46 | end 47 | 48 | defmodule Traverse.RequireAliasAs do 49 | require alias Traverse.Pluto, as: Foo 50 | end 51 | 52 | defmodule Traverse.AliasRequire do 53 | alias require Traverse.Pluto 54 | end 55 | -------------------------------------------------------------------------------- /test/fixtures/context/vars.ex: -------------------------------------------------------------------------------- 1 | defmodule Vars do 2 | def vars do 3 | alias = "alias" 4 | import = "import" 5 | require = "require" 6 | use = "use" 7 | defimpl = "defimpl" 8 | def = "def" 9 | end 10 | 11 | def vars(x) do 12 | alias = identity(x) 13 | import = identity(x) 14 | require = identity(x) 15 | use = identity(x) 16 | defimpl = identity(x) 17 | def = identity(x) 18 | end 19 | 20 | defp identity(x), do: x 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/context/vars.exs: -------------------------------------------------------------------------------- 1 | alias = "alias" 2 | import = "import" 3 | require = "require" 4 | use = "use" 5 | defimpl = "defimpl" 6 | -------------------------------------------------------------------------------- /test/fixtures/context/vars.exs.output: -------------------------------------------------------------------------------- 1 | === 1 === 2 | 1: module: nil 3 | 1: definition: nil 4 | 1: usages: [] 5 | 1: aliases: [] 6 | 1: requirements: [] 7 | 1: imports: [] 8 | 1: moduledoc: nil 9 | 1: doc: nil 10 | 1: spec: nil 11 | 1: impl: nil 12 | === 2 === 13 | 2: module: nil 14 | 2: definition: nil 15 | 2: usages: [] 16 | 2: aliases: [] 17 | 2: requirements: [] 18 | 2: imports: [] 19 | 2: moduledoc: nil 20 | 2: doc: nil 21 | 2: spec: nil 22 | 2: impl: nil 23 | === 3 === 24 | 3: module: nil 25 | 3: definition: nil 26 | 3: usages: [] 27 | 3: aliases: [] 28 | 3: requirements: [] 29 | 3: imports: [] 30 | 3: moduledoc: nil 31 | 3: doc: nil 32 | 3: spec: nil 33 | 3: impl: nil 34 | === 4 === 35 | 4: module: nil 36 | 4: definition: nil 37 | 4: usages: [] 38 | 4: aliases: [] 39 | 4: requirements: [] 40 | 4: imports: [] 41 | 4: moduledoc: nil 42 | 4: doc: nil 43 | 4: spec: nil 44 | 4: impl: nil 45 | === 5 === 46 | 5: module: nil 47 | 5: definition: nil 48 | 5: usages: [] 49 | 5: aliases: [] 50 | 5: requirements: [] 51 | 5: imports: [] 52 | 5: moduledoc: nil 53 | 5: doc: nil 54 | 5: spec: nil 55 | 5: impl: nil 56 | === 6 === 57 | 6: module: nil 58 | 6: definition: nil 59 | 6: usages: [] 60 | 6: aliases: [] 61 | 6: requirements: [] 62 | 6: imports: [] 63 | 6: moduledoc: nil 64 | 6: doc: nil 65 | 6: spec: nil 66 | 6: impl: nil 67 | === 7 === 68 | 7: module: nil 69 | 7: definition: nil 70 | 7: usages: [] 71 | 7: aliases: [] 72 | 7: requirements: [] 73 | 7: imports: [] 74 | 7: moduledoc: nil 75 | 7: doc: nil 76 | 7: spec: nil 77 | 7: impl: nil 78 | === 8 === 79 | 8: module: nil 80 | 8: definition: nil 81 | 8: usages: [] 82 | 8: aliases: [] 83 | 8: requirements: [] 84 | 8: imports: [] 85 | 8: moduledoc: nil 86 | 8: doc: nil 87 | 8: spec: nil 88 | 8: impl: nil 89 | === 9 === 90 | 9: module: nil 91 | 9: definition: nil 92 | 9: usages: [] 93 | 9: aliases: [] 94 | 9: requirements: [] 95 | 9: imports: [] 96 | 9: moduledoc: nil 97 | 9: doc: nil 98 | 9: spec: nil 99 | 9: impl: nil 100 | === 10 === 101 | 10: module: nil 102 | 10: definition: nil 103 | 10: usages: [] 104 | 10: aliases: [] 105 | 10: requirements: [] 106 | 10: imports: [] 107 | 10: moduledoc: nil 108 | 10: doc: nil 109 | 10: spec: nil 110 | 10: impl: nil 111 | === 11 === 112 | 11: module: nil 113 | 11: definition: nil 114 | 11: usages: [] 115 | 11: aliases: [] 116 | 11: requirements: [] 117 | 11: imports: [] 118 | 11: moduledoc: nil 119 | 11: doc: nil 120 | 11: spec: nil 121 | 11: impl: nil 122 | === 12 === 123 | 12: module: nil 124 | 12: definition: nil 125 | 12: usages: [] 126 | 12: aliases: [] 127 | 12: requirements: [] 128 | 12: imports: [] 129 | 12: moduledoc: nil 130 | 12: doc: nil 131 | 12: spec: nil 132 | 12: impl: nil 133 | === 13 === 134 | 13: module: nil 135 | 13: definition: nil 136 | 13: usages: [] 137 | 13: aliases: [] 138 | 13: requirements: [] 139 | 13: imports: [] 140 | 13: moduledoc: nil 141 | 13: doc: nil 142 | 13: spec: nil 143 | 13: impl: nil 144 | === 14 === 145 | 14: module: nil 146 | 14: definition: nil 147 | 14: usages: [] 148 | 14: aliases: [] 149 | 14: requirements: [] 150 | 14: imports: [] 151 | 14: moduledoc: nil 152 | 14: doc: nil 153 | 14: spec: nil 154 | 14: impl: nil 155 | === 15 === 156 | 15: module: nil 157 | 15: definition: nil 158 | 15: usages: [] 159 | 15: aliases: [] 160 | 15: requirements: [] 161 | 15: imports: [] 162 | 15: moduledoc: nil 163 | 15: doc: nil 164 | 15: spec: nil 165 | 15: impl: nil 166 | === 16 === 167 | 16: module: nil 168 | 16: definition: nil 169 | 16: usages: [] 170 | 16: aliases: [] 171 | 16: requirements: [] 172 | 16: imports: [] 173 | 16: moduledoc: nil 174 | 16: doc: nil 175 | 16: spec: nil 176 | 16: impl: nil 177 | === 17 === 178 | 17: module: nil 179 | 17: definition: nil 180 | 17: usages: [] 181 | 17: aliases: [] 182 | 17: requirements: [] 183 | 17: imports: [] 184 | 17: moduledoc: nil 185 | 17: doc: nil 186 | 17: spec: nil 187 | 17: impl: nil 188 | === 18 === 189 | 18: module: nil 190 | 18: definition: nil 191 | 18: usages: [] 192 | 18: aliases: [] 193 | 18: requirements: [] 194 | 18: imports: [] 195 | 18: moduledoc: nil 196 | 18: doc: nil 197 | 18: spec: nil 198 | 18: impl: nil 199 | === 19 === 200 | 19: module: nil 201 | 19: definition: nil 202 | 19: usages: [] 203 | 19: aliases: [] 204 | 19: requirements: [] 205 | 19: imports: [] 206 | 19: moduledoc: nil 207 | 19: doc: nil 208 | 19: spec: nil 209 | 19: impl: nil 210 | === 20 === 211 | 20: module: nil 212 | 20: definition: nil 213 | 20: usages: [] 214 | 20: aliases: [] 215 | 20: requirements: [] 216 | 20: imports: [] 217 | 20: moduledoc: nil 218 | 20: doc: nil 219 | 20: spec: nil 220 | 20: impl: nil 221 | -------------------------------------------------------------------------------- /test/fixtures/context/when.ex: -------------------------------------------------------------------------------- 1 | defmodule Context.When do 2 | def foo(x) when x == 5 do 3 | x * 2 4 | end 5 | 6 | def baz(x) when x == 5, do: :baz 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/invalid_task_config.exs: -------------------------------------------------------------------------------- 1 | [ 2 | version: "0.7.2", 3 | # Can also be set/reset with `--autocorrect`/`--no-autocorrect`. 4 | autocorrect: true, 5 | # With "--dry" no changes will be written to the files. 6 | # Can also be set/reset with `--dry`/`--no-dry`. 7 | # If dry is true then verbose is also active. 8 | dry: false, 9 | # Can also be set/reset with `--verbose`/`--no-verbose`. 10 | verbose: false, 11 | # Can be overwritten by calling `mix recode "lib/**/*.ex"`. 12 | inputs: ["{mix,.formatter}.exs", "{apps,config,lib,test}/**/*.{ex,exs}"], 13 | formatters: [Recode.CLIFormatter], 14 | tasks: [ 15 | # Tasks could be added by a tuple of the tasks module name and an options 16 | # keyword list. A task can be deactivated by `active: false`. The execution of 17 | # a deactivated task can be forced by calling `mix recode --task ModuleName`. 18 | {Recode.Task.AliasExpansion, []}, 19 | {Recode.Task.AliasOrder, []}, 20 | {Recode.Task.Dbg, [autocorrect: false, invalid: :key]}, 21 | {Recode.Task.EnforceLineLength, [active: false]}, 22 | {Recode.Task.FilterCount, []}, 23 | {Recode.Task.PipeFunOne, []}, 24 | {Recode.Task.SinglePipe, []}, 25 | {Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]}, 26 | {Recode.Task.TestFileExt, []}, 27 | {Recode.Task.UnusedVariable, [active: false]} 28 | ] 29 | ] 30 | -------------------------------------------------------------------------------- /test/fixtures/rename/config.exs: -------------------------------------------------------------------------------- 1 | alias Recode.Task 2 | 3 | [ 4 | # Can also be set/reset with "--autocorrect"/"--no-autocorrect". 5 | autocorrect: true, 6 | # With "--dry" no changes will be written to the files. 7 | # Can also be set/reset with "--dry"/"--no-dry". 8 | # If dry is true then verbose is also active. 9 | dry: false, 10 | # Can also be set/reset with "--verbose"/"--no-verbose". 11 | verbose: false, 12 | inputs: ["{config,lib,test}/**/*.{ex,exs}"], 13 | formatter: {Recode.Formatter, []}, 14 | tasks: [ 15 | {Task.SinglePipe, []}, 16 | {Task.PipeFunOne, []}, 17 | {Task.AliasExpansion, []} 18 | ] 19 | ] 20 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/as.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar, do: :bar 3 | end 4 | 5 | defmodule Rename.Baz do 6 | def baz, do: :baz 7 | end 8 | 9 | defmodule Rename.Foo do 10 | alias Rename.Bar, as: Ace 11 | alias Rename.Baz, as: Asdf 12 | 13 | def foo(atom) 14 | 15 | def foo(:a), do: Ace.bar() 16 | 17 | def foo(:b) do 18 | {Ace.bar(), Asdf.baz()} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/bar.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Bla `bar/1` bla 6 | """ 7 | @spec bar(integer()) :: integer 8 | def bar(z) do 9 | z + 10 10 | end 11 | 12 | def zoo(a) do 13 | bar(a) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/baz.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Baz do 2 | import Rename.Bar 3 | 4 | def foo(x) do 5 | bar(x) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/call.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Call do 2 | alias Rename.Bar 3 | alias Rename.Baz 4 | 5 | def foo(x) do 6 | Bar.bar(x) 7 | Bar.bar(x, 5) 8 | Rename.Bar.bar(x) 9 | _ignore = Bar.bar(x) + Rename.Bar.bar(x) 10 | Baz.baz() 11 | Baz.baz(x) 12 | Bar.foo(x) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/capture.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar(x), do: {x, :baz} 3 | end 4 | 5 | defmodule Rename.Foo do 6 | import Rename.Bar 7 | 8 | def foo(list), do: Enum.map(list, &bar/1) 9 | end 10 | 11 | defmodule Rename.FooFoo do 12 | def foofoo(list), do: Enum.map(list, &Rename.Bar.bar/1) 13 | end 14 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/definition.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar, do: :baz 3 | 4 | @spec bar(term()) :: :baz 5 | def bar(1), do: :baz 6 | 7 | def bar(2) do 8 | bar(1) 9 | end 10 | 11 | @spec bar(x, y) :: term() when x: term(), y: term() 12 | def bar(a, b) when b == 5 do 13 | bar(a, b, nil) 14 | end 15 | 16 | def bar(a, b) do 17 | bar(a, b, nil) 18 | end 19 | 20 | defp bar(a, b, c) do 21 | a + b == c 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/import.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar, do: :baz 3 | 4 | def bar(x), do: {x, :baz} 5 | end 6 | 7 | defmodule Rename.Foo do 8 | import Rename.Bar 9 | 10 | def foo(:a), do: bar() 11 | 12 | def foo(:b) do 13 | bar() |> bar() |> List.wrap() 14 | end 15 | 16 | def go(x) do 17 | x 18 | end 19 | end 20 | 21 | defmodule Rename.FooFoo do 22 | import Rename.Bar, only: [bar: 0, bar: 1] 23 | 24 | def foofoo do 25 | bar() |> bar() |> List.wrap() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/import_no_change.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Baz do 2 | def baz, do: :baz 3 | 4 | def baz(x), do: {x, :baz} 5 | end 6 | 7 | defmodule Rename.Foo do 8 | import Rename.Baz 9 | 10 | def foo(:a), do: baz() 11 | 12 | def foo(:b) do 13 | baz() |> baz() |> List.wrap() 14 | end 15 | 16 | def go(x) do 17 | x 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/no_debug_info.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar, do: :baz 3 | 4 | def bar(x), do: {x, :baz} 5 | end 6 | 7 | defmodule Rename.Foo do 8 | import Rename.Bar 9 | 10 | def foo(:a), do: baz() 11 | 12 | def foo(:b) do 13 | baz() |> baz() |> List.wrap() 14 | end 15 | 16 | def go(x) do 17 | x 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/other_definition.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar(x), do: {:baz, x} 3 | end 4 | 5 | defmodule Rename.Foo do 6 | alias Rename.Bar 7 | 8 | def baz, do: :baz 9 | 10 | def baz(1), do: :baz 11 | 12 | def baz(2) do 13 | baz(1) 14 | end 15 | 16 | def baz(x) do 17 | Bar.bar(x) 18 | end 19 | 20 | def baz(a, b) when b == 5 do 21 | baz(a, b, nil) 22 | end 23 | 24 | def baz(a, b) do 25 | baz(a, b, nil) 26 | end 27 | 28 | defp baz(a, b, c) do 29 | a + b == c 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/setup_do.exs: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar, do: :baz 3 | def bar(x), do: {:baz, x} 4 | end 5 | 6 | defmodule Rename.FooTest do 7 | use ExUnit.Case 8 | import Rename.Bar 9 | 10 | setup do 11 | bar() 12 | end 13 | 14 | test "bar" do 15 | bar(5) 16 | assert bar(6) 17 | end 18 | end 19 | 20 | defmodule Rename.BarTest do 21 | use ExUnit.Case 22 | import Rename.Bar 23 | 24 | setup do 25 | x = 5 26 | bar(x) 27 | end 28 | 29 | test "bar" do 30 | bar(5) 31 | assert bar(6) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/use.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def bar, do: :baz 3 | end 4 | 5 | defmodule Rename.Baz do 6 | defmacro __using__(_opts) do 7 | quote do 8 | alias Rename.Bar 9 | end 10 | end 11 | end 12 | 13 | defmodule Rename.Foo do 14 | use Rename.Baz 15 | 16 | def foo(:a), do: Bar.bar() 17 | 18 | def foo(:b) do 19 | Bar.bar() 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/rename/exp/with_arity.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | 4 | def bar(1), do: :baz 5 | 6 | def bar(2) do 7 | bar(1) 8 | end 9 | 10 | def baz(a, b) when b == 5 do 11 | baz(a, b, nil) 12 | end 13 | 14 | def baz(a, b) do 15 | baz(a, b, nil) 16 | end 17 | 18 | defp baz(a, b, c) do 19 | a + b == c 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/as.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :bar 3 | end 4 | 5 | defmodule Rename.Baz do 6 | def baz, do: :baz 7 | end 8 | 9 | defmodule Rename.Foo do 10 | alias Rename.Bar, as: Ace 11 | alias Rename.Baz, as: Asdf 12 | 13 | def foo(atom) 14 | 15 | def foo(:a), do: Ace.baz() 16 | 17 | def foo(:b) do 18 | {Ace.baz(), Asdf.baz()} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/bar.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Bla `baz/1` bla 6 | """ 7 | @spec baz(integer()) :: integer 8 | def baz(z) do 9 | z + 10 10 | end 11 | 12 | def zoo(a) do 13 | baz(a) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/baz.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Baz do 2 | import Rename.Bar 3 | 4 | def foo(x) do 5 | baz(x) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/call.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Call do 2 | alias Rename.Bar 3 | alias Rename.Baz 4 | 5 | def foo(x) do 6 | Bar.baz(x) 7 | Bar.baz(x, 5) 8 | Rename.Bar.baz(x) 9 | _ignore = Bar.baz(x) + Rename.Bar.baz(x) 10 | Baz.baz() 11 | Baz.baz(x) 12 | Bar.foo(x) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/capture.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz(x), do: {x, :baz} 3 | end 4 | 5 | defmodule Rename.Foo do 6 | import Rename.Bar 7 | 8 | def foo(list), do: Enum.map(list, &baz/1) 9 | end 10 | 11 | defmodule Rename.FooFoo do 12 | def foofoo(list), do: Enum.map(list, &Rename.Bar.baz/1) 13 | end 14 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/definition.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | 4 | @spec baz(term()) :: :baz 5 | def baz(1), do: :baz 6 | 7 | def baz(2) do 8 | baz(1) 9 | end 10 | 11 | @spec baz(x, y) :: term() when x: term(), y: term() 12 | def baz(a, b) when b == 5 do 13 | baz(a, b, nil) 14 | end 15 | 16 | def baz(a, b) do 17 | baz(a, b, nil) 18 | end 19 | 20 | defp baz(a, b, c) do 21 | a + b == c 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/import.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | 4 | def baz(x), do: {x, :baz} 5 | end 6 | 7 | defmodule Rename.Foo do 8 | import Rename.Bar 9 | 10 | def foo(:a), do: baz() 11 | 12 | def foo(:b) do 13 | baz() |> baz() |> List.wrap() 14 | end 15 | 16 | def go(x) do 17 | x 18 | end 19 | end 20 | 21 | defmodule Rename.FooFoo do 22 | import Rename.Bar, only: [baz: 0, baz: 1] 23 | 24 | def foofoo do 25 | baz() |> baz() |> List.wrap() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/import_no_change.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Baz do 2 | def baz, do: :baz 3 | 4 | def baz(x), do: {x, :baz} 5 | end 6 | 7 | defmodule Rename.Foo do 8 | import Rename.Baz 9 | 10 | def foo(:a), do: baz() 11 | 12 | def foo(:b) do 13 | baz() |> baz() |> List.wrap() 14 | end 15 | 16 | def go(x) do 17 | x 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/no_debug_info.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | 4 | def baz(x), do: {x, :baz} 5 | end 6 | 7 | defmodule Rename.Foo do 8 | import Rename.Bar 9 | 10 | def foo(:a), do: baz() 11 | 12 | def foo(:b) do 13 | baz() |> baz() |> List.wrap() 14 | end 15 | 16 | def go(x) do 17 | x 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/other_definition.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz(x), do: {:baz, x} 3 | end 4 | 5 | defmodule Rename.Foo do 6 | alias Rename.Bar 7 | 8 | def baz, do: :baz 9 | 10 | def baz(1), do: :baz 11 | 12 | def baz(2) do 13 | baz(1) 14 | end 15 | 16 | def baz(x) do 17 | Bar.baz(x) 18 | end 19 | 20 | def baz(a, b) when b == 5 do 21 | baz(a, b, nil) 22 | end 23 | 24 | def baz(a, b) do 25 | baz(a, b, nil) 26 | end 27 | 28 | defp baz(a, b, c) do 29 | a + b == c 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/setup_do.exs: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | def baz(x), do: {:baz, x} 4 | end 5 | 6 | defmodule Rename.FooTest do 7 | use ExUnit.Case 8 | import Rename.Bar 9 | 10 | setup do 11 | baz() 12 | end 13 | 14 | test "bar" do 15 | baz(5) 16 | assert baz(6) 17 | end 18 | end 19 | 20 | defmodule Rename.BarTest do 21 | use ExUnit.Case 22 | import Rename.Bar 23 | 24 | setup do 25 | x = 5 26 | baz(x) 27 | end 28 | 29 | test "bar" do 30 | baz(5) 31 | assert baz(6) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/use.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | end 4 | 5 | defmodule Rename.Baz do 6 | defmacro __using__(_opts) do 7 | quote do 8 | alias Rename.Bar 9 | end 10 | end 11 | end 12 | 13 | defmodule Rename.Foo do 14 | use Rename.Baz 15 | 16 | def foo(:a), do: Bar.baz() 17 | 18 | def foo(:b) do 19 | Bar.baz() 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/rename/lib/with_arity.ex: -------------------------------------------------------------------------------- 1 | defmodule Rename.Bar do 2 | def baz, do: :baz 3 | 4 | def baz(1), do: :baz 5 | 6 | def baz(2) do 7 | baz(1) 8 | end 9 | 10 | def baz(a, b) when b == 5 do 11 | baz(a, b, nil) 12 | end 13 | 14 | def baz(a, b) do 15 | baz(a, b, nil) 16 | end 17 | 18 | defp baz(a, b, c) do 19 | a + b == c 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/runner/config.exs: -------------------------------------------------------------------------------- 1 | alias Recode.Task 2 | 3 | [ 4 | # Can also be set/reset with "--autocorrect"/"--no-autocorrect". 5 | autocorrect: true, 6 | # With "--dry" no changes will be written to the files. 7 | # Can also be set/reset with "--dry"/"--no-dry". 8 | # If dry is true then verbose is also active. 9 | dry: false, 10 | # Can also be set/reset with "--verbose"/"--no-verbose". 11 | verbose: false, 12 | inputs: ["{config,lib,test}/**/*.{ex,exs}"], 13 | formatters: [Recode.CLIFormatter], 14 | tasks: [ 15 | {Task.SinglePipe, []}, 16 | {Task.PipeFunOne, []}, 17 | {Task.AliasExpansion, []} 18 | ] 19 | ] 20 | -------------------------------------------------------------------------------- /test/fixtures/runner/lib/foo.ex: -------------------------------------------------------------------------------- 1 | defmodule Foo do 2 | alias Bar 3 | alias Alpha 4 | 5 | def foo, do: :foo 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/runner/test/foo_test.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrzndhrn/recode/42042cfbb7dd594c21cd12eb58eb38e6c11f70af/test/fixtures/runner/test/foo_test.exs -------------------------------------------------------------------------------- /test/mix/tasks/recode.gen.config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Recode.Gen.ConfigTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureIO 5 | 6 | alias Mix.Tasks.Recode.Gen.Config 7 | 8 | @config ".recode.exs" 9 | 10 | test "mix recode.gen.config" do 11 | if !File.exists?(@config) do 12 | capture_io(fn -> Config.run([]) end) 13 | config = File.read!(@config) 14 | File.rm!(@config) 15 | assert config == Recode.Config.to_string() 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/mix/tasks/recode.help_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Recode.Gen.HelpTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureIO 5 | 6 | alias Mix.Tasks.Recode.Help 7 | 8 | test "mix recode.help" do 9 | output = 10 | capture_io(fn -> 11 | assert Help.run([]) == :ok 12 | end) 13 | 14 | assert output =~ "Design tasks:" 15 | assert output =~ "Readability tasks:" 16 | assert output =~ "Refactor tasks:" 17 | assert output =~ "Warning tasks:" 18 | assert output =~ ~r/.*#.Checker\s*-/ 19 | assert output =~ ~r/.*#.Corrector\s*-/ 20 | end 21 | 22 | test "mix recode.help Dbg" do 23 | output = 24 | capture_io(fn -> 25 | assert Help.run(["Dbg"]) == :ok 26 | end) 27 | 28 | assert output =~ "Recode.Task.Dbg" 29 | end 30 | 31 | test "mix recode.help FooBar" do 32 | message = """ 33 | The recode task FooBar could not be found. Run "mix recode.help" for a list of recode tasks.\ 34 | """ 35 | 36 | assert_raise Mix.Error, message, fn -> 37 | Help.run(["FooBar"]) 38 | end 39 | end 40 | 41 | test "mix recode.help foo bar" do 42 | message = """ 43 | recode.help does not support this command. \ 44 | For more information run "mix help recode.help"\ 45 | """ 46 | 47 | assert_raise Mix.Error, message, fn -> 48 | Help.run(["foo", "bar"]) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/mix/tasks/recode.update.config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Recode.Gen.UpdateTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureIO 5 | 6 | alias Mix.Tasks.Recode.Update.Config 7 | 8 | @config ".recode.exs" 9 | 10 | test "mix recode.update.config" do 11 | if !File.exists?(@config) do 12 | File.write!(@config, "[verbose: true, tasks: []]") 13 | 14 | capture_io(fn -> Config.run(["--force"]) end) 15 | 16 | config = File.read!(@config) 17 | File.rm!(@config) 18 | 19 | assert config == 20 | Recode.Config.default() 21 | |> Keyword.put(:verbose, true) 22 | |> Recode.Config.to_string() 23 | end 24 | end 25 | 26 | test "mix recode.update.config # with missing .recode.exs" do 27 | message = ~s|config file .recode.exs not found, run "mix recode.gen.config"| 28 | 29 | assert_raise Mix.Error, message, fn -> 30 | Config.run([]) 31 | end 32 | end 33 | 34 | test "mix recode.update.config foo" do 35 | message = ~s|get unknown options: ["--foo"]| 36 | 37 | assert_raise Mix.Error, message, fn -> 38 | Config.run(["--foo"]) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/mix/tasks/recode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.RecodeTest do 2 | use RecodeCase 3 | 4 | import ExUnit.CaptureIO 5 | import Mox 6 | 7 | alias Mix.Tasks 8 | alias Recode.RunnerMock 9 | 10 | setup :verify_on_exit! 11 | 12 | test "mix recode --config test/fixtures/config.exs" do 13 | expect(RunnerMock, :run, fn config -> 14 | assert Keyword.keyword?(config) 15 | {:ok, 0} 16 | end) 17 | 18 | capture_io(fn -> 19 | assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs"])) == :normal 20 | end) 21 | end 22 | 23 | test "mix recode --config test/fixtures/config.exs --dry" do 24 | expect(RunnerMock, :run, fn config -> 25 | assert Keyword.keyword?(config) 26 | assert config[:verbose] == true 27 | {:ok, 0} 28 | end) 29 | 30 | capture_io(fn -> 31 | assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs", "--dry"])) == 32 | :normal 33 | end) 34 | end 35 | 36 | test "mix recode --config test/fixtures/config.exs -" do 37 | expect(RunnerMock, :run, fn config -> 38 | assert Keyword.keyword?(config) 39 | assert config[:inputs] == ["-"] 40 | {:ok, 0} 41 | end) 42 | 43 | capture_io(fn -> 44 | assert catch_exit(Tasks.Recode.run(["--config", "test/fixtures/config.exs", "-"])) == 45 | :normal 46 | end) 47 | end 48 | 49 | test "mix recode file_1.ex file_2.ex" do 50 | expect(RunnerMock, :run, fn config -> 51 | assert Keyword.keyword?(config) 52 | assert config[:inputs] == ["file_1.ex", "file_2.ex"] 53 | {:ok, 0} 54 | end) 55 | 56 | capture_io(fn -> 57 | assert catch_exit( 58 | Tasks.Recode.run([ 59 | "--config", 60 | "test/fixtures/config.exs", 61 | "file_1.ex", 62 | "file_2.ex" 63 | ]) 64 | ) == 65 | :normal 66 | end) 67 | end 68 | 69 | test "mix recode raises exception for unknown config file" do 70 | message = "Config file not found. Run `mix recode.gen.config` to create `.recode.exs`." 71 | 72 | assert_raise Mix.Error, message, fn -> 73 | Tasks.Recode.run(["--config", "priv/no_config.exs"]) 74 | end 75 | end 76 | 77 | test "mix recode raises exception for unknown arg" do 78 | assert_raise OptionParser.ParseError, ~r|unknown-arg : Unknown option|, fn -> 79 | Tasks.Recode.run(["--config", "test/fixtures/config.exs", "--unknown-arg", "inputs", "foo"]) 80 | end 81 | end 82 | 83 | test "mix recode raises exception for missing inputs" do 84 | message = "No sources found" 85 | 86 | assert_raise Mix.Error, message, fn -> 87 | Tasks.Recode.run(["--config", "test/fixtures/config.exs", "no-sources"]) 88 | end 89 | end 90 | 91 | test "mix recode raises exception for an invalid task config" do 92 | message = """ 93 | Invalid config keys [:invalid] for Recode.Task.Dbg found. 94 | Did you want to create a task-specific configuration: 95 | {Recode.Task.Dbg, [autocorrect: false, config: [invalid: :key]]} 96 | """ 97 | 98 | assert_raise Mix.Error, message, fn -> 99 | Tasks.Recode.run(["--config", "test/fixtures/invalid_task_config.exs", "no-sources"]) 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /test/recode/ast_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.ASTTest do 2 | use ExUnit.Case 3 | 4 | alias Recode.AST 5 | 6 | doctest Recode.AST, import: true 7 | 8 | describe "reduce/3" do 9 | test "reduces the AST" do 10 | ast = 11 | Code.string_to_quoted!(""" 12 | defmodule Foo do 13 | def x, do: :y 14 | end 15 | """) 16 | 17 | fun = fn 18 | {form, _meta, _args}, acc when is_atom(form) -> [form | acc] 19 | _ast, acc -> acc 20 | end 21 | 22 | assert AST.reduce(ast, [], fun) == [:x, :def, :__aliases__, :defmodule] 23 | end 24 | 25 | test "compares to Macro.prewalk/3" do 26 | # Macro.prewalk/3 uses Macro.travers/4 27 | ast = 28 | Code.string_to_quoted!(~s''' 29 | defmodule Foo do 30 | @moduledoc """ 31 | The Foo 32 | """ 33 | @data :y 34 | 35 | def x, do: @data 36 | 37 | def rev(list), do: Enum.reverse(list) 38 | end 39 | ''') 40 | 41 | collect = fn ast, acc -> {ast, [ast | acc]} end 42 | {_ast, prewalk} = Macro.prewalk(ast, [], collect) 43 | 44 | collect = fn ast, acc -> [ast | acc] end 45 | assert AST.reduce(ast, [], collect) == prewalk 46 | end 47 | end 48 | 49 | describe "reduce_while/3" do 50 | test "reduces the AST" do 51 | ast = 52 | Code.string_to_quoted!(""" 53 | defmodule Foo do 54 | def x, do: :y 55 | end 56 | """) 57 | 58 | fun = fn 59 | {form, _meta, _args}, acc when is_atom(form) -> {:cont, [form | acc]} 60 | _ast, acc -> {:cont, acc} 61 | end 62 | 63 | assert AST.reduce_while(ast, [], fun) == [:x, :def, :__aliases__, :defmodule] 64 | end 65 | 66 | test "skips parts of the AST" do 67 | ast = 68 | Code.string_to_quoted!(""" 69 | defmodule Foo do 70 | def x, do: :y 71 | end 72 | """) 73 | 74 | fun = fn 75 | {:def, _meta, _args}, acc -> {:skip, acc} 76 | {form, _meta, _args}, acc when is_atom(form) -> {:cont, [form | acc]} 77 | _ast, acc -> {:cont, acc} 78 | end 79 | 80 | assert AST.reduce_while(ast, [], fun) == [:__aliases__, :defmodule] 81 | end 82 | 83 | test "compares to Macro.prewalk/3" do 84 | # Macro.prewalk/3 uses Macro.travers/4 85 | ast = 86 | Code.string_to_quoted!(~s''' 87 | defmodule Foo do 88 | @moduledoc """ 89 | The Foo 90 | """ 91 | @data :y 92 | 93 | def x, do: @data 94 | 95 | def rev(list), do: Enum.reverse(list) 96 | end 97 | ''') 98 | 99 | collect = fn ast, acc -> {ast, [ast | acc]} end 100 | {_ast, prewalk} = Macro.prewalk(ast, [], collect) 101 | 102 | collect = fn ast, acc -> {:cont, [ast | acc]} end 103 | assert AST.reduce_while(ast, [], collect) == prewalk 104 | end 105 | end 106 | 107 | describe "multiline?/1" do 108 | test "with operator" do 109 | assert "x && y" |> Sourceror.parse_string!() |> AST.multiline?() == false 110 | assert "x &&\ny" |> Sourceror.parse_string!() |> AST.multiline?() == true 111 | end 112 | 113 | test "with empty list" do 114 | assert AST.multiline?([]) == false 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/recode/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.ConfigTest do 2 | use ExUnit.Case 3 | 4 | doctest Recode.Config 5 | 6 | import GlobEx.Sigils 7 | 8 | alias Recode.Config 9 | 10 | test "version in Config.default() is equal to the version in mix.exs" do 11 | assert Config.default()[:version] == Mix.Project.config()[:version] 12 | end 13 | 14 | @exclude_tasks [Recode.Task.Format, Recode.Task.Moduledoc, Recode.Task.Tags] 15 | test "all tasks in config" do 16 | tasks = 17 | Config.default() 18 | |> Keyword.get(:tasks) 19 | |> Enum.map(fn {task, _} -> task |> inspect() |> Macro.underscore() end) 20 | 21 | files = 22 | ~g|lib/recode/task/**/*.ex| 23 | |> GlobEx.ls() 24 | |> Enum.map(fn path -> 25 | ~r/^lib.(.*).ex$/ 26 | |> Regex.run(path) 27 | |> Enum.at(1) 28 | end) 29 | 30 | exclude = Enum.map(@exclude_tasks, fn task -> Macro.underscore(task) end) 31 | 32 | missing = files -- tasks 33 | missing = missing -- exclude 34 | missing = Enum.map(missing, fn module -> Macro.camelize(module) end) 35 | 36 | assert Enum.empty?(missing), """ 37 | The default config is missing entries for #{inspect(missing)}. 38 | 39 | You can add these to the `@default_config` in `Recode.Config`. 40 | """ 41 | end 42 | 43 | describe "read/1" do 44 | test "reads config" do 45 | assert {:ok, config} = Config.read("test/fixtures/config.exs") 46 | assert Keyword.keyword?(config) 47 | end 48 | 49 | test "returns an error tuple for a missing config file" do 50 | assert Config.read("foo/bar/conf.exs") == {:error, :not_found} 51 | end 52 | end 53 | 54 | describe "merge/2" do 55 | test "merges configs" do 56 | old = [ 57 | version: "0.0.1", 58 | verbose: true, 59 | tasks: [ 60 | {Recode.Task.EnforceLineLength, []}, 61 | {Recode.Task.Specs, 62 | [ 63 | exclude: ["test/**/*.{ex,exs}", "int_test/**/*.{ex,exs}"], 64 | config: [only: :visible] 65 | ]}, 66 | {Recode.Task.PipeFunOne, [active: true]}, 67 | {MyApp.RecodeTask, []} 68 | ] 69 | ] 70 | 71 | new = [ 72 | version: "0.0.2", 73 | verbose: false, 74 | autocorrect: true, 75 | tasks: [ 76 | {Recode.Task.EnforceLineLength, [active: false]}, 77 | {Recode.Task.PipeFunOne, [active: false]}, 78 | {Recode.Task.Specs, [exclude: "test/**/*.{ex,exs}", config: [only: :visible]]}, 79 | {Recode.Task.FilterCount, []}, 80 | {Recode.Task.SinglePipe, []} 81 | ] 82 | ] 83 | 84 | assert new |> Config.merge(old) |> Enum.sort() == [ 85 | autocorrect: true, 86 | tasks: [ 87 | {MyApp.RecodeTask, []}, 88 | {Recode.Task.EnforceLineLength, [{:active, false}]}, 89 | {Recode.Task.FilterCount, []}, 90 | {Recode.Task.PipeFunOne, [active: true]}, 91 | {Recode.Task.SinglePipe, []}, 92 | {Recode.Task.Specs, 93 | [ 94 | exclude: ["test/**/*.{ex,exs}", "int_test/**/*.{ex,exs}"], 95 | config: [only: :visible] 96 | ]} 97 | ], 98 | verbose: true, 99 | version: "0.0.2" 100 | ] 101 | end 102 | 103 | test "merges task config" do 104 | old = [ 105 | tasks: [ 106 | {Test, config: [a: 1, b: 2, c: 3]} 107 | ] 108 | ] 109 | 110 | new = [ 111 | tasks: [ 112 | {Test, config: [b: 2, c: 9, d: 4]} 113 | ] 114 | ] 115 | 116 | assert Config.merge(new, old) == [ 117 | tasks: [ 118 | {Test, [config: [a: 1, b: 2, c: 3, d: 4]]} 119 | ] 120 | ] 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/recode/formatter_plugin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.FormatterPluginTest do 2 | use RecodeCase, async: false 3 | 4 | import Mox 5 | 6 | alias Recode.FormatterPlugin 7 | alias Recode.RunnerMock 8 | alias Recode.Task.SinglePipe 9 | 10 | setup :verify_on_exit! 11 | 12 | setup do 13 | _erase = :persistent_term.erase({Recode.FormatterPlugin, :config}) 14 | :ok 15 | end 16 | 17 | test "returns features" do 18 | assert FormatterPlugin.features(recode: [tasks: []]) == [extensions: [".ex", ".exs"]] 19 | end 20 | 21 | test "runs with tasks from the .formatter config" do 22 | expect(RunnerMock, :run, fn content, config, path -> 23 | assert content == "code" 24 | assert path == "source.ex" 25 | 26 | assert config == [ 27 | dot_formatter_opts: [locals_without_parens: [foo: 2], plugins: []], 28 | tasks: [{Recode.Task.SinglePipe, []}], 29 | dry: false, 30 | verbose: false, 31 | autocorrect: true, 32 | check: false 33 | ] 34 | 35 | :ok 36 | end) 37 | 38 | dot_formatter_opts = [ 39 | locals_without_parens: [foo: 2], 40 | recode: [tasks: [{SinglePipe, []}]] 41 | ] 42 | 43 | assert FormatterPlugin.format("code", dot_formatter_opts) == :ok 44 | end 45 | 46 | test "removes the FormatterPlugin from plugins" do 47 | expect(RunnerMock, :run, fn content, config, path -> 48 | assert content == "code" 49 | assert path == "source.ex" 50 | 51 | assert config == [ 52 | dot_formatter_opts: [locals_without_parens: [foo: 2], plugins: [FreedomFormatter]], 53 | tasks: [{Recode.Task.SinglePipe, []}], 54 | dry: false, 55 | verbose: false, 56 | autocorrect: true, 57 | check: false 58 | ] 59 | 60 | :ok 61 | end) 62 | 63 | dot_formatter_opts = [ 64 | recode: [tasks: [{SinglePipe, []}]], 65 | locals_without_parens: [foo: 2], 66 | plugins: [FreedomFormatter, Recode.FormatterPlugin] 67 | ] 68 | 69 | assert FormatterPlugin.format("code", dot_formatter_opts) 70 | end 71 | 72 | @tag :tmp_dir 73 | test "formats files", %{tmp_dir: dir} do 74 | File.cd!(dir, fn -> 75 | File.mkdir!("lib") 76 | 77 | File.write!(".recode.exs", """ 78 | [ 79 | autocorrect: true, 80 | dry: false, 81 | verbose: false, 82 | inputs: ["{config,lib,test}/**/*.{ex,exs}"], 83 | formatters: [Recode.CLIFormatter], 84 | tasks: [ 85 | {Recode.Task.SinglePipe, []}, 86 | {Recode.Task.PipeFunOne, []}, 87 | {Recode.Task.AliasExpansion, []} 88 | ] 89 | ] 90 | """) 91 | 92 | path = "lib/foo.ex" 93 | 94 | code = """ 95 | defmodule Foo do 96 | def foo, do: :foo 97 | end 98 | """ 99 | 100 | File.write!(path, code) 101 | 102 | FormatterPlugin.features([]) 103 | 104 | assert FormatterPlugin.format(code, file: path, plugins: [Recode.FormatterPlugin]) == """ 105 | defmodule Foo do 106 | def foo, do: :foo 107 | end 108 | """ 109 | end) 110 | end 111 | 112 | test "raises an error for missing tasks key" do 113 | assert_raise Mix.Error, "No `:tasks` key found in configuration.", fn -> 114 | FormatterPlugin.format("", recode: []) 115 | end 116 | end 117 | 118 | test "raises an error for missing config" do 119 | message = """ 120 | No configuration for `Recode.FormatterPlugin` found. Run `mix recode.gen.config` \ 121 | to create a config file or add config in `.formatter.exs` under the key `:recode`. 122 | """ 123 | 124 | assert_raise Mix.Error, message, fn -> 125 | FormatterPlugin.format("", []) 126 | end 127 | end 128 | 129 | @tag :tmp_dir 130 | test "throws an exception for an outdated config", %{tmp_dir: dir} do 131 | File.cd!(dir, fn -> 132 | File.write!(".recode.exs", """ 133 | [ 134 | version: "0.0.0", 135 | tasks: [ {Recode.Task.SinglePipe, []} ] 136 | ] 137 | """) 138 | 139 | message = "The config is out of date. Run `mix recode.update.config` to update." 140 | 141 | assert_raise Mix.Error, message, fn -> FormatterPlugin.format("", []) end 142 | end) 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /test/recode/formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.FormatterTest do 2 | use ExUnit.Case 3 | 4 | doctest Recode.Formatter 5 | end 6 | -------------------------------------------------------------------------------- /test/recode/issue_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.IssueTest do 2 | use ExUnit.Case 3 | 4 | doctest Recode.Issue 5 | end 6 | -------------------------------------------------------------------------------- /test/recode/task/alias_expansion_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.AliasExapnasionTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.AliasExpansion 5 | 6 | describe "run/1" do 7 | test "expands aliases" do 8 | code = """ 9 | defmodule Mod do 10 | alias Alpha.{Beta, Charlie} 11 | alias Delta.{Foxtrot, Golf} 12 | 13 | def foo, do: :foo 14 | end 15 | """ 16 | 17 | expected = """ 18 | defmodule Mod do 19 | alias Alpha.Beta 20 | alias Alpha.Charlie 21 | alias Delta.Foxtrot 22 | alias Delta.Golf 23 | 24 | def foo, do: :foo 25 | end 26 | """ 27 | 28 | code 29 | |> run_task(AliasExpansion, autocorrect: true) 30 | |> assert_code(expected) 31 | end 32 | 33 | test "expands aliases keeps newline" do 34 | code = """ 35 | defmodule Mod do 36 | alias Alpha.{Beta, Charlie} 37 | 38 | alias Delta.{Foxtrot, Golf} 39 | 40 | def foo, do: :foo 41 | end 42 | """ 43 | 44 | expected = """ 45 | defmodule Mod do 46 | alias Alpha.Beta 47 | alias Alpha.Charlie 48 | 49 | alias Delta.Foxtrot 50 | alias Delta.Golf 51 | 52 | def foo, do: :foo 53 | end 54 | """ 55 | 56 | code 57 | |> run_task(AliasExpansion, autocorrect: true) 58 | |> assert_code(expected) 59 | end 60 | 61 | test "expands aliases with with comments" do 62 | code = """ 63 | defmodule Mod do 64 | # a comment 65 | alias Bar 66 | alias Foo.{Zumsel, Baz} 67 | 68 | def zoo, do: :zoo 69 | end 70 | """ 71 | 72 | expected = """ 73 | defmodule Mod do 74 | # a comment 75 | alias Bar 76 | alias Foo.Zumsel 77 | alias Foo.Baz 78 | 79 | def zoo, do: :zoo 80 | end 81 | """ 82 | 83 | code 84 | |> run_task(AliasExpansion, autocorrect: true) 85 | |> assert_code(expected) 86 | end 87 | 88 | test "expands long aliases" do 89 | code = """ 90 | defmodule Mod do 91 | alias Alpha.Beta.Charlie.{Delta, Foxtrot, Foxtrot.Golf} 92 | 93 | def foo, do: :foo 94 | end 95 | """ 96 | 97 | expected = """ 98 | defmodule Mod do 99 | alias Alpha.Beta.Charlie.Delta 100 | alias Alpha.Beta.Charlie.Foxtrot 101 | alias Alpha.Beta.Charlie.Foxtrot.Golf 102 | 103 | def foo, do: :foo 104 | end 105 | """ 106 | 107 | code 108 | |> run_task(AliasExpansion, autocorrect: true) 109 | |> assert_code(expected) 110 | end 111 | 112 | test "expands aliases with __MODULE__" do 113 | code = """ 114 | defmodule Mod do 115 | alias __MODULE__.{Beta, Alpha} 116 | alias __MODULE__.Delta.{Foxtrot, Golf, Golf.Hotel} 117 | 118 | def foo, do: :foo 119 | end 120 | """ 121 | 122 | expected = """ 123 | defmodule Mod do 124 | alias __MODULE__.Beta 125 | alias __MODULE__.Alpha 126 | alias __MODULE__.Delta.Foxtrot 127 | alias __MODULE__.Delta.Golf 128 | alias __MODULE__.Delta.Golf.Hotel 129 | 130 | def foo, do: :foo 131 | end 132 | """ 133 | 134 | code 135 | |> run_task(AliasExpansion, autocorrect: true) 136 | |> assert_code(expected) 137 | end 138 | 139 | test "keeps the code as it is" do 140 | """ 141 | defmodule Mod do 142 | alias Beta 143 | alias Alpha 144 | end 145 | """ 146 | |> run_task(AliasExpansion, autocorrect: true) 147 | |> refute_update() 148 | end 149 | 150 | test "keeps the code as it is with __MODULE__" do 151 | """ 152 | defmodule Mod do 153 | alias __MODULE__.Beta 154 | alias __MODULE__.Alpha.Delta 155 | end 156 | """ 157 | |> run_task(AliasExpansion, autocorrect: true) 158 | |> refute_update() 159 | end 160 | 161 | test "reports no issues" do 162 | """ 163 | defmodule Mod do 164 | alias Beta 165 | alias Alpha 166 | end 167 | """ 168 | |> run_task(AliasExpansion, autocorrect: false) 169 | |> refute_issues() 170 | end 171 | 172 | test "reports an issue" do 173 | """ 174 | defmodule Mod do 175 | alias Foo.{Beta, Alpha} 176 | end 177 | """ 178 | |> run_task(AliasExpansion, autocorrect: false) 179 | |> assert_issue_with(reporter: AliasExpansion) 180 | end 181 | 182 | test "reports an issue for aliases with __MODULE__" do 183 | """ 184 | defmodule Mod do 185 | alias __MODULE__.{Beta, Alpha} 186 | end 187 | """ 188 | |> run_task(AliasExpansion, autocorrect: false) 189 | |> assert_issue_with(reporter: AliasExpansion) 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /test/recode/task/format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.FormatTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.Format 5 | alias Rewrite.Source 6 | 7 | test "keeps formatted code" do 8 | """ 9 | defmodule MyModule do 10 | def foo, do: :foo 11 | end 12 | """ 13 | |> run_task(Format, autocorrect: true) 14 | |> refute_update() 15 | end 16 | 17 | test "formats the code" do 18 | code = """ 19 | defmodule MyModule do 20 | 21 | def foo, do: :foo 22 | end 23 | """ 24 | 25 | expected = """ 26 | defmodule MyModule do 27 | def foo, do: :foo 28 | end 29 | """ 30 | 31 | code 32 | |> run_task(Format, autocorrect: true) 33 | |> assert_code(expected) 34 | end 35 | 36 | test "formats the code and ignores the Recode.FormatterPlugin" do 37 | code = """ 38 | defmodule MyModule do 39 | import Bar 40 | 41 | def foo( x ) do 42 | bar x 43 | end 44 | end 45 | """ 46 | 47 | expected = """ 48 | defmodule MyModule do 49 | import Bar 50 | 51 | def foo(x) do 52 | bar x 53 | end 54 | end 55 | """ 56 | 57 | source = 58 | code 59 | |> source() 60 | |> Source.Ex.put_formatter_opts( 61 | plugins: [Recode.FormatterPlugin], 62 | locals_without_parens: [bar: 1] 63 | ) 64 | 65 | source 66 | |> run_task(Format, autocorrect: true) 67 | |> assert_code(expected) 68 | end 69 | 70 | test "formats the code with a plugin" do 71 | code = """ 72 | defmodule MyModule do 73 | def foo( x ), do: {:foo, x} 74 | end 75 | """ 76 | 77 | expected = """ 78 | defmodule MyModule do 79 | def foo(x) do 80 | {:foo, x} 81 | end 82 | end 83 | """ 84 | 85 | source = 86 | code 87 | |> source() 88 | |> Source.Ex.put_formatter_opts(plugins: [FakePlugin]) 89 | 90 | source 91 | |> run_task(Format, autocorrect: true) 92 | |> assert_code(expected) 93 | end 94 | 95 | test "formats an empty string" do 96 | code = " " 97 | expected = "\n" 98 | 99 | code 100 | |> run_task(Format, autocorrect: true) 101 | |> assert_code(expected) 102 | end 103 | 104 | test "reports no issues" do 105 | code = """ 106 | defmodule MyModule do 107 | def foo, do: :foo 108 | end 109 | """ 110 | 111 | code 112 | |> run_task(Format, autocorrect: false) 113 | |> refute_issues() 114 | end 115 | 116 | test "reports an issue" do 117 | code = """ 118 | defmodule MyModule do 119 | 120 | def foo, do: :foo 121 | end 122 | """ 123 | 124 | code 125 | |> run_task(Format, autocorrect: false) 126 | |> assert_issue() 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/recode/task/io_inspect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.IOInspectTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.IOInspect 5 | 6 | describe "run/1" do 7 | # 8 | # cases NOT changing code 9 | # 10 | 11 | test "keeps code" do 12 | """ 13 | def foo(x) do 14 | {:ok, x} 15 | end 16 | """ 17 | |> run_task(IOInspect, autocorrect: true) 18 | |> refute_update() 19 | end 20 | 21 | # 22 | # cases changing code 23 | # 24 | 25 | test "removes a IO.inspect call" do 26 | code = """ 27 | def foo(x) do 28 | IO.inspect(x) 29 | IO.inspect(x, op: :po) 30 | {:ok, x} 31 | end 32 | """ 33 | 34 | expected = """ 35 | def foo(x) do 36 | {:ok, x} 37 | end 38 | """ 39 | 40 | code 41 | |> run_task(IOInspect, autocorrect: true) 42 | |> assert_code(expected) 43 | end 44 | 45 | test "removes piping into IO.inspect" do 46 | code = """ 47 | def foo(x) do 48 | {:ok, x} |> IO.inspect() 49 | end 50 | """ 51 | 52 | expected = """ 53 | def foo(x) do 54 | {:ok, x} 55 | end 56 | """ 57 | 58 | code 59 | |> run_task(IOInspect, autocorrect: true) 60 | |> assert_code(expected) 61 | end 62 | 63 | test "removes piping into IO.inspect inside op" do 64 | code = """ 65 | def foo(x, y) do 66 | x + y |> IO.inspect() 67 | end 68 | """ 69 | 70 | expected = """ 71 | def foo(x, y) do 72 | x + y 73 | end 74 | """ 75 | 76 | code 77 | |> run_task(IOInspect, autocorrect: true) 78 | |> assert_code(expected) 79 | end 80 | 81 | test "removes piping into IO.inspect at the end" do 82 | code = """ 83 | def foo(x) do 84 | x 85 | |> alpha() 86 | |> bravo() 87 | |> IO.inspect() 88 | end 89 | """ 90 | 91 | expected = """ 92 | def foo(x) do 93 | x 94 | |> alpha() 95 | |> bravo() 96 | end 97 | """ 98 | 99 | code 100 | |> run_task(IOInspect, autocorrect: true) 101 | |> assert_code(expected) 102 | end 103 | 104 | test "removes piping into IO.inspect inside of a pipe" do 105 | code = """ 106 | def foo(x) do 107 | x 108 | |> alpha() 109 | |> bravo() 110 | |> IO.inspect() 111 | |> charlie() 112 | |> delta() 113 | end 114 | """ 115 | 116 | expected = """ 117 | def foo(x) do 118 | x 119 | |> alpha() 120 | |> bravo() 121 | |> charlie() 122 | |> delta() 123 | end 124 | """ 125 | 126 | code 127 | |> run_task(IOInspect, autocorrect: true) 128 | |> assert_code(expected) 129 | end 130 | 131 | test "removes code when ref IO.inspect" do 132 | code = """ 133 | def foo(x) do 134 | Enum.each(x, &IO.inspect/1) 135 | {:ok, x} 136 | end 137 | """ 138 | 139 | expected = """ 140 | def foo(x) do 141 | {:ok, x} 142 | end 143 | """ 144 | 145 | code 146 | |> run_task(IOInspect, autocorrect: true) 147 | |> assert_code(expected) 148 | end 149 | 150 | test "removes code when ref IO.inspect in pipe" do 151 | code = """ 152 | def foo(x) do 153 | x 154 | |> bar() 155 | |> Enum.map(&IO.inspect/1) 156 | |> baz() 157 | end 158 | """ 159 | 160 | expected = """ 161 | def foo(x) do 162 | x 163 | |> bar() 164 | |> baz() 165 | end 166 | """ 167 | 168 | code 169 | |> run_task(IOInspect, autocorrect: true) 170 | |> assert_code(expected) 171 | end 172 | 173 | # 174 | # cases NOT raising issues 175 | # 176 | 177 | test "does not trigger" do 178 | """ 179 | def foo(x) do 180 | {:ok, x} 181 | end 182 | """ 183 | |> run_task(IOInspect, autocorrect: false) 184 | |> refute_issues() 185 | end 186 | 187 | # 188 | # cases raising issues 189 | # 190 | 191 | test "reports issue for IO.inspect call" do 192 | """ 193 | def foo(x) do 194 | IO.inspect(x) 195 | {:ok, x} 196 | end 197 | """ 198 | |> run_task(IOInspect, autocorrect: false) 199 | |> assert_issue_with(reporter: IOInspect, line: 2) 200 | end 201 | 202 | test "reports issue when piping into IO.inspect" do 203 | """ 204 | def foo(x) do 205 | {:ok, x} |> IO.inspect() 206 | end 207 | """ 208 | |> run_task(IOInspect, autocorrect: false) 209 | |> assert_issue_with(reporter: IOInspect, line: 2) 210 | end 211 | 212 | test "reports issue when piping into IO.inspect at the end" do 213 | """ 214 | def foo(x) do 215 | x 216 | |> Enum.reverse() 217 | |> bar() 218 | |> IO.inspect() 219 | end 220 | """ 221 | |> run_task(IOInspect, autocorrect: false) 222 | |> assert_issue_with(reporter: IOInspect, line: 5) 223 | end 224 | 225 | test "reports issue when piping into IO.inspect inside of a pipe" do 226 | """ 227 | def foo(x) do 228 | x 229 | |> Enum.reverse() 230 | |> IO.inspect() 231 | |> bar() 232 | end 233 | """ 234 | |> run_task(IOInspect, autocorrect: false) 235 | |> assert_issue_with(reporter: IOInspect, line: 4, column: 6) 236 | end 237 | 238 | test "reports issue when ref IO.inspect" do 239 | """ 240 | def foo(x) do 241 | Enum.each(x, &IO.inspect/1) 242 | {:ok, x} 243 | end 244 | """ 245 | |> run_task(IOInspect, autocorrect: false) 246 | |> assert_issue_with(reporter: IOInspect, line: 2, column: 16) 247 | end 248 | 249 | test "reports issue when ref IO.inspect in pipe" do 250 | """ 251 | def foo(x) do 252 | x 253 | |> bar() 254 | |> Enum.map(&IO.inspect/1) 255 | |> baz() 256 | end 257 | """ 258 | |> run_task(IOInspect, autocorrect: false) 259 | |> assert_issue_with(reporter: IOInspect, line: 4) 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /test/recode/task/locals_without_parens.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.LocalsWithoutParensTest do 2 | use RecodeCase, async: false 3 | 4 | alias Recode.Task.LocalsWithoutParens 5 | 6 | @moduletag :tmp_dir 7 | 8 | setup context do 9 | File.write("#{context.tmp_dir}/.formatter.exs", """ 10 | [ 11 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 12 | locals_without_parens: [foo: 1, bar: 2] 13 | ] 14 | """) 15 | 16 | {:ok, context} 17 | end 18 | 19 | test "remove parens", %{tmp_dir: tmp_dir} do 20 | File.cd!(tmp_dir, fn -> 21 | code = """ 22 | x = foo(bar(y)) 23 | bar(y, x) 24 | """ 25 | 26 | expected = """ 27 | x = foo bar(y) 28 | bar y, x 29 | """ 30 | 31 | code 32 | |> run_task(RemoveParens, autocorrect: true) 33 | |> assert_code(expected) 34 | end) 35 | end 36 | 37 | test "adds issue", %{tmp_dir: tmp_dir} do 38 | File.cd!(tmp_dir, fn -> 39 | """ 40 | x = foo(bar) 41 | """ 42 | |> run_task(RemoveParens, autocorrect: false) 43 | |> assert_issue() 44 | end) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/recode/task/nesting_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.NestingTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.Nesting 5 | 6 | describe "run/1" do 7 | # 8 | # cases NOT raising issues 9 | # 10 | 11 | test "does not trigger" do 12 | """ 13 | defmodule Sample do 14 | def fun(x) do 15 | if x do 16 | :ok 17 | else 18 | :error 19 | end 20 | end 21 | end 22 | """ 23 | |> run_task(Nesting) 24 | |> refute_issues() 25 | end 26 | 27 | test "does not trigger for multiple nested expressions" do 28 | """ 29 | defmodule Sample do 30 | def fun(x, y, z) do 31 | a = if x do 32 | case y do 33 | true -> Bar.bar(y) 34 | false -> Bar.bar(x) 35 | end 36 | else 37 | :y 38 | end 39 | 40 | b = if x do 41 | if y == 42 do 42 | Bar.bar(y) 43 | else 44 | Bar.bar(x) 45 | end 46 | else 47 | :y 48 | end 49 | 50 | {a, b} 51 | end 52 | end 53 | """ 54 | |> run_task(Nesting) 55 | |> refute_issues() 56 | end 57 | 58 | test "trigers not when max_depth is great enough" do 59 | """ 60 | defmodule Sample do 61 | def function(x, y) do 62 | if x do 63 | if y do 64 | case x do 65 | 0 -> nil 66 | 1 -> foo(x) 67 | end 68 | end 69 | end 70 | end 71 | end 72 | """ 73 | |> run_task(Nesting, max_depth: 3) 74 | |> refute_issues() 75 | end 76 | 77 | # 78 | # cases raising issues 79 | # 80 | 81 | test "trigers when max depth is exceeded" do 82 | """ 83 | defmodule Sample do 84 | def something(x, y) do 85 | if x do 86 | if y do 87 | case x do 88 | 0 -> nil 89 | 1 -> foo(x) 90 | end 91 | end 92 | end 93 | end 94 | end 95 | """ 96 | |> run_task(Nesting) 97 | |> assert_issue_with(reporter: Nesting, line: 5) 98 | end 99 | 100 | test "trigers once when max depth is exceeded by more then one step" do 101 | """ 102 | defmodule Sample do 103 | def something(x, y) do 104 | if x do 105 | if y do 106 | case x do 107 | 0 -> 108 | cond do 109 | y == 5 -> boo(y) 110 | y > 5 -> foo(y) 111 | end 112 | 1 -> 113 | foo(x) 114 | end 115 | end 116 | end 117 | end 118 | end 119 | """ 120 | |> run_task(Nesting) 121 | |> assert_issue_with(reporter: Nesting, line: 5) 122 | end 123 | 124 | test "trigers with a greate max_depth" do 125 | """ 126 | defmodule Sample do 127 | def something(x, y) do 128 | if x do 129 | if y do 130 | case x do 131 | 0 -> 132 | cond do 133 | y == 5 -> boo(y) 134 | y > 5 -> foo(y) 135 | end 136 | 1 -> 137 | foo(x) 138 | end 139 | end 140 | end 141 | end 142 | end 143 | """ 144 | |> run_task(Nesting, max_depth: 3) 145 | |> assert_issue_with(reporter: Nesting, line: 7) 146 | end 147 | 148 | test "trigers twice when max depth is exceeded twice" do 149 | """ 150 | defmodule Sample do 151 | def something(x, y) do 152 | if x do 153 | if y do 154 | case x do 155 | 0 -> nil 156 | 1 -> foo(x) 157 | end 158 | end 159 | end 160 | 161 | if x do 162 | if y do 163 | case x do 164 | 0 -> nil 165 | 1 -> foo(x) 166 | end 167 | end 168 | end 169 | end 170 | end 171 | """ 172 | |> run_task(Nesting) 173 | |> assert_issues(2) 174 | end 175 | 176 | test "trigers in an anonymous function" do 177 | """ 178 | defmodule Sample do 179 | def something(x) do 180 | Enum.each(x, fn y -> 181 | Enum.each(x, fn z -> 182 | Enum.each(z, fn i -> foo(i) end) 183 | end) 184 | end) 185 | end 186 | end 187 | """ 188 | |> run_task(Nesting) 189 | |> assert_issue_with(reporter: Nesting, line: 5) 190 | end 191 | end 192 | 193 | describe "init/1" do 194 | test "sets default max_depth" do 195 | assert Nesting.init([]) == {:ok, max_depth: 2} 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /test/recode/task/pipe_fun_one_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.PipeFunOneTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.PipeFunOne 5 | 6 | describe "run/1" do 7 | test "adds parentheses" do 8 | code = """ 9 | def foo(arg) do 10 | arg 11 | |> bar 12 | |> baz(:baz) 13 | |> zoo() 14 | end 15 | """ 16 | 17 | expected = """ 18 | def foo(arg) do 19 | arg 20 | |> bar() 21 | |> baz(:baz) 22 | |> zoo() 23 | end 24 | """ 25 | 26 | code 27 | |> run_task(PipeFunOne, autocorrect: true) 28 | |> assert_code(expected) 29 | end 30 | 31 | test "adds parenthes in single pipe" do 32 | code = """ 33 | a |> Foo.bar 34 | """ 35 | 36 | expected = """ 37 | a |> Foo.bar() 38 | """ 39 | 40 | code 41 | |> run_task(PipeFunOne, autocorrect: true) 42 | |> assert_code(expected) 43 | end 44 | 45 | test "reports issue" do 46 | """ 47 | def foo(arg) do 48 | arg 49 | |> bar 50 | |> baz(:baz) 51 | |> zoo() 52 | end 53 | """ 54 | |> run_task(PipeFunOne, autocorrect: false) 55 | |> assert_issue_with(reporter: PipeFunOne) 56 | end 57 | 58 | test "reports issue for single pipe" do 59 | """ 60 | def foo(arg) do 61 | arg |> Foo.bar 62 | end 63 | """ 64 | |> run_task(PipeFunOne, autocorrect: false) 65 | |> assert_issue() 66 | end 67 | 68 | test "reports no issue" do 69 | """ 70 | def foo(arg) do 71 | arg 72 | |> bar() 73 | |> baz(:baz) 74 | |> zoo() 75 | end 76 | """ 77 | |> run_task(PipeFunOne, autocorrect: false) 78 | |> refute_issues() 79 | end 80 | end 81 | 82 | test "keeps code when |> is not used as pipe operator" do 83 | code = """ 84 | defmodule Foo do 85 | defmacro a |> b do 86 | a |> Bar.foo 87 | 88 | fun = fn {x, pos}, acc -> 89 | Macro.pipe(acc, x, pos) 90 | end 91 | 92 | :lists.foldl(fun, left, Macro.unpipe(right)) 93 | end 94 | 95 | defdelegate left |> right, to: Bar 96 | end 97 | """ 98 | 99 | expected = """ 100 | defmodule Foo do 101 | defmacro a |> b do 102 | a |> Bar.foo() 103 | 104 | fun = fn {x, pos}, acc -> 105 | Macro.pipe(acc, x, pos) 106 | end 107 | 108 | :lists.foldl(fun, left, Macro.unpipe(right)) 109 | end 110 | 111 | defdelegate left |> right, to: Bar 112 | end 113 | """ 114 | 115 | code 116 | |> run_task(PipeFunOne, autocorrect: true) 117 | |> assert_code(expected) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/recode/task/single_pipe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.SinglePipeTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.SinglePipe 5 | 6 | test "fix single pipe" do 7 | code = """ 8 | x |> foo() 9 | """ 10 | 11 | expected = """ 12 | foo(x) 13 | """ 14 | 15 | code 16 | |> run_task(SinglePipe, autocorrect: true) 17 | |> assert_code(expected) 18 | end 19 | 20 | test "fixes single pipes" do 21 | code = """ 22 | def fixme(arg) do 23 | arg |> zoo() 24 | arg |> zoo(:tiger) 25 | one() |> two() 26 | one() |> two(:b) 27 | "" |> String.split() 28 | "go go" |> String.split() 29 | 1 |> to_string() 30 | :anton |> Foo.bar() 31 | [1, 2, 3] |> Enum.map(fn x -> x * x end) 32 | %{a: 1, b: 2} |> Enum.map(fn {k, v} -> {k, v + 1} end) 33 | end 34 | """ 35 | 36 | expected = """ 37 | def fixme(arg) do 38 | zoo(arg) 39 | zoo(arg, :tiger) 40 | two(one()) 41 | two(one(), :b) 42 | String.split("") 43 | String.split("go go") 44 | to_string(1) 45 | Foo.bar(:anton) 46 | Enum.map([1, 2, 3], fn x -> x * x end) 47 | Enum.map(%{a: 1, b: 2}, fn {k, v} -> {k, v + 1} end) 48 | end 49 | """ 50 | 51 | code 52 | |> run_task(SinglePipe, autocorrect: true) 53 | |> assert_code(expected) 54 | end 55 | 56 | test "fixes single pipes with heredoc" do 57 | code = """ 58 | def hello do 59 | \"\"\" 60 | world 61 | \"\"\" 62 | |> String.split() 63 | 64 | \"\"\" 65 | bar 66 | \"\"\" 67 | |> foo("baz") 68 | 69 | end 70 | """ 71 | 72 | expected = """ 73 | def hello do 74 | String.split(\"\"\" 75 | world 76 | \"\"\") 77 | 78 | foo( 79 | \"\"\" 80 | bar 81 | \"\"\", 82 | "baz" 83 | ) 84 | end 85 | """ 86 | 87 | code 88 | |> run_task(SinglePipe, autocorrect: true) 89 | |> assert_code(expected) 90 | end 91 | 92 | test "does not expands single pipes that starts with a none zero fun" do 93 | """ 94 | def fixme(arg) do 95 | foo(arg) |> zoo() 96 | foo(arg, :animal) |> zoo(:tiger) 97 | one(:a) |> two() 98 | end 99 | """ 100 | |> run_task(SinglePipe, autocorrect: true) 101 | |> refute_update() 102 | end 103 | 104 | test "keeps pipes" do 105 | """ 106 | def ok(arg) do 107 | arg 108 | |> bar() 109 | |> baz(:baz) 110 | end 111 | """ 112 | |> run_task(SinglePipe, autocorrect: true) 113 | |> refute_update() 114 | end 115 | 116 | test "keeps pipes (length 3)" do 117 | """ 118 | def ok(arg) do 119 | arg 120 | |> bar() 121 | |> baz(:baz) 122 | |> bing() 123 | end 124 | """ 125 | |> run_task(SinglePipe, autocorrect: true) 126 | |> refute_update() 127 | end 128 | 129 | test "keeps pipes with tap" do 130 | """ 131 | def ok(arg) do 132 | arg 133 | |> bar() 134 | |> tap(fn x -> IO.puts(x) end) 135 | |> baz(:baz) 136 | end 137 | """ 138 | |> run_task(SinglePipe, autocorrect: true) 139 | |> refute_update() 140 | end 141 | 142 | test "keeps pipes with then" do 143 | """ 144 | def ok(arg) do 145 | arg 146 | |> bar() 147 | |> then(fn x -> Enum.reverse(x) end) 148 | end 149 | """ 150 | |> run_task(SinglePipe, autocorrect: true) 151 | |> refute_update() 152 | end 153 | 154 | test "reports single pipes violation" do 155 | """ 156 | def fixme(arg) do 157 | arg |> zoo() 158 | arg |> zoo(:tiger) 159 | end 160 | """ 161 | |> run_task(SinglePipe, autocorrect: false) 162 | |> assert_issues(2) 163 | end 164 | 165 | test "keeps |> when not used as pipe operator" do 166 | code = """ 167 | defmodule Foo do 168 | defmacro a |> b do 169 | a |> Bar.foo() 170 | 171 | fun = fn {x, pos}, acc -> 172 | Macro.pipe(acc, x, pos) 173 | end 174 | 175 | :lists.foldl(fun, left, Macro.unpipe(right)) 176 | end 177 | 178 | defdelegate left |> right, to: Bar 179 | end 180 | """ 181 | 182 | expected = """ 183 | defmodule Foo do 184 | defmacro a |> b do 185 | Bar.foo(a) 186 | 187 | fun = fn {x, pos}, acc -> 188 | Macro.pipe(acc, x, pos) 189 | end 190 | 191 | :lists.foldl(fun, left, Macro.unpipe(right)) 192 | end 193 | 194 | defdelegate left |> right, to: Bar 195 | end 196 | """ 197 | 198 | code 199 | |> run_task(SinglePipe, autocorrect: true) 200 | |> assert_code(expected) 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /test/recode/task/specs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.SpecsTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.Specs 5 | 6 | test "reports missing specs" do 7 | code = """ 8 | defmodule Bar do 9 | def bar(x) do 10 | {:bar, x} 11 | end 12 | 13 | def foo(y) do 14 | bar(y) 15 | end 16 | end 17 | """ 18 | 19 | code 20 | |> run_task(Specs) 21 | |> assert_issues(2) 22 | 23 | code 24 | |> run_task(Specs, only: :public) 25 | |> assert_issues(2) 26 | 27 | code 28 | |> run_task(Specs, only: :visible) 29 | |> assert_issues(2) 30 | end 31 | 32 | test "reports missing specs - with private function" do 33 | code = """ 34 | defmodule Bar do 35 | defp bar(x) do 36 | {:bar, x} 37 | end 38 | 39 | def foo(y) do 40 | bar(y) 41 | end 42 | end 43 | """ 44 | 45 | code 46 | |> run_task(Specs) 47 | |> assert_issues(2) 48 | 49 | code 50 | |> run_task(Specs, only: :public) 51 | |> assert_issues(1) 52 | 53 | code 54 | |> run_task(Specs, only: :visible) 55 | |> assert_issues(1) 56 | end 57 | 58 | test "reports missing specs - with private and invisible function" do 59 | code = """ 60 | defmodule Bar do 61 | defp bar(x) do 62 | {:bar, x, baz()} 63 | end 64 | 65 | @doc false 66 | def baz, do: :baz 67 | 68 | def foo(y) do 69 | bar(y) 70 | end 71 | end 72 | """ 73 | 74 | code 75 | |> run_task(Specs) 76 | |> assert_issues(3) 77 | 78 | code 79 | |> run_task(Specs, only: :public) 80 | |> assert_issues(2) 81 | 82 | code 83 | |> run_task(Specs, only: :visible) 84 | |> assert_issues(1) 85 | end 86 | 87 | test "reports missing specs - with private function and invisible module" do 88 | code = """ 89 | defmodule Bar do 90 | @moduledoc false 91 | 92 | defp bar(x) do 93 | {:bar, x, baz()} 94 | end 95 | 96 | def foo(y) do 97 | bar(y) 98 | end 99 | end 100 | """ 101 | 102 | code 103 | |> run_task(Specs) 104 | |> assert_issues(2) 105 | 106 | code 107 | |> run_task(Specs, only: :public) 108 | |> assert_issues(1) 109 | 110 | code 111 | |> run_task(Specs, only: :visible) 112 | |> refute_issues() 113 | end 114 | 115 | test "reports nothing when specs are available" do 116 | code = """ 117 | defmodule Bar do 118 | @spec bar(term()) :: integer() 119 | defp bar(x) do 120 | {:bar, x, baz()} 121 | end 122 | 123 | @spec foo(integer()) :: boolean() 124 | def foo(nil), do: nil 125 | 126 | def foo(y) do 127 | bar(y) 128 | end 129 | end 130 | """ 131 | 132 | code 133 | |> run_task(Specs) 134 | |> refute_issues() 135 | 136 | code 137 | |> run_task(Specs, only: :public) 138 | |> refute_issues() 139 | 140 | code 141 | |> run_task(Specs, only: :visible) 142 | |> refute_issues() 143 | end 144 | 145 | test "reports nothing for macros when macros: false is set" do 146 | code = """ 147 | defmodule Bar do 148 | defmacro bar(x) do 149 | quote do 150 | unquote(x) 151 | end 152 | end 153 | end 154 | """ 155 | 156 | code 157 | |> run_task(Specs) 158 | |> refute_issues() 159 | end 160 | 161 | test "reports issues for macros when macros: true is set" do 162 | code = """ 163 | defmodule Bar do 164 | defmacro bar(x) do 165 | quote do 166 | unquote(x) 167 | end 168 | end 169 | end 170 | """ 171 | 172 | code 173 | |> run_task(Specs, macros: true) 174 | |> assert_issue_with(reporter: Specs) 175 | end 176 | 177 | test "reports issues for definitions inside quotes" do 178 | code = """ 179 | defmodule Bar do 180 | @spec bar(atom()) :: Macro.t() 181 | defmacro bar(x) do 182 | quote do 183 | def unquote(x) do 184 | unquote(x) 185 | end 186 | end 187 | end 188 | end 189 | """ 190 | 191 | code 192 | |> run_task(Specs, macros: true) 193 | |> assert_issue_with(reporter: Specs) 194 | end 195 | 196 | test "reports no issues for macro __using__" do 197 | code = """ 198 | defmodule MyModule do 199 | defmacro __using__(_opts) do 200 | quote do 201 | import MyModule.Foo 202 | end 203 | end 204 | end 205 | """ 206 | 207 | code 208 | |> run_task(Specs, macros: true) 209 | |> refute_issues() 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /test/recode/task/tag_fixme_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.TagFIXMETest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.TagFIXME 5 | 6 | describe "run/1" do 7 | # 8 | # cases NOT raising issues 9 | # 10 | 11 | test "does not trigger" do 12 | ~s''' 13 | defmodule FIXME do 14 | @moduledoc """ 15 | The FIXME module 16 | """ 17 | 18 | # Returns FIXME atom 19 | def todo(x), do: :FIXME 20 | end 21 | ''' 22 | |> run_task(TagFIXME) 23 | |> refute_issues() 24 | end 25 | 26 | test "does not triggers tags in doc when deactivated" do 27 | ~s''' 28 | defmodule FIXME do 29 | @moduledoc """ 30 | The FIXME module 31 | FIXME add examples 32 | """ 33 | 34 | def todo(x), do: :FIXME 35 | end 36 | ''' 37 | |> run_task(TagFIXME, include_docs: false) 38 | |> refute_issues() 39 | end 40 | 41 | # 42 | # cases NOT raising issues 43 | # 44 | 45 | test "triggers an issue for a tag in comment" do 46 | ~s''' 47 | defmodule FIXME do 48 | @moduledoc """ 49 | The FIXME module 50 | """ 51 | 52 | # FIXME: add spec 53 | def todo(x), do: :FIXME 54 | end 55 | ''' 56 | |> run_task(TagFIXME) 57 | |> assert_issue_with( 58 | reporter: TagFIXME, 59 | line: 6, 60 | message: "Found a tag: FIXME: add spec" 61 | ) 62 | end 63 | end 64 | 65 | describe "init/1" do 66 | test "returns an ok tuple with added defaults" do 67 | assert TagFIXME.init(tag: "FIXME", reporter: TagFIXME) == 68 | {:ok, include_docs: true, tag: "FIXME", reporter: TagFIXME} 69 | end 70 | 71 | test "returns an ok tuple" do 72 | assert TagFIXME.init(tag: "FIXME", reporter: TagFIXME, include_docs: false) == 73 | {:ok, tag: "FIXME", reporter: TagFIXME, include_docs: false} 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/recode/task/tag_todo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.TagTODOTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.TagTODO 5 | 6 | describe "run/1" do 7 | # 8 | # cases NOT raising issues 9 | # 10 | 11 | test "does not trigger" do 12 | ~s''' 13 | defmodule TODO do 14 | @moduledoc """ 15 | The TODO module 16 | """ 17 | 18 | # Returns TODO atom 19 | def todo(x), do: :TODO 20 | end 21 | ''' 22 | |> run_task(TagTODO) 23 | |> refute_issues() 24 | end 25 | 26 | test "does not triggers tags in doc when deactivated" do 27 | ~s''' 28 | defmodule TODO do 29 | @moduledoc """ 30 | The TODO module 31 | TODO add examples 32 | """ 33 | 34 | def todo(x), do: :TODO 35 | end 36 | ''' 37 | |> run_task(TagTODO, include_docs: false) 38 | |> refute_issues() 39 | end 40 | 41 | test "does not triggers when doc tags are false" do 42 | ~s''' 43 | defmodule TODO do 44 | @moduledoc false 45 | 46 | @doc false 47 | def todo(x), do: :TODO 48 | end 49 | ''' 50 | |> run_task(TagTODO, include_docs: true) 51 | |> refute_issues() 52 | end 53 | 54 | # 55 | # cases raising issues 56 | # 57 | 58 | test "triggers an issue for a tag in comment" do 59 | ~s''' 60 | defmodule TODO do 61 | @moduledoc """ 62 | The TODO module 63 | """ 64 | 65 | # TODO: add spec 66 | def todo(x), do: :TODO 67 | end 68 | ''' 69 | |> run_task(TagTODO) 70 | |> assert_issue_with( 71 | reporter: TagTODO, 72 | line: 6, 73 | message: "Found a tag: TODO: add spec" 74 | ) 75 | end 76 | 77 | test "triggers an issue for a tag in @moduledoc" do 78 | ~s''' 79 | defmodule TODO do 80 | @moduledoc """ 81 | The TODO module 82 | TODO: just do it 83 | asdf 84 | """ 85 | 86 | def todo(x), do: :TODO 87 | end 88 | ''' 89 | |> run_task(TagTODO, include_docs: true) 90 | |> assert_issue_with( 91 | reporter: TagTODO, 92 | line: 4, 93 | message: "Found a tag: TODO: just do it" 94 | ) 95 | end 96 | 97 | test "triggers an issue for a tag in @doc" do 98 | ~s''' 99 | defmodule TODO do 100 | @doc """ 101 | TODO: just do it 102 | asdf 103 | """ 104 | def todo(x), do: :TODO 105 | end 106 | ''' 107 | |> run_task(TagTODO, include_docs: true) 108 | |> assert_issue_with( 109 | reporter: TagTODO, 110 | line: 3, 111 | message: "Found a tag: TODO: just do it" 112 | ) 113 | end 114 | end 115 | 116 | describe "init/1" do 117 | test "returns an ok tuple with added defaults" do 118 | assert TagTODO.init(tag: "TODO", reporter: TagTODO) == 119 | {:ok, include_docs: true, tag: "TODO", reporter: TagTODO} 120 | end 121 | 122 | test "returns an ok tuple" do 123 | assert TagTODO.init(tag: "TODO", reporter: TagTODO, include_docs: false) == 124 | {:ok, tag: "TODO", reporter: TagTODO, include_docs: false} 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/recode/task/test_file_ext_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.TestFileExtTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.TestFileExt 5 | 6 | test "updates path" do 7 | path = "test/foo_test.ex" 8 | 9 | ":test" 10 | |> source(path) 11 | |> run_task(TestFileExt, autocorrect: true) 12 | |> assert_path(path <> "s") 13 | end 14 | 15 | test "keeps path" do 16 | path = "test/foo_test.exs" 17 | 18 | ":test" 19 | |> source(path) 20 | |> run_task(TestFileExt, autocorrect: true) 21 | |> refute_update() 22 | end 23 | 24 | test "reports issue" do 25 | path = "test/foo_test.ex" 26 | 27 | ":test" 28 | |> source(path) 29 | |> run_task(TestFileExt, autocorrect: false) 30 | |> assert_issue_with(reporter: TestFileExt) 31 | end 32 | 33 | test "reports no issues" do 34 | path = "test/foo_test.exs" 35 | 36 | ":test" 37 | |> source(path) 38 | |> run_task(TestFileExt, autocorrect: false) 39 | |> refute_issues() 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/recode/task/unnecessary_if_unless_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.UnnecessaryIfUnlessTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.UnnecessaryIfUnless 5 | 6 | describe "run/1" do 7 | test "equals" do 8 | code = """ 9 | if foo == bar do 10 | true 11 | else 12 | false 13 | end 14 | """ 15 | 16 | expected = """ 17 | foo == bar 18 | """ 19 | 20 | code 21 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 22 | |> assert_code(expected) 23 | end 24 | 25 | test "predicate" do 26 | code = """ 27 | if foo?(bar) do 28 | true 29 | else 30 | false 31 | end 32 | """ 33 | 34 | expected = """ 35 | foo?(bar) 36 | """ 37 | 38 | code 39 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 40 | |> assert_code(expected) 41 | end 42 | 43 | test "keyword syntax" do 44 | code = """ 45 | if foo?(bar), do: true, else: false 46 | """ 47 | 48 | expected = """ 49 | foo?(bar) 50 | """ 51 | 52 | code 53 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 54 | |> assert_code(expected) 55 | end 56 | 57 | test "negates code with reverse booleans" do 58 | code = """ 59 | if some_call?() && (some_other_call?() || another_call?()) do 60 | false 61 | else 62 | true 63 | end 64 | """ 65 | 66 | expected = """ 67 | not (some_call?() && (some_other_call?() || another_call?())) 68 | """ 69 | 70 | code 71 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 72 | |> assert_code(expected) 73 | end 74 | 75 | test "maintains leading comments" do 76 | code = """ 77 | # I'm a comment 78 | if foo?(bar) do 79 | true 80 | else 81 | false 82 | end 83 | """ 84 | 85 | expected = """ 86 | # I'm a comment 87 | foo?(bar) 88 | """ 89 | 90 | code 91 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 92 | |> assert_code(expected) 93 | end 94 | 95 | test "keeps code as is without booleans" do 96 | code = """ 97 | if foo?(bar) do 98 | bar 99 | else 100 | false 101 | end 102 | """ 103 | 104 | expected = """ 105 | if foo?(bar) do 106 | bar 107 | else 108 | false 109 | end 110 | """ 111 | 112 | code 113 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 114 | |> assert_code(expected) 115 | end 116 | 117 | test "works with unless" do 118 | code = """ 119 | unless foo?(bar) do 120 | false 121 | else 122 | true 123 | end 124 | """ 125 | 126 | expected = """ 127 | foo?(bar) 128 | """ 129 | 130 | code 131 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 132 | |> assert_code(expected) 133 | end 134 | 135 | test "negates unless" do 136 | code = """ 137 | unless foo?(bar) do 138 | true 139 | else 140 | false 141 | end 142 | """ 143 | 144 | expected = """ 145 | not foo?(bar) 146 | """ 147 | 148 | code 149 | |> run_task(UnnecessaryIfUnless, autocorrect: true) 150 | |> assert_code(expected) 151 | end 152 | 153 | test "reports no issues" do 154 | """ 155 | foo == bar 156 | """ 157 | |> run_task(UnnecessaryIfUnless, autocorrect: false) 158 | |> refute_issues() 159 | end 160 | 161 | test "reports an issue" do 162 | """ 163 | if foo == bar do 164 | true 165 | else 166 | false 167 | end 168 | """ 169 | |> run_task(UnnecessaryIfUnless, autocorrect: false) 170 | |> assert_issue_with(reporter: UnnecessaryIfUnless) 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/recode/task/unused_variable_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.Task.UnusedVariableTest do 2 | use RecodeCase 3 | 4 | alias Recode.Task.UnusedVariable 5 | 6 | describe "run/1" do 7 | test "fix simple unused variables" do 8 | code = """ 9 | def foo(bar) do 10 | baz = 1 11 | bar 12 | end 13 | """ 14 | 15 | expected = """ 16 | def foo(bar) do 17 | _baz = 1 18 | bar 19 | end 20 | """ 21 | 22 | code 23 | |> run_task(UnusedVariable, autocorrect: true) 24 | |> assert_code(expected) 25 | end 26 | 27 | test "ignores comments" do 28 | code = """ 29 | def foo(bar) do 30 | # bar is not used 31 | end 32 | """ 33 | 34 | expected = """ 35 | def foo(_bar) do 36 | # bar is not used 37 | end 38 | """ 39 | 40 | code 41 | |> run_task(UnusedVariable, autocorrect: true) 42 | |> assert_code(expected) 43 | end 44 | 45 | test "keeps code" do 46 | """ 47 | def foo() do 48 | bar = String.to_atom() 49 | Bar.call(bar) 50 | end 51 | """ 52 | |> run_task(UnusedVariable, autocorrect: true) 53 | |> refute_update() 54 | end 55 | 56 | test "fix in module" do 57 | code = """ 58 | defmodule MyMod do 59 | require Logger 60 | 61 | defp download(items, dir) do 62 | Enum.map(items, fn item -> 63 | download(item, dir) 64 | end) 65 | end 66 | 67 | defp download(item, dir) do 68 | Logger.info("Downloading") 69 | end 70 | end 71 | """ 72 | 73 | expected = """ 74 | defmodule MyMod do 75 | require Logger 76 | 77 | defp download(items, dir) do 78 | Enum.map(items, fn item -> 79 | download(item, dir) 80 | end) 81 | end 82 | 83 | defp download(_item, _dir) do 84 | Logger.info("Downloading") 85 | end 86 | end 87 | """ 88 | 89 | code 90 | |> run_task(UnusedVariable, autocorrect: true) 91 | |> assert_code(expected) 92 | end 93 | 94 | test "fix multiple unused variables" do 95 | code = """ 96 | def foo(bar) do 97 | baz = other_var = 1 98 | hello = 1 99 | bar 100 | end 101 | """ 102 | 103 | expected = """ 104 | def foo(bar) do 105 | _baz = _other_var = 1 106 | _hello = 1 107 | bar 108 | end 109 | """ 110 | 111 | code 112 | |> run_task(UnusedVariable, autocorrect: true) 113 | |> assert_code(expected) 114 | end 115 | 116 | test "fix unused variables in anonymous function" do 117 | code = """ 118 | fn bar -> 119 | foo = 1 120 | bar 121 | end 122 | """ 123 | 124 | expected = """ 125 | fn bar -> 126 | _foo = 1 127 | bar 128 | end 129 | """ 130 | 131 | code 132 | |> run_task(UnusedVariable, autocorrect: true) 133 | |> assert_code(expected) 134 | end 135 | 136 | test "reports an issue" do 137 | """ 138 | def foo(bar) do 139 | baz = 1 140 | bar 141 | end 142 | """ 143 | |> run_task(UnusedVariable, autocorrect: false) 144 | |> assert_issue() 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /test/recode/task_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Recode.TaskTest do 2 | use ExUnit.Case 3 | 4 | alias Recode.Task 5 | 6 | defmodule Dummy do 7 | @shortdoc "Lorem ispum Dummy" 8 | 9 | use Recode.Task 10 | 11 | @impl true 12 | def run(source, _opts), do: source 13 | end 14 | 15 | defmodule CorrectorDummy do 16 | @shortdoc "Lorem ispum CorrectorDummy" 17 | 18 | use Recode.Task, corrector: true, checker: false, category: :test 19 | 20 | @impl true 21 | def run(source, _opts), do: source 22 | 23 | @impl true 24 | def init(config) do 25 | if Keyword.has_key?(config, :test) do 26 | {:ok, config} 27 | else 28 | {:error, "missing :test"} 29 | end 30 | end 31 | end 32 | 33 | test "corrector?/1" do 34 | assert Task.corrector?(Dummy) == false 35 | assert Task.corrector?(CorrectorDummy) == true 36 | end 37 | 38 | test "checker?/1" do 39 | assert Task.checker?(Dummy) == true 40 | assert Task.checker?(CorrectorDummy) == false 41 | end 42 | 43 | test "category/1" do 44 | assert Task.category(Dummy) == nil 45 | assert Task.category(CorrectorDummy) == :test 46 | end 47 | 48 | test "shortdoc/1" do 49 | assert Task.shortdoc(Dummy) == "Lorem ispum Dummy" 50 | assert Task.shortdoc(CorrectorDummy) == "Lorem ispum CorrectorDummy" 51 | end 52 | 53 | test "init/1" do 54 | assert Dummy.init(foo: :bar) == {:ok, foo: :bar} 55 | assert CorrectorDummy.init(foo: :bar) == {:error, "missing :test"} 56 | assert CorrectorDummy.init(test: :bar) == {:ok, test: :bar} 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/recode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RecodeTest do 2 | use ExUnit.Case 3 | 4 | doctest Recode 5 | end 6 | -------------------------------------------------------------------------------- /test/support/fake_plugin.ex: -------------------------------------------------------------------------------- 1 | defmodule FakePlugin do 2 | @moduledoc """ 3 | A formatter plugin that always set `force_do_end_blocks: true`. 4 | """ 5 | 6 | @behaviour Mix.Tasks.Format 7 | 8 | def features(_opts) do 9 | [sigils: [], extensions: [".ex", ".exs"]] 10 | end 11 | 12 | def format(contents, opts) do 13 | contents 14 | |> Code.format_string!(Keyword.put(opts, :force_do_end_blocks, true)) 15 | |> IO.iodata_to_binary() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/support/plts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrzndhrn/recode/42042cfbb7dd594c21cd12eb58eb38e6c11f70af/test/support/plts/.keep -------------------------------------------------------------------------------- /test/support/recode_case.ex: -------------------------------------------------------------------------------- 1 | defmodule RecodeCase do 2 | @moduledoc false 3 | 4 | use ExUnit.CaseTemplate 5 | 6 | alias Rewrite.Source 7 | 8 | using do 9 | quote do 10 | import RecodeCase 11 | end 12 | end 13 | 14 | setup context do 15 | Mox.stub_with(Recode.RunnerMock, Recode.Runner.Impl) 16 | 17 | context 18 | end 19 | 20 | defmacro assert_issue(source) do 21 | quote bind_quoted: [source: source] do 22 | assert length(source.issues) == 1, 23 | "Expected one issue, got:\n#{inspect(source.issues, pretty: true)}" 24 | end 25 | end 26 | 27 | defmacro assert_issue_with(source, keyword) do 28 | quote bind_quoted: [source: source, keyword: keyword] do 29 | assert length(source.issues) == 1, 30 | "Expected one issue, got:\n#{inspect(source.issues, pretty: true)}" 31 | 32 | {_, issue} = hd(source.issues) 33 | 34 | Enum.each(keyword, fn {key, value} -> 35 | got = Map.fetch!(issue, key) 36 | 37 | error = 38 | "Expected #{inspect(value)} for key #{inspect(key)} in issue, got: #{inspect(got)}" 39 | 40 | assert got == value, error 41 | end) 42 | end 43 | end 44 | 45 | defmacro assert_issues_with(source, list) do 46 | quote bind_quoted: [source: source, list: list] do 47 | assert length(source.issues) == length(list), 48 | "Expected #{length(list)} issue(s), got:\n#{inspect(source.issues, pretty: true)}" 49 | 50 | for {{_version, issue}, keyword} <- Enum.zip(source.issues, list), 51 | {key, value} <- keyword do 52 | got = Map.fetch!(issue, key) 53 | 54 | error = 55 | "Expected #{inspect(value)} for key #{inspect(key)} in issue, got: #{inspect(got)}" 56 | 57 | assert got == value, error 58 | end 59 | end 60 | end 61 | 62 | defmacro assert_issues(source, amount) do 63 | quote bind_quoted: [source: source, amount: amount] do 64 | assert length(source.issues) == amount, 65 | "Expected #{amount} issue(s), got: #{inspect(source.issues, pretty: true)}" 66 | end 67 | end 68 | 69 | defmacro refute_issues(source) do 70 | quote bind_quoted: [source: source] do 71 | assert Enum.empty?(source.issues), 72 | "Expected no issues, got #{inspect(source.issues, pretty: true)}" 73 | end 74 | end 75 | 76 | defmacro refute_update(source) do 77 | quote bind_quoted: [source: source] do 78 | refute Source.updated?(source) 79 | end 80 | end 81 | 82 | defmacro assert_code(source, expected) do 83 | quote bind_quoted: [source: source, expected: expected] do 84 | assert Source.get(source, :content) == expected 85 | end 86 | end 87 | 88 | defmacro assert_path(source, expected) do 89 | quote bind_quoted: [source: source, expected: expected] do 90 | assert Source.get(source, :path) == expected 91 | end 92 | end 93 | 94 | defmacro assert_config_error(error_tuple, expected_message \\ nil) do 95 | quote bind_quoted: [error_tuple: error_tuple, expected_message: expected_message] do 96 | if expected_message do 97 | assert error_tuple == {:error, expected_message} 98 | else 99 | assert {:error, error_message} = error_tuple 100 | assert is_binary(error_message) 101 | end 102 | end 103 | end 104 | 105 | def source(string, path \\ nil) do 106 | Source.Ex.from_string(string, path) 107 | end 108 | 109 | def project(%Source{} = source) do 110 | Rewrite.from_sources!([source]) 111 | end 112 | 113 | def run_task(code, task, opts \\ []) 114 | 115 | def run_task(code, task, opts) when is_binary(code) do 116 | run_task(Source.Ex.from_string(code), task, opts) 117 | end 118 | 119 | def run_task(%Source{} = source, task, opts) do 120 | with {:ok, opts} <- task.init(opts) do 121 | task.run(source, opts) 122 | end 123 | end 124 | 125 | def formated?(code) do 126 | String.trim(code) == code |> Code.format_string!() |> IO.iodata_to_binary() 127 | end 128 | 129 | def eof_newline(string), do: String.trim_trailing(string) <> "\n" 130 | end 131 | -------------------------------------------------------------------------------- /test/support/strip.ex: -------------------------------------------------------------------------------- 1 | defmodule Strip do 2 | @moduledoc false 3 | 4 | def strip_meta(quoted, opts) when not is_struct(quoted) do 5 | Macro.prewalk(quoted, fn quoted -> do_strip_meta(quoted, opts) end) 6 | end 7 | 8 | defp do_strip_meta({form, meta, args}, opts) do 9 | cond do 10 | Keyword.has_key?(opts, :take) -> {form, Keyword.take(meta, opts[:take]), args} 11 | Keyword.has_key?(opts, :drop) -> {form, Keyword.drop(meta, opts[:drop]), args} 12 | true -> {form, [], args} 13 | end 14 | end 15 | 16 | defp do_strip_meta(quoted, _opts), do: quoted 17 | 18 | def strip_esc_seq(string) do 19 | string 20 | |> String.replace(~r/\e[^m]+m/, "") 21 | |> String.split("\n") 22 | |> Enum.map_join("\n", fn string -> 23 | ~r/^\s(\w.*)/ 24 | |> Regex.replace(string, "\\1") 25 | |> String.trim_trailing() 26 | end) 27 | |> String.trim_leading() 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Code.put_compiler_option(:ignore_module_conflict, true) 2 | 3 | Code.compile_file("test/support/recode_case.ex") 4 | Code.compile_file("test/support/fake_plugin.ex") 5 | 6 | Mox.defmock(Recode.RunnerMock, for: Recode.Runner) 7 | Mox.defmock(Recode.TaskMock, for: Recode.Task) 8 | Application.put_env(:recode, :runner, Recode.RunnerMock) 9 | 10 | ExUnit.start(capture_log: true) 11 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hrzndhrn/recode/42042cfbb7dd594c21cd12eb58eb38e6c11f70af/tmp/.keep --------------------------------------------------------------------------------