├── .ameba.yml ├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bench └── check_sources.cr ├── bin ├── .keep └── ameba.cr ├── shard.yml ├── spec ├── ameba │ ├── ast │ │ ├── branch_spec.cr │ │ ├── branchable_spec.cr │ │ ├── flow_expression_spec.cr │ │ ├── scope_spec.cr │ │ ├── util_spec.cr │ │ ├── variabling │ │ │ ├── argument_spec.cr │ │ │ ├── assignment_spec.cr │ │ │ ├── reference_spec.cr │ │ │ ├── type_dec_variable_spec.cr │ │ │ └── variable_spec.cr │ │ └── visitors │ │ │ ├── counting_visitor_spec.cr │ │ │ ├── flow_expression_visitor_spec.cr │ │ │ ├── node_visitor_spec.cr │ │ │ ├── redundant_control_expression_visitor_spec.cr │ │ │ ├── scope_calls_with_self_receiver_visitor_spec.cr │ │ │ ├── scope_visitor_spec.cr │ │ │ └── top_level_nodes_visitor_spec.cr │ ├── base_spec.cr │ ├── cli │ │ └── cmd_spec.cr │ ├── config_spec.cr │ ├── ext │ │ └── location_spec.cr │ ├── formatter │ │ ├── disabled_formatter_spec.cr │ │ ├── dot_formatter_spec.cr │ │ ├── explain_formatter_spec.cr │ │ ├── flycheck_formatter_spec.cr │ │ ├── github_actions_formatter_spec.cr │ │ ├── json_formatter_spec.cr │ │ ├── todo_formatter_spec.cr │ │ └── util_spec.cr │ ├── glob_utils_spec.cr │ ├── inline_comments_spec.cr │ ├── issue_spec.cr │ ├── presenter │ │ ├── rule_collection_presenter_spec.cr │ │ ├── rule_presenter_spec.cr │ │ └── rule_versions_presenter_spec.cr │ ├── reportable_spec.cr │ ├── rule │ │ ├── base_spec.cr │ │ ├── documentation │ │ │ ├── documentation_admonition_spec.cr │ │ │ └── documentation_spec.cr │ │ ├── layout │ │ │ ├── line_length_spec.cr │ │ │ ├── trailing_blank_lines_spec.cr │ │ │ └── trailing_whitespace_spec.cr │ │ ├── lint │ │ │ ├── ambiguous_assignment_spec.cr │ │ │ ├── bad_directive_spec.cr │ │ │ ├── comparison_to_boolean_spec.cr │ │ │ ├── debug_calls_spec.cr │ │ │ ├── debugger_statement_spec.cr │ │ │ ├── duplicate_when_condition_spec.cr │ │ │ ├── duplicated_require_spec.cr │ │ │ ├── else_nil_spec.cr │ │ │ ├── empty_ensure_spec.cr │ │ │ ├── empty_expression_spec.cr │ │ │ ├── empty_loop_spec.cr │ │ │ ├── formatting_spec.cr │ │ │ ├── hash_duplicated_key_spec.cr │ │ │ ├── literal_assignments_in_expressions_spec.cr │ │ │ ├── literal_in_condition_spec.cr │ │ │ ├── literal_in_interpolation_spec.cr │ │ │ ├── literals_comparison_spec.cr │ │ │ ├── missing_block_argument_spec.cr │ │ │ ├── not_nil_after_no_bang_spec.cr │ │ │ ├── not_nil_spec.cr │ │ │ ├── percent_arrays_spec.cr │ │ │ ├── rand_zero_spec.cr │ │ │ ├── redundant_string_cercion_spec.cr │ │ │ ├── redundant_with_index_spec.cr │ │ │ ├── redundant_with_object_spec.cr │ │ │ ├── require_parentheses_spec.cr │ │ │ ├── shadowed_argument_spec.cr │ │ │ ├── shadowed_exception_spec.cr │ │ │ ├── shadowing_outer_local_var_spec.cr │ │ │ ├── shared_var_in_fiber_spec.cr │ │ │ ├── signal_trap_spec.cr │ │ │ ├── spec_eq_with_bool_or_nil_literal_spec.cr │ │ │ ├── spec_filename_spec.cr │ │ │ ├── spec_focus_spec.cr │ │ │ ├── syntax_spec.cr │ │ │ ├── top_level_operator_definition_spec.cr │ │ │ ├── trailing_rescue_exception_spec.cr │ │ │ ├── typos_spec.cr │ │ │ ├── unneeded_disable_directive_spec.cr │ │ │ ├── unreachable_code_spec.cr │ │ │ ├── unused_argument_spec.cr │ │ │ ├── unused_block_argument_spec.cr │ │ │ ├── unused_class_variable_access_spec.cr │ │ │ ├── unused_comparison_spec.cr │ │ │ ├── unused_generic_or_union_spec.cr │ │ │ ├── unused_instance_variable_access_spec.cr │ │ │ ├── unused_literal_spec.cr │ │ │ ├── unused_local_variable_access_spec.cr │ │ │ ├── unused_pseudo_method_call_spec.cr │ │ │ ├── unused_self_access_spec.cr │ │ │ ├── useless_assign_spec.cr │ │ │ └── useless_condition_in_when_spec.cr │ │ ├── metrics │ │ │ └── cyclomatic_complexity_spec.cr │ │ ├── naming │ │ │ ├── accessor_method_name_spec.cr │ │ │ ├── ascii_identifiers_spec.cr │ │ │ ├── binary_operator_parameter_name_spec.cr │ │ │ ├── block_parameter_name_spec.cr │ │ │ ├── constant_names_spec.cr │ │ │ ├── filename_spec.cr │ │ │ ├── method_names_spec.cr │ │ │ ├── predicate_name_spec.cr │ │ │ ├── query_bool_methods_spec.cr │ │ │ ├── rescued_exceptions_variable_name_spec.cr │ │ │ ├── type_names_spec.cr │ │ │ └── variable_names_spec.cr │ │ ├── performance │ │ │ ├── any_after_filter_spec.cr │ │ │ ├── any_instead_of_empty_spec.cr │ │ │ ├── base_spec.cr │ │ │ ├── chained_call_with_no_bang_spec.cr │ │ │ ├── compact_after_map_spec.cr │ │ │ ├── excessive_allocations_spec.cr │ │ │ ├── first_last_after_filter_spec.cr │ │ │ ├── flatten_after_map_spec.cr │ │ │ ├── map_instead_of_block_spec.cr │ │ │ ├── minmax_after_map_spec.cr │ │ │ └── size_after_filter_spec.cr │ │ ├── style │ │ │ ├── guard_clause_spec.cr │ │ │ ├── heredoc_escape_spec.cr │ │ │ ├── heredoc_indent_spec.cr │ │ │ ├── is_a_filter_spec.cr │ │ │ ├── is_a_nil_spec.cr │ │ │ ├── large_numbers_spec.cr │ │ │ ├── multiline_curly_block_spec.cr │ │ │ ├── negated_conditions_in_unless_spec.cr │ │ │ ├── parentheses_around_condition_spec.cr │ │ │ ├── percent_literal_delimiters_spec.cr │ │ │ ├── redundant_begin_spec.cr │ │ │ ├── redundant_next_spec.cr │ │ │ ├── redundant_return_spec.cr │ │ │ ├── redundant_self_spec.cr │ │ │ ├── unless_else_spec.cr │ │ │ ├── verbose_block_spec.cr │ │ │ └── while_true_spec.cr │ │ └── typing │ │ │ ├── macro_call_argument_type_restriction_spec.cr │ │ │ ├── method_parameter_type_restriction_spec.cr │ │ │ ├── method_return_type_restriction_spec.cr │ │ │ └── proc_literal_return_type_restriction_spec.cr │ ├── runner_spec.cr │ ├── severity_spec.cr │ ├── source │ │ └── rewriter_spec.cr │ ├── source_spec.cr │ ├── spec │ │ └── annotated_source_spec.cr │ └── tokenizer_spec.cr ├── ameba_spec.cr ├── fixtures │ └── config.yml └── spec_helper.cr └── src ├── ameba.cr ├── ameba ├── ast │ ├── branch.cr │ ├── branchable.cr │ ├── flow_expression.cr │ ├── scope.cr │ ├── util.cr │ ├── variabling │ │ ├── argument.cr │ │ ├── assignment.cr │ │ ├── ivariable.cr │ │ ├── reference.cr │ │ ├── type_dec_variable.cr │ │ └── variable.cr │ └── visitors │ │ ├── base_visitor.cr │ │ ├── counting_visitor.cr │ │ ├── flow_expression_visitor.cr │ │ ├── implicit_return_visitor.cr │ │ ├── node_visitor.cr │ │ ├── redundant_control_expression_visitor.cr │ │ ├── scope_calls_with_self_receiver_visitor.cr │ │ ├── scope_visitor.cr │ │ └── top_level_nodes_visitor.cr ├── cli │ └── cmd.cr ├── config.cr ├── ext │ └── location.cr ├── formatter │ ├── base_formatter.cr │ ├── disabled_formatter.cr │ ├── dot_formatter.cr │ ├── explain_formatter.cr │ ├── flycheck_formatter.cr │ ├── github_actions_formatter.cr │ ├── json_formatter.cr │ ├── todo_formatter.cr │ └── util.cr ├── glob_utils.cr ├── inline_comments.cr ├── issue.cr ├── presenter │ ├── base_presenter.cr │ ├── rule_collection_presenter.cr │ ├── rule_presenter.cr │ └── rule_versions_presenter.cr ├── reportable.cr ├── rule │ ├── base.cr │ ├── documentation │ │ ├── documentation.cr │ │ └── documentation_admonition.cr │ ├── layout │ │ ├── line_length.cr │ │ ├── trailing_blank_lines.cr │ │ └── trailing_whitespace.cr │ ├── lint │ │ ├── ambiguous_assignment.cr │ │ ├── bad_directive.cr │ │ ├── comparison_to_boolean.cr │ │ ├── debug_calls.cr │ │ ├── debugger_statement.cr │ │ ├── duplicate_when_condition.cr │ │ ├── duplicated_require.cr │ │ ├── else_nil.cr │ │ ├── empty_ensure.cr │ │ ├── empty_expression.cr │ │ ├── empty_loop.cr │ │ ├── formatting.cr │ │ ├── hash_duplicated_key.cr │ │ ├── literal_assignments_in_expressions.cr │ │ ├── literal_in_condition.cr │ │ ├── literal_in_interpolation.cr │ │ ├── literals_comparison.cr │ │ ├── missing_block_argument.cr │ │ ├── not_nil.cr │ │ ├── not_nil_after_no_bang.cr │ │ ├── percent_arrays.cr │ │ ├── rand_zero.cr │ │ ├── redundant_string_coercion.cr │ │ ├── redundant_with_index.cr │ │ ├── redundant_with_object.cr │ │ ├── require_parentheses.cr │ │ ├── shadowed_argument.cr │ │ ├── shadowed_exception.cr │ │ ├── shadowing_outer_local_var.cr │ │ ├── shared_var_in_fiber.cr │ │ ├── signal_trap.cr │ │ ├── spec_eq_with_bool_or_nil_literal.cr │ │ ├── spec_filename.cr │ │ ├── spec_focus.cr │ │ ├── syntax.cr │ │ ├── top_level_operator_definition.cr │ │ ├── trailing_rescue_exception.cr │ │ ├── typos.cr │ │ ├── unneeded_disable_directive.cr │ │ ├── unreachable_code.cr │ │ ├── unused_argument.cr │ │ ├── unused_block_argument.cr │ │ ├── unused_class_variable_access.cr │ │ ├── unused_comparison.cr │ │ ├── unused_generic_or_union.cr │ │ ├── unused_instance_variable_access.cr │ │ ├── unused_literal.cr │ │ ├── unused_local_variable_access.cr │ │ ├── unused_pseudo_method_call.cr │ │ ├── unused_self_access.cr │ │ ├── useless_assign.cr │ │ └── useless_condition_in_when.cr │ ├── metrics │ │ └── cyclomatic_complexity.cr │ ├── naming │ │ ├── accessor_method_name.cr │ │ ├── ascii_identifiers.cr │ │ ├── binary_operator_parameter_name.cr │ │ ├── block_parameter_name.cr │ │ ├── constant_names.cr │ │ ├── filename.cr │ │ ├── method_names.cr │ │ ├── predicate_name.cr │ │ ├── query_bool_methods.cr │ │ ├── rescued_exceptions_variable_name.cr │ │ ├── type_names.cr │ │ └── variable_names.cr │ ├── performance │ │ ├── any_after_filter.cr │ │ ├── any_instead_of_empty.cr │ │ ├── base.cr │ │ ├── chained_call_with_no_bang.cr │ │ ├── compact_after_map.cr │ │ ├── excessive_allocations.cr │ │ ├── first_last_after_filter.cr │ │ ├── flatten_after_map.cr │ │ ├── map_instead_of_block.cr │ │ ├── minmax_after_map.cr │ │ └── size_after_filter.cr │ ├── style │ │ ├── guard_clause.cr │ │ ├── heredoc_escape.cr │ │ ├── heredoc_indent.cr │ │ ├── is_a_filter.cr │ │ ├── is_a_nil.cr │ │ ├── large_numbers.cr │ │ ├── multiline_curly_block.cr │ │ ├── negated_conditions_in_unless.cr │ │ ├── parentheses_around_condition.cr │ │ ├── percent_literal_delimiters.cr │ │ ├── redundant_begin.cr │ │ ├── redundant_next.cr │ │ ├── redundant_return.cr │ │ ├── redundant_self.cr │ │ ├── unless_else.cr │ │ ├── verbose_block.cr │ │ └── while_true.cr │ └── typing │ │ ├── macro_call_argument_type_restriction.cr │ │ ├── method_parameter_type_restriction.cr │ │ ├── method_return_type_restriction.cr │ │ └── proc_literal_return_type_restriction.cr ├── runner.cr ├── severity.cr ├── source.cr ├── source │ ├── corrector.cr │ ├── rewriter.cr │ └── rewriter │ │ └── action.cr ├── spec │ ├── annotated_source.cr │ ├── be_valid.cr │ ├── expect_issue.cr │ ├── support.cr │ └── util.cr └── tokenizer.cr ├── cli.cr └── contrib └── read_type_doc.cr /.ameba.yml: -------------------------------------------------------------------------------- 1 | Documentation/DocumentationAdmonition: 2 | Timezone: UTC 3 | Admonitions: [FIXME, BUG] 4 | 5 | Lint/Typos: 6 | Excluded: 7 | - spec/ameba/rule/lint/typos_spec.cr 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !Makefile 3 | !shard.yml 4 | !src -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cr] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: daily 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | schedule: 9 | - cron: "0 3 * * 1" # Every monday at 3 AM 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | crystal: [latest, nightly] 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Set timezone to UTC 22 | uses: szenius/set-timezone@v2.0 23 | 24 | - name: Install Crystal 25 | uses: crystal-lang/install-crystal@v1 26 | with: 27 | crystal: ${{ matrix.crystal }} 28 | 29 | - name: Download source 30 | uses: actions/checkout@v4 31 | 32 | - name: Install dependencies 33 | run: shards install 34 | 35 | - name: Install typos-cli 36 | if: matrix.os == 'macos-latest' 37 | run: brew install typos-cli 38 | 39 | - name: Run specs 40 | run: make spec 41 | 42 | - name: Build ameba binary 43 | run: make build 44 | 45 | - name: Run ameba linter 46 | run: make lint 47 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build-and-deploy: 12 | concurrency: ci-${{ github.ref }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Inject slug/short variables 16 | uses: rlespinasse/github-slug-action@v5 17 | 18 | - name: Install Crystal 19 | uses: crystal-lang/install-crystal@v1 20 | 21 | - name: Download source 22 | uses: actions/checkout@v4 23 | 24 | - name: Install dependencies 25 | run: shards install 26 | 27 | - name: Build docs 28 | run: crystal docs --project-version="${{ env.GITHUB_REF_SLUG }}" --source-refname="${{ env.GITHUB_SHA_SHORT }}" 29 | 30 | - name: Deploy docs 🚀 31 | uses: JamesIves/github-pages-deploy-action@v4 32 | with: 33 | branch: gh-pages 34 | folder: docs 35 | clean: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ameba 4 | /bin/ameba.dwarf 5 | /.shards/ 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | 11 | # Workspace settings used by common text-editors 12 | /.vscode 13 | /.zed 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge AS builder 2 | RUN apk add --update crystal shards yaml-dev musl-dev make 3 | RUN mkdir /ameba 4 | WORKDIR /ameba 5 | COPY . /ameba/ 6 | RUN make clean && make 7 | 8 | FROM alpine:latest 9 | RUN apk add --update yaml pcre2 gc libevent libgcc 10 | RUN mkdir /src 11 | WORKDIR /src 12 | COPY --from=builder /ameba/bin/ameba /usr/bin/ 13 | RUN ameba -v 14 | ENTRYPOINT [ "/usr/bin/ameba" ] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2020 Vitalii Elenhaupt 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /bench/check_sources.cr: -------------------------------------------------------------------------------- 1 | require "../src/ameba" 2 | require "benchmark" 3 | 4 | private def get_files(n) 5 | Dir["src/**/*.cr"].first(n) 6 | end 7 | 8 | puts "== Compare:" 9 | Benchmark.ips do |x| 10 | [ 11 | 1, 12 | 3, 13 | 5, 14 | 10, 15 | 20, 16 | 30, 17 | 40, 18 | ].each do |n| # ameba:disable Naming/BlockParameterName 19 | config = Ameba::Config.load 20 | config.formatter = Ameba::Formatter::BaseFormatter.new 21 | config.globs = get_files(n) 22 | s = n == 1 ? "" : "s" 23 | x.report("#{n} source#{s}") { Ameba.run config } 24 | end 25 | end 26 | 27 | puts "== Measure:" 28 | config = Ameba::Config.load 29 | config.formatter = Ameba::Formatter::BaseFormatter.new 30 | puts Benchmark.measure { Ameba.run config } 31 | -------------------------------------------------------------------------------- /bin/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-ameba/ameba/d861f4ed0f904e0ecdad11c26f98e968f7d95afa/bin/.keep -------------------------------------------------------------------------------- /bin/ameba.cr: -------------------------------------------------------------------------------- 1 | # Require ameba cli which starts the inspection. 2 | require "ameba/cli" 3 | 4 | # Require ameba extensions here which are added as project dependencies. 5 | # Example: 6 | # 7 | # require "ameba-performance" 8 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: ameba 2 | version: 1.7.0-dev 3 | 4 | authors: 5 | - Vitalii Elenhaupt 6 | - Sijawusz Pur Rahnama 7 | 8 | targets: 9 | ameba: 10 | main: src/cli.cr 11 | 12 | scripts: 13 | postinstall: shards build -Dpreview_mt 14 | 15 | # TODO: remove pre-compiled executable in future releases 16 | executables: 17 | - ameba 18 | - ameba.cr 19 | 20 | crystal: ~> 1.10 21 | 22 | license: MIT 23 | -------------------------------------------------------------------------------- /spec/ameba/ast/branchable_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba::AST 4 | describe Branchable do 5 | describe "#initialize" do 6 | it "creates a new branchable" do 7 | branchable = Branchable.new as_node "a = 2 if true" 8 | branchable.node.should_not be_nil 9 | end 10 | end 11 | 12 | describe "delegation" do 13 | it "delegates to_s to @node" do 14 | node = as_node "a = 2 if true" 15 | branchable = Branchable.new node 16 | branchable.to_s.should eq node.to_s 17 | end 18 | 19 | it "delegates locations to @node" do 20 | node = as_node "a = 2 if true" 21 | branchable = Branchable.new node 22 | branchable.location.should eq node.location 23 | branchable.end_location.should eq node.end_location 24 | end 25 | end 26 | 27 | describe "#loop?" do 28 | it "returns true if it is a while loop" do 29 | branchable = Branchable.new as_node "while true; a = 2; end" 30 | branchable.loop?.should be_true 31 | end 32 | 33 | it "returns true if it is the until loop" do 34 | branchable = Branchable.new as_node "until false; a = 2; end" 35 | branchable.loop?.should be_true 36 | end 37 | 38 | it "returns true if it is loop" do 39 | branchable = Branchable.new as_node "loop {}" 40 | branchable.loop?.should be_true 41 | end 42 | 43 | it "returns false otherwise" do 44 | branchable = Branchable.new as_node "a = 2 if true" 45 | branchable.loop?.should be_false 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/ameba/ast/variabling/argument_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | describe Argument do 5 | arg = Crystal::Arg.new "a" 6 | scope = Scope.new as_node "foo = 1" 7 | variable = Variable.new(Crystal::Var.new("foo"), scope) 8 | 9 | describe "#initialize" do 10 | it "creates a new argument" do 11 | argument = Argument.new(arg, variable) 12 | argument.node.should_not be_nil 13 | end 14 | end 15 | 16 | describe "delegation" do 17 | it "delegates locations to node" do 18 | argument = Argument.new(arg, variable) 19 | argument.location.should eq arg.location 20 | argument.end_location.should eq arg.end_location 21 | end 22 | 23 | it "delegates to_s to node" do 24 | argument = Argument.new(arg, variable) 25 | argument.to_s.should eq arg.to_s 26 | end 27 | end 28 | 29 | describe "#ignored?" do 30 | it "is true if arg starts with _" do 31 | argument = Argument.new(Crystal::Arg.new("_a"), variable) 32 | argument.ignored?.should be_true 33 | end 34 | 35 | it "is false otherwise" do 36 | argument = Argument.new(arg, variable) 37 | argument.ignored?.should be_false 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/ameba/ast/variabling/reference_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | describe Reference do 5 | it "is derived from a Variable" do 6 | node = Crystal::Var.new "foo" 7 | ref = Reference.new(node, Scope.new as_node "foo = 1") 8 | ref.should be_a Variable 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/ameba/ast/variabling/type_dec_variable_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | describe TypeDecVariable do 5 | var = Crystal::Var.new("foo") 6 | declared_type = Crystal::Path.new("String") 7 | type_dec = Crystal::TypeDeclaration.new(var, declared_type) 8 | 9 | describe "#initialize" do 10 | it "creates a new type dec variable" do 11 | variable = TypeDecVariable.new(type_dec) 12 | variable.node.should_not be_nil 13 | end 14 | end 15 | 16 | describe "#name" do 17 | it "returns var name" do 18 | variable = TypeDecVariable.new(type_dec) 19 | variable.name.should eq var.name 20 | end 21 | 22 | it "raises if type declaration is incorrect" do 23 | type_dec = Crystal::TypeDeclaration.new(declared_type, declared_type) 24 | 25 | expect_raises(Exception, "Unsupported var node type: Crystal::Path") do 26 | TypeDecVariable.new(type_dec).name 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/ameba/ast/visitors/flow_expression_visitor_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | describe FlowExpressionVisitor do 5 | it "creates an expression for return" do 6 | rule = FlowExpressionRule.new 7 | FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL 8 | def foo 9 | return :bar 10 | end 11 | CRYSTAL 12 | rule.expressions.size.should eq 1 13 | end 14 | 15 | it "can create multiple expressions" do 16 | rule = FlowExpressionRule.new 17 | FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL 18 | def foo 19 | if bar 20 | return :baz 21 | else 22 | return :foobar 23 | end 24 | end 25 | CRYSTAL 26 | rule.expressions.size.should eq 3 27 | end 28 | 29 | it "properly creates nested flow expressions" do 30 | rule = FlowExpressionRule.new 31 | FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL 32 | def foo 33 | return ( 34 | loop do 35 | break if a > 1 36 | return a 37 | end 38 | ) 39 | end 40 | CRYSTAL 41 | rule.expressions.size.should eq 4 42 | end 43 | 44 | it "creates an expression for break" do 45 | rule = FlowExpressionRule.new 46 | FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL 47 | while true 48 | break 49 | end 50 | CRYSTAL 51 | rule.expressions.size.should eq 1 52 | end 53 | 54 | it "creates an expression for next" do 55 | rule = FlowExpressionRule.new 56 | FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL 57 | while true 58 | next if something 59 | end 60 | CRYSTAL 61 | rule.expressions.size.should eq 1 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/ameba/ast/visitors/node_visitor_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | rule = DummyRule.new 5 | source = Source.new "" 6 | 7 | describe NodeVisitor do 8 | describe "visit" do 9 | it "allow to visit ASTNode" do 10 | visitor = NodeVisitor.new rule, source 11 | nodes = Crystal::Parser.new("").parse 12 | nodes.accept visitor 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/ameba/ast/visitors/redundant_control_expression_visitor_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | source = Source.new "" 5 | rule = RedundantControlExpressionRule.new 6 | 7 | describe RedundantControlExpressionVisitor do 8 | node = as_node <<-CRYSTAL 9 | a = 1 10 | b = 2 11 | return a + b 12 | CRYSTAL 13 | subject = RedundantControlExpressionVisitor.new(rule, source, node) 14 | 15 | it "assigns valid attributes" do 16 | subject.rule.should eq rule 17 | subject.source.should eq source 18 | subject.node.should eq node 19 | end 20 | 21 | it "fires a callback with a valid node" do 22 | rule.nodes.size.should eq 1 23 | rule.nodes.first.to_s.should eq "return a + b" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/ameba/ast/visitors/top_level_nodes_visitor_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::AST 4 | describe TopLevelNodesVisitor do 5 | describe "#require_nodes" do 6 | it "returns require node" do 7 | source = Source.new <<-CRYSTAL 8 | require "foo" 9 | 10 | def bar 11 | end 12 | CRYSTAL 13 | visitor = TopLevelNodesVisitor.new(source.ast) 14 | visitor.require_nodes.size.should eq 1 15 | visitor.require_nodes.first.to_s.should eq %q(require "foo") 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/ameba/ext/location_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | describe Crystal::Location do 4 | subject = Crystal::Location.new(nil, 2, 3) 5 | 6 | describe "#with" do 7 | it "changes line number" do 8 | subject.with(line_number: 1).to_s.should eq ":1:3" 9 | end 10 | 11 | it "changes column number" do 12 | subject.with(column_number: 1).to_s.should eq ":2:1" 13 | end 14 | 15 | it "changes line and column numbers" do 16 | subject.with(line_number: 1, column_number: 2).to_s.should eq ":1:2" 17 | end 18 | end 19 | 20 | describe "#adjust" do 21 | it "adjusts line number" do 22 | subject.adjust(line_number: 1).to_s.should eq ":3:3" 23 | end 24 | 25 | it "adjusts column number" do 26 | subject.adjust(column_number: 1).to_s.should eq ":2:4" 27 | end 28 | 29 | it "adjusts line and column numbers" do 30 | subject.adjust(line_number: 1, column_number: 2).to_s.should eq ":3:5" 31 | end 32 | end 33 | 34 | describe "#seek" do 35 | it "adjusts column number if line offset is 1" do 36 | subject.seek(Crystal::Location.new(nil, 1, 2)).to_s.should eq ":2:4" 37 | end 38 | 39 | it "adjusts line number and changes column number if line offset is greater than 1" do 40 | subject.seek(Crystal::Location.new(nil, 2, 1)).to_s.should eq ":3:1" 41 | end 42 | 43 | it "adjusts line number and changes column number if line offset is less than 1" do 44 | subject.seek(Crystal::Location.new(nil, 0, 1)).to_s.should eq ":1:1" 45 | end 46 | 47 | it "raises exception if filenames don't match" do 48 | expect_raises(ArgumentError, "Mismatching filenames:\n source.cr\n source2.cr") do 49 | location = Crystal::Location.new("source.cr", 1, 1) 50 | location.seek(Crystal::Location.new("source2.cr", 1, 1)) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/ameba/formatter/disabled_formatter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba::Formatter 4 | describe DisabledFormatter do 5 | output = IO::Memory.new 6 | subject = DisabledFormatter.new output 7 | 8 | before_each do 9 | output.clear 10 | end 11 | 12 | describe "#finished" do 13 | it "writes a final message" do 14 | subject.finished [Source.new ""] 15 | output.to_s.should contain "Disabled rules using inline directives:" 16 | end 17 | 18 | it "writes disabled rules if any" do 19 | Colorize.enabled = false 20 | 21 | path = "source.cr" 22 | s = Source.new("", path).tap do |source| 23 | source.add_issue(ErrorRule.new, {1, 2}, message: "ErrorRule", status: :disabled) 24 | source.add_issue(NamedRule.new, location: {2, 2}, message: "NamedRule", status: :disabled) 25 | end 26 | subject.finished [s] 27 | log = output.to_s 28 | log.should contain "#{path}:1 #{ErrorRule.rule_name}" 29 | log.should contain "#{path}:2 #{NamedRule.rule_name}" 30 | ensure 31 | Colorize.enabled = true 32 | end 33 | 34 | it "does not write not-disabled rules" do 35 | s = Source.new("", "source.cr").tap do |source| 36 | source.add_issue(ErrorRule.new, {1, 2}, "ErrorRule") 37 | source.add_issue(NamedRule.new, location: {2, 2}, 38 | message: "NamedRule", status: :disabled) 39 | end 40 | subject.finished [s] 41 | output.to_s.should_not contain ErrorRule.rule_name 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/ameba/formatter/flycheck_formatter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba::Formatter 4 | describe FlycheckFormatter do 5 | output = IO::Memory.new 6 | subject = FlycheckFormatter.new output 7 | 8 | before_each do 9 | output.clear 10 | end 11 | 12 | context "problems not found" do 13 | it "reports nothing" do 14 | subject.source_finished Source.new "" 15 | subject.output.to_s.empty?.should be_true 16 | end 17 | end 18 | 19 | context "when problems found" do 20 | it "reports an issue" do 21 | s = Source.new "a = 1", "source.cr" 22 | s.add_issue DummyRule.new, {1, 2}, "message" 23 | 24 | subject.source_finished s 25 | subject.output.to_s.should eq( 26 | "source.cr:1:2: C: [#{DummyRule.rule_name}] message\n" 27 | ) 28 | end 29 | 30 | it "properly reports multi-line message" do 31 | s = Source.new "a = 1", "source.cr" 32 | s.add_issue DummyRule.new, {1, 2}, "multi\nline" 33 | 34 | subject.source_finished s 35 | subject.output.to_s.should eq( 36 | "source.cr:1:2: C: [#{DummyRule.rule_name}] multi line\n" 37 | ) 38 | end 39 | 40 | it "reports nothing if location was not set" do 41 | s = Source.new "a = 1", "source.cr" 42 | s.add_issue DummyRule.new, Crystal::Nop.new, "message" 43 | 44 | subject.source_finished s 45 | subject.output.to_s.should eq "" 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/ameba/issue_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module Ameba 4 | describe Issue do 5 | it "accepts rule and message" do 6 | issue = Issue.new code: "", 7 | rule: DummyRule.new, 8 | location: nil, 9 | end_location: nil, 10 | message: "Blah", 11 | status: nil 12 | 13 | issue.rule.should_not be_nil 14 | issue.message.should eq "Blah" 15 | end 16 | 17 | it "accepts location" do 18 | location = Crystal::Location.new("path", 3, 2) 19 | issue = Issue.new code: "", 20 | rule: DummyRule.new, 21 | location: location, 22 | end_location: nil, 23 | message: "Blah", 24 | status: nil 25 | 26 | issue.location.to_s.should eq location.to_s 27 | issue.end_location.should be_nil 28 | end 29 | 30 | it "accepts end_location" do 31 | location = Crystal::Location.new("path", 3, 2) 32 | issue = Issue.new code: "", 33 | rule: DummyRule.new, 34 | location: nil, 35 | end_location: location, 36 | message: "Blah", 37 | status: nil 38 | 39 | issue.location.should be_nil 40 | issue.end_location.to_s.should eq location.to_s 41 | end 42 | 43 | it "accepts status" do 44 | issue = Issue.new code: "", 45 | rule: DummyRule.new, 46 | location: nil, 47 | end_location: nil, 48 | message: "", 49 | status: :disabled 50 | 51 | issue.status.should eq Issue::Status::Disabled 52 | issue.disabled?.should be_true 53 | issue.enabled?.should be_false 54 | end 55 | 56 | it "sets status to :enabled by default" do 57 | issue = Issue.new code: "", 58 | rule: DummyRule.new, 59 | location: nil, 60 | end_location: nil, 61 | message: "" 62 | 63 | issue.status.should eq Issue::Status::Enabled 64 | issue.enabled?.should be_true 65 | issue.disabled?.should be_false 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/ameba/presenter/rule_collection_presenter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba 4 | private def with_rule_collection_presenter(&) 5 | rules = Config.load.rules 6 | 7 | with_presenter(Presenter::RuleCollectionPresenter, rules) do |presenter, output| 8 | yield rules, output, presenter 9 | end 10 | end 11 | 12 | describe Presenter::RuleCollectionPresenter do 13 | it "outputs rule collection details" do 14 | with_rule_collection_presenter do |rules, output| 15 | rules.each do |rule| 16 | output.should contain rule.name 17 | output.should contain rule.severity.symbol 18 | 19 | if description = rule.description 20 | output.should contain description 21 | end 22 | end 23 | output.should contain "Total rules: #{rules.size}" 24 | output.should match /\d+ enabled/ 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/ameba/presenter/rule_presenter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba 4 | private def rule_presenter_each_rule(&) 5 | rules = Config.load.rules 6 | 7 | rules.each do |rule| 8 | with_presenter(Presenter::RulePresenter, rule) do |presenter, output| 9 | yield rule, output, presenter 10 | end 11 | end 12 | end 13 | 14 | describe Presenter::RulePresenter do 15 | it "outputs rule details" do 16 | rule_presenter_each_rule do |rule, output| 17 | output.should contain rule.name 18 | output.should contain rule.severity.to_s 19 | 20 | if description = rule.description 21 | output.should contain description 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/ameba/presenter/rule_versions_presenter_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba 4 | private def with_rule_versions_presenter(&) 5 | rules = Config.load.rules 6 | 7 | with_presenter(Presenter::RuleVersionsPresenter, rules) do |presenter, output| 8 | yield rules, output, presenter 9 | end 10 | end 11 | 12 | describe Presenter::RuleVersionsPresenter do 13 | it "outputs rule versions" do 14 | with_rule_versions_presenter do |_rules, output| 15 | output.should contain <<-TEXT 16 | - 0.1.0 17 | - Layout/LineLength 18 | - Layout/TrailingBlankLines 19 | - Layout/TrailingWhitespace 20 | - Lint/ComparisonToBoolean 21 | - Lint/DebuggerStatement 22 | - Lint/LiteralInCondition 23 | - Lint/LiteralInInterpolation 24 | - Style/UnlessElse 25 | TEXT 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/ameba/reportable_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module Ameba 4 | describe Reportable do 5 | describe "#add_issue" do 6 | it "adds a new issue for node" do 7 | s = Source.new "", "source.cr" 8 | s.add_issue(DummyRule.new, Crystal::Nop.new, "Error!") 9 | 10 | issue = s.issues.first 11 | issue.rule.should_not be_nil 12 | issue.location.to_s.should eq "" 13 | issue.message.should eq "Error!" 14 | end 15 | 16 | it "adds a new issue by line and column number" do 17 | s = Source.new "", "source.cr" 18 | s.add_issue(DummyRule.new, {23, 2}, "Error!") 19 | 20 | issue = s.issues.first 21 | issue.rule.should_not be_nil 22 | issue.location.to_s.should eq "source.cr:23:2" 23 | issue.message.should eq "Error!" 24 | end 25 | end 26 | 27 | describe "#valid?" do 28 | it "returns true if no issues added" do 29 | s = Source.new "", "source.cr" 30 | s.should be_valid 31 | end 32 | 33 | it "returns false if there are issues added" do 34 | s = Source.new "", "source.cr" 35 | s.add_issue DummyRule.new, {22, 2}, "ERROR!" 36 | s.should_not be_valid 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/ameba/rule/base_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Ameba 4 | describe Rule::Base do 5 | describe "#catch" do 6 | it "accepts and returns source" do 7 | s = Source.new "", "" 8 | DummyRule.new.catch(s).should eq s 9 | end 10 | end 11 | 12 | describe "#name" do 13 | it "returns name of the rule" do 14 | DummyRule.new.name.should eq "Ameba/DummyRule" 15 | end 16 | end 17 | 18 | describe "#group" do 19 | it "returns a group rule belongs to" do 20 | DummyRule.new.group.should eq "Ameba" 21 | end 22 | end 23 | end 24 | 25 | describe Rule do 26 | describe ".rules" do 27 | it "returns a list of all defined rules" do 28 | Rule.rules.includes?(DummyRule).should be_true 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/ameba/rule/layout/line_length_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Layout 4 | describe LineLength do 5 | subject = LineLength.new 6 | long_line = "*" * (subject.max_length + 1) 7 | 8 | it "passes if all lines are shorter than MaxLength symbols" do 9 | expect_no_issues subject, <<-CRYSTAL 10 | short line 11 | CRYSTAL 12 | end 13 | 14 | it "passes if line consists of MaxLength symbols" do 15 | expect_no_issues subject, <<-CRYSTAL 16 | #{"*" * subject.max_length} 17 | CRYSTAL 18 | end 19 | 20 | it "fails if there is at least one line longer than MaxLength symbols" do 21 | source = Source.new long_line 22 | subject.catch(source).should_not be_valid 23 | end 24 | 25 | it "reports rule, pos and message" do 26 | source = Source.new long_line, "source.cr" 27 | subject.catch(source).should_not be_valid 28 | 29 | issue = source.issues.first 30 | issue.rule.should eq subject 31 | issue.location.to_s.should eq "source.cr:1:#{subject.max_length + 1}" 32 | issue.end_location.should be_nil 33 | issue.message.should eq "Line too long" 34 | end 35 | 36 | context "properties" do 37 | it "#max_length" do 38 | rule = LineLength.new 39 | rule.max_length = long_line.size 40 | 41 | expect_no_issues rule, long_line 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/ameba/rule/layout/trailing_whitespace_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Layout 4 | describe TrailingWhitespace do 5 | subject = TrailingWhitespace.new 6 | 7 | it "passes if all lines do not have trailing whitespace" do 8 | expect_no_issues subject, "no-whitespace" 9 | end 10 | 11 | it "fails if there is a line with trailing whitespace" do 12 | source = expect_issue subject, 13 | "whitespace at the end \n" \ 14 | " # ^^ error: Trailing whitespace detected" 15 | 16 | expect_correction source, "whitespace at the end" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/bad_directive_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe BadDirective do 5 | subject = BadDirective.new 6 | 7 | it "does not report if rule is correct" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | # ameba:disable Lint/BadDirective 10 | CRYSTAL 11 | end 12 | 13 | it "reports if there is incorrect action" do 14 | expect_issue subject, <<-CRYSTAL 15 | # ameba:foo Lint/BadDirective 16 | # ^^^ error: Bad action in comment directive: 'foo'. Possible values: disable, enable 17 | CRYSTAL 18 | end 19 | 20 | it "reports if there are incorrect rule names" do 21 | expect_issue subject, <<-CRYSTAL 22 | # ameba:enable BadRule1, BadRule2 23 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Such rules do not exist: BadRule1, BadRule2 24 | CRYSTAL 25 | end 26 | 27 | it "does not report if there no action and rules at all" do 28 | expect_no_issues subject, <<-CRYSTAL 29 | # ameba: 30 | CRYSTAL 31 | end 32 | 33 | it "does not report if there are no rules" do 34 | expect_no_issues subject, <<-CRYSTAL 35 | # ameba:enable 36 | # ameba:disable 37 | CRYSTAL 38 | end 39 | 40 | it "does not report if there are group names in the directive" do 41 | expect_no_issues subject, <<-CRYSTAL 42 | # ameba:disable Style Performance 43 | CRYSTAL 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/debug_calls_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe DebugCalls do 5 | subject = DebugCalls.new 6 | 7 | it "fails if there is a debug call" do 8 | subject.method_names.each do |name| 9 | source = expect_issue subject, <<-CRYSTAL, name: name 10 | a = 2 11 | %{name} a 12 | # ^{name} error: Possibly forgotten debug-related `%{name}` call detected 13 | a = a + 1 14 | CRYSTAL 15 | 16 | expect_no_corrections source 17 | end 18 | end 19 | 20 | it "passes if there is no debug call" do 21 | subject.method_names.each do |name| 22 | expect_no_issues subject, <<-CRYSTAL 23 | class A 24 | def #{name} 25 | end 26 | end 27 | A.new.#{name} 28 | CRYSTAL 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/debugger_statement_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe DebuggerStatement do 5 | subject = DebuggerStatement.new 6 | 7 | it "passes if there is no debugger statement" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | "this is not a debugger statement" 10 | s = "debugger" 11 | 12 | def debugger(program) 13 | end 14 | debugger "" 15 | 16 | class A 17 | def debugger 18 | end 19 | end 20 | A.new.debugger 21 | CRYSTAL 22 | end 23 | 24 | it "fails if there is a debugger statement" do 25 | source = expect_issue subject, <<-CRYSTAL 26 | a = 2 27 | debugger 28 | # ^^^^^^ error: Possible forgotten debugger statement detected 29 | a = a + 1 30 | CRYSTAL 31 | 32 | expect_no_corrections source 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/duplicate_when_condition_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe DuplicateWhenCondition do 5 | subject = DuplicateWhenCondition.new 6 | 7 | it "passes if there are no duplicated `when` conditions" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | case x 10 | when .nil? 11 | do_something 12 | when Symbol 13 | do_something_else 14 | end 15 | CRYSTAL 16 | end 17 | 18 | it "reports if there are a duplicated `when` conditions in `case` expression" do 19 | expect_issue subject, <<-CRYSTAL 20 | case x 21 | when .foo?, .nil? 22 | do_something 23 | when .nil? 24 | # ^^^^^ error: Duplicate `when` condition detected 25 | do_something_else 26 | end 27 | CRYSTAL 28 | 29 | expect_issue subject, <<-CRYSTAL 30 | case 31 | when foo? 32 | :foo 33 | when foo?, bar? 34 | # ^^^^ error: Duplicate `when` condition detected 35 | :foobar 36 | when Time.utc.year == 1996 37 | :yo 38 | when Time.utc.year == 1996 39 | # ^^^^^^^^^^^^^^^^^^^^^ error: Duplicate `when` condition detected 40 | :yo 41 | end 42 | CRYSTAL 43 | end 44 | 45 | it "reports if there are a duplicated `when` conditions in `select` expression" do 46 | expect_issue subject, <<-CRYSTAL 47 | select 48 | when foo = foo_channel.receive 49 | puts foo 50 | when foo = foo_channel.receive 51 | # ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicate `when` condition detected 52 | puts foo 53 | when bar = bar_channel.receive? 54 | puts bar 55 | end 56 | CRYSTAL 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/duplicated_require_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe DuplicatedRequire do 5 | subject = DuplicatedRequire.new 6 | 7 | it "passes if there are no duplicated requires" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | require "math" 10 | require "big" 11 | require "big/big_decimal" 12 | CRYSTAL 13 | end 14 | 15 | it "reports if there are a duplicated requires" do 16 | source = expect_issue subject, <<-CRYSTAL 17 | require "big" 18 | require "math" 19 | require "big" 20 | # ^^^^^^^^^^^ error: Duplicated require of `big` 21 | CRYSTAL 22 | 23 | expect_no_corrections source 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/else_nil_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe ElseNil do 5 | subject = ElseNil.new 6 | 7 | {% for keyword in %w[if unless].map(&.id) %} 8 | it "does not report if `else` block of an `{{ keyword }}` has a non-nil body" do 9 | expect_no_issues subject, <<-CRYSTAL 10 | {{ keyword }} foo 11 | do_foo 12 | else 13 | do_bar 14 | end 15 | CRYSTAL 16 | end 17 | 18 | it "reports if there is an `else` block of an `{{ keyword }}` with a `nil` body" do 19 | source = expect_issue subject, <<-CRYSTAL 20 | {{ keyword }} foo 21 | do_foo 22 | else 23 | nil 24 | # ^^^ error: Avoid `else` blocks with `nil` as their body 25 | end 26 | CRYSTAL 27 | 28 | expect_correction source, <<-CRYSTAL 29 | {{ keyword }} foo 30 | do_foo 31 | end 32 | CRYSTAL 33 | end 34 | {% end %} 35 | 36 | it "does not report if there is an `else` block of an ternary `if` with a `nil` body" do 37 | expect_no_issues subject, <<-CRYSTAL 38 | foo ? do_foo : nil 39 | CRYSTAL 40 | end 41 | 42 | it "does not report if `else` block of a `case` has a non-nil body" do 43 | expect_no_issues subject, <<-CRYSTAL 44 | case foo 45 | when :foo 46 | do_foo 47 | else 48 | do_bar 49 | end 50 | CRYSTAL 51 | end 52 | 53 | it "reports if there is an `else` block of a `case` with a `nil` body" do 54 | source = expect_issue subject, <<-CRYSTAL 55 | case foo 56 | when :foo 57 | do_foo 58 | else 59 | nil 60 | # ^^^ error: Avoid `else` blocks with `nil` as their body 61 | end 62 | CRYSTAL 63 | 64 | expect_no_corrections source 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/empty_ensure_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe EmptyEnsure do 5 | subject = EmptyEnsure.new 6 | 7 | it "passes if there is no empty ensure blocks" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def some_method 10 | do_some_stuff 11 | ensure 12 | do_something_else 13 | end 14 | 15 | begin 16 | do_some_stuff 17 | ensure 18 | do_something_else 19 | end 20 | 21 | def method_with_rescue 22 | rescue 23 | ensure 24 | nil 25 | end 26 | CRYSTAL 27 | end 28 | 29 | it "fails if there is an empty ensure in method" do 30 | expect_issue subject, <<-CRYSTAL 31 | def method 32 | do_some_stuff 33 | ensure 34 | # ^^^^ error: Empty `ensure` block detected 35 | end 36 | CRYSTAL 37 | end 38 | 39 | it "fails if there is an empty ensure in a block" do 40 | expect_issue subject, <<-CRYSTAL 41 | begin 42 | do_some_stuff 43 | rescue 44 | do_some_other_stuff 45 | ensure 46 | # ^^^^ error: Empty `ensure` block detected 47 | # nothing here 48 | end 49 | CRYSTAL 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/empty_loop_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe EmptyLoop do 5 | subject = EmptyLoop.new 6 | 7 | it "does not report if there are not empty loops" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | a = 1 10 | 11 | while a < 10 12 | a += 1 13 | end 14 | 15 | until a == 10 16 | a += 1 17 | end 18 | 19 | loop do 20 | a += 1 21 | end 22 | CRYSTAL 23 | end 24 | 25 | it "reports if there is an empty while loop" do 26 | expect_issue subject, <<-CRYSTAL 27 | a = 1 28 | while true 29 | # ^^^^^^^^ error: Empty loop detected 30 | end 31 | CRYSTAL 32 | end 33 | 34 | it "doesn't report if while loop has non-literals in cond block" do 35 | expect_no_issues subject, <<-CRYSTAL 36 | a = 1 37 | while a = gets.to_s 38 | # nothing here 39 | end 40 | CRYSTAL 41 | end 42 | 43 | it "reports if there is an empty until loop" do 44 | expect_issue subject, <<-CRYSTAL 45 | do_something 46 | until false 47 | # ^^^^^^^^^ error: Empty loop detected 48 | end 49 | CRYSTAL 50 | end 51 | 52 | it "doesn't report if until loop has non-literals in cond block" do 53 | expect_no_issues subject, <<-CRYSTAL 54 | until socket_open? 55 | end 56 | CRYSTAL 57 | end 58 | 59 | it "reports if there an empty loop" do 60 | expect_issue subject, <<-CRYSTAL 61 | a = 1 62 | loop do 63 | # ^^^^^ error: Empty loop detected 64 | end 65 | CRYSTAL 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/formatting_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe Formatting do 5 | subject = Formatting.new 6 | 7 | it "passes if source is formatted" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def method(a, b) 10 | a + b 11 | end 12 | 13 | CRYSTAL 14 | end 15 | 16 | it "reports if source is not formatted" do 17 | source = expect_issue subject, <<-CRYSTAL 18 | def method(a,b,c=0) 19 | # ^{} error: Use built-in formatter to format this source 20 | a+b+c 21 | end 22 | 23 | CRYSTAL 24 | 25 | expect_correction source, <<-CRYSTAL 26 | def method(a, b, c = 0) 27 | a + b + c 28 | end 29 | 30 | CRYSTAL 31 | end 32 | 33 | context "properties" do 34 | context "#fail_on_error" do 35 | it "passes on formatter errors by default" do 36 | rule = Formatting.new 37 | 38 | expect_no_issues rule, <<-CRYSTAL 39 | def method(a, b) 40 | a + b 41 | CRYSTAL 42 | end 43 | 44 | it "reports on formatter errors when enabled" do 45 | rule = Formatting.new 46 | rule.fail_on_error = true 47 | 48 | expect_issue rule, <<-CRYSTAL 49 | def method(a, b) 50 | a + b 51 | # ^ error: Error while formatting: expecting identifier 'end', not 'EOF' 52 | CRYSTAL 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/hash_duplicated_key_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe HashDuplicatedKey do 5 | subject = HashDuplicatedKey.new 6 | 7 | it "passes if there is no duplicated keys in a hash literals" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | h = {"a" => 1, :a => 2, "b" => 3} 10 | h = {"a" => 1, "b" => 2, "c" => {"a" => 3, "b" => 4}} 11 | h = {} of String => String 12 | CRYSTAL 13 | end 14 | 15 | it "fails if there is a duplicated key in a hash literal" do 16 | expect_issue subject, <<-CRYSTAL 17 | h = {"a" => 1, "b" => 2, "a" => 3} 18 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicated keys in hash literal: `"a"` 19 | CRYSTAL 20 | end 21 | 22 | it "fails if there is a duplicated key in the inner hash literal" do 23 | expect_issue subject, <<-CRYSTAL 24 | h = {"a" => 1, "b" => {"a" => 3, "b" => 4, "a" => 5}} 25 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicated keys in hash literal: `"a"` 26 | CRYSTAL 27 | end 28 | 29 | it "reports multiple duplicated keys" do 30 | expect_issue subject, <<-CRYSTAL 31 | h = {"key1" => 1, "key1" => 2, "key2" => 3, "key2" => 4} 32 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicated keys in hash literal: `"key1"`, `"key2"` 33 | CRYSTAL 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/literal_assignments_in_expressions_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | LITERAL_SAMPLES = { 4 | nil, true, 42, 4.2, 'c', "foo", :foo, /foo/, 5 | 0..42, [1, 2, 3], {1, 2, 3}, 6 | {foo: :bar}, {:foo => :bar}, 7 | } 8 | 9 | module Ameba::Rule::Lint 10 | describe LiteralAssignmentsInExpressions do 11 | subject = LiteralAssignmentsInExpressions.new 12 | 13 | it "passes if the assignment value is not a literal" do 14 | expect_no_issues subject, <<-CRYSTAL 15 | if a = b 16 | :ok 17 | end 18 | 19 | unless a = b.presence 20 | :ok 21 | end 22 | 23 | :ok if a = b 24 | :ok unless a = b 25 | 26 | case {a, b} 27 | when {0, 1} then :gt 28 | when {1, 0} then :lt 29 | end 30 | CRYSTAL 31 | end 32 | 33 | {% for literal in LITERAL_SAMPLES %} 34 | it %(reports if the assignment value is a {{ literal }} literal) do 35 | expect_issue subject, <<-CRYSTAL, literal: {{ literal.stringify }} 36 | raise "boo!" if foo = {{ literal }} 37 | # ^{literal}^^^^^^ error: Detected assignment with a literal value in control expression 38 | CRYSTAL 39 | 40 | expect_issue subject, <<-CRYSTAL, literal: {{ literal.stringify }} 41 | raise "boo!" unless foo = {{ literal }} 42 | # ^{literal}^^^^^^ error: Detected assignment with a literal value in control expression 43 | CRYSTAL 44 | end 45 | {% end %} 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/literal_in_interpolation_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe LiteralInInterpolation do 5 | subject = LiteralInInterpolation.new 6 | 7 | it "passes with good interpolation examples" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | name = "Ary" 10 | "Hello, #{name}" 11 | 12 | "#{name}" 13 | 14 | "Name size: #{name.size}" 15 | CRYSTAL 16 | end 17 | 18 | it "fails if there is useless interpolation" do 19 | [ 20 | %q("#{:Ary}"), 21 | %q("#{[1, 2, 3]}"), 22 | %q("#{true}"), 23 | %q("#{false}"), 24 | %q("here are #{4} cats"), 25 | ].each do |str| 26 | subject.catch(Source.new str).should_not be_valid 27 | end 28 | end 29 | 30 | it "works with magic constants (#593)" do 31 | expect_no_issues subject, <<-'CRYSTAL', "/home/foo/source.cr" 32 | "Hello from #{__FILE__} at line #{__LINE__} in #{__DIR__}" 33 | CRYSTAL 34 | end 35 | 36 | it "reports rule, pos and message" do 37 | s = Source.new %q( 38 | "Hello, #{:world} from #{:ameba}" 39 | ), "source.cr" 40 | subject.catch(s).should_not be_valid 41 | s.issues.size.should eq 2 42 | 43 | issue = s.issues.first 44 | issue.rule.should_not be_nil 45 | issue.location.to_s.should eq "source.cr:1:11" 46 | issue.end_location.to_s.should eq "source.cr:1:16" 47 | issue.message.should eq "Literal value found in interpolation" 48 | 49 | issue = s.issues.last 50 | issue.rule.should_not be_nil 51 | issue.location.to_s.should eq "source.cr:1:26" 52 | issue.end_location.to_s.should eq "source.cr:1:31" 53 | issue.message.should eq "Literal value found in interpolation" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/missing_block_argument_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe MissingBlockArgument do 5 | subject = MissingBlockArgument.new 6 | 7 | it "passes if the block argument is defined" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def foo(&) 10 | yield 42 11 | end 12 | 13 | def bar(&block) 14 | yield 24 15 | end 16 | 17 | def baz(a, b, c, &block) 18 | yield a, b, c 19 | end 20 | CRYSTAL 21 | end 22 | 23 | it "reports if the block argument is missing" do 24 | expect_issue subject, <<-CRYSTAL 25 | def foo 26 | # ^^^ error: Missing anonymous block argument. Use `&` as an argument name to indicate yielding method. 27 | yield 42 28 | end 29 | 30 | def bar 31 | # ^^^ error: Missing anonymous block argument. Use `&` as an argument name to indicate yielding method. 32 | yield 24 33 | end 34 | 35 | def baz(a, b, c) 36 | # ^^^ error: Missing anonymous block argument. Use `&` as an argument name to indicate yielding method. 37 | yield a, b, c 38 | end 39 | CRYSTAL 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/not_nil_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe NotNil do 5 | subject = NotNil.new 6 | 7 | it "passes for valid cases" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | (1..3).first?.not_nil!(:foo) 10 | not_nil! 11 | CRYSTAL 12 | end 13 | 14 | it "reports if there is a `not_nil!` call" do 15 | expect_issue subject, <<-CRYSTAL 16 | (1..3).first?.not_nil! 17 | # ^^^^^^^^ error: Avoid using `not_nil!` 18 | CRYSTAL 19 | end 20 | 21 | it "reports if there is a `not_nil!` call in the middle of the call-chain" do 22 | expect_issue subject, <<-CRYSTAL 23 | (1..3).first?.not_nil!.to_s 24 | # ^^^^^^^^ error: Avoid using `not_nil!` 25 | CRYSTAL 26 | end 27 | 28 | context "macro" do 29 | it "doesn't report in macro scope" do 30 | expect_no_issues subject, <<-CRYSTAL 31 | {{ [1, 2, 3].first.not_nil! }} 32 | CRYSTAL 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/rand_zero_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe RandZero do 5 | subject = RandZero.new 6 | 7 | it "passes if it is not rand(1) or rand(0)" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | rand(1.0) 10 | rand(0.11) 11 | rand(2) 12 | CRYSTAL 13 | end 14 | 15 | it "fails if it is rand(0)" do 16 | expect_issue subject, <<-CRYSTAL 17 | rand(0) 18 | # ^^^^^ error: `rand(0)` always returns `0` 19 | CRYSTAL 20 | end 21 | 22 | it "fails if it is rand(1)" do 23 | expect_issue subject, <<-CRYSTAL 24 | rand(1) 25 | # ^^^^^ error: `rand(1)` always returns `0` 26 | CRYSTAL 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/require_parentheses_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe RequireParentheses do 5 | subject = RequireParentheses.new 6 | 7 | it "passes if logical operator in call args has parentheses" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | foo.includes?("bar") || foo.includes?("baz") 10 | foo.includes?("bar" || foo.includes? "baz") 11 | CRYSTAL 12 | end 13 | 14 | it "passes if logical operator in call doesn't involve another method call" do 15 | expect_no_issues subject, <<-CRYSTAL 16 | foo.includes? "bar" || "baz" 17 | CRYSTAL 18 | end 19 | 20 | it "passes if logical operator in call involves another method call with no arguments" do 21 | expect_no_issues subject, <<-CRYSTAL 22 | foo.includes? "bar" || foo.not_nil! 23 | CRYSTAL 24 | end 25 | 26 | it "passes if logical operator is used in an assignment call" do 27 | expect_no_issues subject, <<-CRYSTAL 28 | foo.bar = "baz" || bat.call :foo 29 | foo.bar ||= "baz" || bat.call :foo 30 | foo[bar] = "baz" || bat.call :foo 31 | CRYSTAL 32 | end 33 | 34 | it "passes if logical operator is used in a square bracket call" do 35 | expect_no_issues subject, <<-CRYSTAL 36 | foo["bar" || baz.call :bat] 37 | foo["bar" || baz.call :bat]? 38 | CRYSTAL 39 | end 40 | 41 | it "fails if logical operator in call args doesn't have parentheses" do 42 | expect_issue subject, <<-CRYSTAL 43 | foo.includes? "bar" || foo.includes? "baz" 44 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use parentheses in the method call to avoid confusion about precedence 45 | 46 | foo.in? "bar", "baz" || foo.ends_with? "bat" 47 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use parentheses in the method call to avoid confusion about precedence 48 | CRYSTAL 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/signal_trap_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe SignalTrap do 5 | subject = SignalTrap.new 6 | 7 | it "reports when `Signal::INT/HUP/TERM.trap` is used" do 8 | source = expect_issue subject, <<-CRYSTAL 9 | ::Signal::INT.trap { shutdown } 10 | # ^^^^^^^^^^^^^^^^ error: Use `Process.on_terminate` instead of `::Signal::INT.trap` 11 | Signal::HUP.trap { shutdown } 12 | # ^^^^^^^^^^^^^^ error: Use `Process.on_terminate` instead of `Signal::HUP.trap` 13 | Signal::TERM.trap &shutdown 14 | # ^^^^^^^^^^^^^^^ error: Use `Process.on_terminate` instead of `Signal::TERM.trap` 15 | CRYSTAL 16 | 17 | expect_correction source, <<-CRYSTAL 18 | Process.on_terminate { shutdown } 19 | Process.on_terminate { shutdown } 20 | Process.on_terminate &shutdown 21 | CRYSTAL 22 | end 23 | 24 | it "respects the comment between the path and the call name" do 25 | source = expect_issue subject, <<-CRYSTAL 26 | Signal::INT 27 | # ^^^^^^^^^ error: Use `Process.on_terminate` instead of `Signal::INT.trap` 28 | # foo 29 | .trap { shutdown } 30 | CRYSTAL 31 | 32 | expect_correction source, <<-CRYSTAL 33 | Process 34 | # foo 35 | .on_terminate { shutdown } 36 | CRYSTAL 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/spec_filename_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe SpecFilename do 5 | subject = SpecFilename.new 6 | 7 | it "passes if relative file path does not start with `spec/`" do 8 | expect_no_issues subject, code: "", path: "src/spec/foo.cr" 9 | expect_no_issues subject, code: "", path: "src/spec/foo/bar.cr" 10 | end 11 | 12 | it "passes if file extension is not `.cr`" do 13 | expect_no_issues subject, code: "", path: "spec/foo.json" 14 | expect_no_issues subject, code: "", path: "spec/foo/bar.json" 15 | end 16 | 17 | it "passes if filename is correct" do 18 | expect_no_issues subject, code: "", path: "spec/foo_spec.cr" 19 | expect_no_issues subject, code: "", path: "spec/foo/bar_spec.cr" 20 | end 21 | 22 | it "fails if filename is wrong" do 23 | expect_issue subject, <<-CRYSTAL, path: "spec/foo.cr" 24 | 25 | # ^{} error: Spec filename should have `_spec` suffix: `foo_spec.cr`, not `foo.cr` 26 | CRYSTAL 27 | end 28 | 29 | context "properties" do 30 | context "#ignored_dirs" do 31 | it "provide sane defaults" do 32 | expect_no_issues subject, code: "", path: "spec/support/foo.cr" 33 | expect_no_issues subject, code: "", path: "spec/fixtures/foo.cr" 34 | expect_no_issues subject, code: "", path: "spec/data/foo.cr" 35 | end 36 | end 37 | 38 | context "#ignored_filenames" do 39 | it "ignores spec_helper by default" do 40 | expect_no_issues subject, code: "", path: "spec/spec_helper.cr" 41 | expect_no_issues subject, code: "", path: "spec/foo/spec_helper.cr" 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/syntax_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe Syntax do 5 | subject = Syntax.new 6 | 7 | it "passes if there is no invalid syntax" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def hello 10 | puts "totally valid" 11 | rescue e : Exception 12 | end 13 | CRYSTAL 14 | end 15 | 16 | it "fails if there is an invalid syntax" do 17 | expect_issue subject, <<-CRYSTAL 18 | def hello 19 | puts "invalid" 20 | rescue Exception => e 21 | # ^ error: expecting any of these tokens: ;, NEWLINE (not '=>') 22 | end 23 | CRYSTAL 24 | end 25 | 26 | it "has highest severity" do 27 | subject.severity.should eq Severity::Error 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/trailing_rescue_exception_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe TrailingRescueException do 5 | subject = TrailingRescueException.new 6 | 7 | it "passes for trailing rescue with literal values" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | puts "foo" rescue "bar" 10 | puts :foo rescue 42 11 | CRYSTAL 12 | end 13 | 14 | it "passes for trailing rescue with class initialization" do 15 | expect_no_issues subject, <<-CRYSTAL 16 | puts "foo" rescue MyClass.new 17 | CRYSTAL 18 | end 19 | 20 | it "fails if trailing rescue has exception name" do 21 | expect_issue subject, <<-CRYSTAL 22 | puts "hello" rescue MyException 23 | # ^^^^^^^^^^^ error: Use a block variant of `rescue` to filter by the exception type 24 | CRYSTAL 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/typos_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | private def check_typos_bin! 4 | unless Ameba::Rule::Lint::Typos::BIN_PATH 5 | pending! "`typos` executable is not available" 6 | end 7 | end 8 | 9 | module Ameba::Rule::Lint 10 | describe Typos do 11 | subject = Typos.new 12 | subject.fail_on_error = true 13 | 14 | it "reports typos" do 15 | check_typos_bin! 16 | 17 | source = expect_issue subject, <<-CRYSTAL 18 | # method with no arugments 19 | # ^^^^^^^^^ error: Typo found: `arugments` -> `arguments` 20 | def tpos 21 | # ^^^^ error: Typo found: `tpos` -> `typos` 22 | :otput 23 | # ^^^^^ error: Typo found: `otput` -> `output` 24 | end 25 | CRYSTAL 26 | 27 | expect_correction source, <<-CRYSTAL 28 | # method with no arguments 29 | def typos 30 | :output 31 | end 32 | CRYSTAL 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/unused_class_variable_access_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe UnusedClassVariableAccess do 5 | subject = UnusedClassVariableAccess.new 6 | 7 | it "passes if class variables are used for assignment" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | class MyClass 10 | foo = @@ivar 11 | end 12 | CRYSTAL 13 | end 14 | 15 | it "passes if an class variable is used as a target in multi-assignment" do 16 | expect_no_issues subject, <<-CRYSTAL 17 | class MyClass 18 | @@foo, @@bar = 1, 2 19 | end 20 | CRYSTAL 21 | end 22 | 23 | it "fails if class variables are unused in void context of class" do 24 | expect_issue subject, <<-CRYSTAL 25 | class Actor 26 | @@name : String = "George" 27 | 28 | @@name 29 | # ^^^^^^ error: Value from class variable access is unused 30 | end 31 | CRYSTAL 32 | end 33 | 34 | it "fails if class variables are unused in void context of method" do 35 | expect_issue subject, <<-'CRYSTAL' 36 | def hello : String 37 | @@name 38 | # ^^^^^^ error: Value from class variable access is unused 39 | 40 | "Hello, #{@@name}!" 41 | end 42 | CRYSTAL 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/unused_instance_variable_access_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe UnusedInstanceVariableAccess do 5 | subject = UnusedInstanceVariableAccess.new 6 | 7 | it "passes if instance variables are used for assignment" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | class MyClass 10 | foo = @ivar 11 | end 12 | CRYSTAL 13 | end 14 | 15 | it "passes if an instance variable is used as a target in multi-assignment" do 16 | expect_no_issues subject, <<-CRYSTAL 17 | class MyClass 18 | @foo, @bar = 1, 2 19 | end 20 | CRYSTAL 21 | end 22 | 23 | it "fails if instance variables are unused in void context of class" do 24 | expect_issue subject, <<-CRYSTAL 25 | class Actor 26 | @name : String = "George" 27 | 28 | @name 29 | # ^^^^^ error: Value from instance variable access is unused 30 | end 31 | CRYSTAL 32 | end 33 | 34 | it "fails if instance variables are unused in void context of method" do 35 | expect_issue subject, <<-'CRYSTAL' 36 | def hello : String 37 | @name 38 | # ^^^^^ error: Value from instance variable access is unused 39 | 40 | "Hello, #{@name}!" 41 | end 42 | CRYSTAL 43 | end 44 | 45 | it "passes if @type is unused within a macro expression" do 46 | expect_no_issues subject, <<-CRYSTAL 47 | def foo 48 | {% @type %} 49 | :bar 50 | end 51 | CRYSTAL 52 | end 53 | 54 | it "fails if instance variable is unused within a macro expression" do 55 | expect_issue subject, <<-CRYSTAL 56 | def foo 57 | {% @bar %} 58 | # ^^^^ error: Value from instance variable access is unused 59 | :baz 60 | end 61 | CRYSTAL 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/unused_local_variable_access_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe UnusedLocalVariableAccess do 5 | subject = UnusedLocalVariableAccess.new 6 | 7 | it "passes if local variables are used in assign" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | foo = 1 10 | foo += 1 11 | foo, bar = 2, 3 12 | CRYSTAL 13 | end 14 | 15 | it "passes if a local variable is a call argument" do 16 | expect_no_issues subject, <<-CRYSTAL 17 | foo = 1 18 | puts foo 19 | CRYSTAL 20 | end 21 | 22 | it "passes if local variable on left side of a comparison" do 23 | expect_no_issues subject, <<-CRYSTAL 24 | def hello 25 | foo = 1 26 | foo || (puts "foo is falsey") 27 | foo 28 | end 29 | CRYSTAL 30 | end 31 | 32 | it "passes if skip_file is used in a macro" do 33 | expect_no_issues subject, <<-CRYSTAL 34 | {% skip_file %} 35 | CRYSTAL 36 | end 37 | 38 | it "passes if debug is used in a macro" do 39 | expect_no_issues subject, <<-CRYSTAL 40 | {% debug %} 41 | CRYSTAL 42 | end 43 | 44 | it "fails if a local variable is in a void context" do 45 | expect_issue subject, <<-CRYSTAL 46 | foo = 1 47 | 48 | begin 49 | foo 50 | # ^^^ error: Value from local variable access is unused 51 | puts foo 52 | end 53 | CRYSTAL 54 | end 55 | 56 | it "fails if a parameter is in a void context" do 57 | expect_issue subject, <<-CRYSTAL 58 | def foo(bar) 59 | if bar > 0 60 | bar 61 | # ^^^ error: Value from local variable access is unused 62 | end 63 | 64 | nil 65 | end 66 | CRYSTAL 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/unused_self_access_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe UnusedSelfAccess do 5 | subject = UnusedSelfAccess.new 6 | 7 | it "passes if self is used as receiver for a method def" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def self.foo 10 | end 11 | CRYSTAL 12 | end 13 | 14 | it "passes if self is used as object of call" do 15 | expect_no_issues subject, <<-CRYSTAL 16 | self.foo 17 | CRYSTAL 18 | end 19 | 20 | it "passes if self is used as method of call" do 21 | expect_no_issues subject, <<-CRYSTAL 22 | foo.self 23 | CRYSTAL 24 | end 25 | 26 | it "fails if self is unused in void context of class body" do 27 | expect_issue subject, <<-CRYSTAL 28 | class MyClass 29 | self 30 | # ^^^^ error: `self` is not used 31 | end 32 | CRYSTAL 33 | end 34 | 35 | it "fails if self is unused in void context of begin" do 36 | expect_issue subject, <<-CRYSTAL 37 | begin 38 | self 39 | # ^^^^ error: `self` is not used 40 | 41 | "foo" 42 | end 43 | CRYSTAL 44 | end 45 | 46 | it "fails if self is unused in void context of method def" do 47 | expect_issue subject, <<-CRYSTAL 48 | def foo 49 | self 50 | # ^^^^ error: `self` is not used 51 | "bar" 52 | end 53 | CRYSTAL 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/ameba/rule/lint/useless_condition_in_when_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Lint 4 | describe UselessConditionInWhen do 5 | subject = UselessConditionInWhen.new 6 | 7 | it "passes if there is not useless condition" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | case 10 | when utc? 11 | io << " UTC" 12 | when local? 13 | Format.new(" %:z").format(self, io) if utc? 14 | end 15 | CRYSTAL 16 | end 17 | 18 | it "fails if there is useless if condition" do 19 | expect_issue subject, <<-CRYSTAL 20 | case 21 | when utc? 22 | io << " UTC" if utc? 23 | # ^^^^ error: Useless condition in `when` detected 24 | end 25 | CRYSTAL 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/ameba/rule/metrics/cyclomatic_complexity_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Metrics 4 | describe CyclomaticComplexity do 5 | subject = CyclomaticComplexity.new 6 | complex_method = <<-CRYSTAL 7 | def hello(a, b, c) 8 | if a && b && c 9 | begin 10 | while true 11 | return if false && b 12 | end 13 | "" 14 | rescue 15 | "" 16 | end 17 | end 18 | end 19 | CRYSTAL 20 | 21 | it "passes for empty methods" do 22 | expect_no_issues subject, <<-CRYSTAL 23 | def hello 24 | end 25 | CRYSTAL 26 | end 27 | 28 | it "reports one issue for a complex method" do 29 | rule = CyclomaticComplexity.new 30 | rule.max_complexity = 5 31 | 32 | source = Source.new(complex_method, "source.cr") 33 | rule.catch(source).should_not be_valid 34 | 35 | issue = source.issues.first 36 | issue.rule.should eq rule 37 | issue.location.to_s.should eq "source.cr:1:5" 38 | issue.end_location.to_s.should eq "source.cr:1:9" 39 | issue.message.should eq "Cyclomatic complexity too high [8/5]" 40 | end 41 | 42 | it "doesn't report an issue for an increased threshold" do 43 | rule = CyclomaticComplexity.new 44 | rule.max_complexity = 100 45 | 46 | expect_no_issues rule, complex_method 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/binary_operator_parameter_name_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Naming 4 | describe BinaryOperatorParameterName do 5 | subject = BinaryOperatorParameterName.new 6 | 7 | it "ignores `other` parameter name in binary method definitions" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def +(other); end 10 | def -(other); end 11 | def *(other); end 12 | CRYSTAL 13 | end 14 | 15 | it "ignores binary method definitions with arity other than 1" do 16 | expect_no_issues subject, <<-CRYSTAL 17 | def +; end 18 | def +(foo, bar); end 19 | def -; end 20 | def -(foo, bar); end 21 | CRYSTAL 22 | end 23 | 24 | it "ignores non-binary method definitions" do 25 | expect_no_issues subject, <<-CRYSTAL 26 | def foo(bar); end 27 | def bąk(genus); end 28 | CRYSTAL 29 | end 30 | 31 | it "reports binary methods definitions with incorrectly named parameter" do 32 | expect_issue subject, <<-CRYSTAL 33 | def +(foo); end 34 | # ^ error: When defining the `+` operator, name its argument `other` 35 | def -(foo); end 36 | # ^ error: When defining the `-` operator, name its argument `other` 37 | def *(foo); end 38 | # ^ error: When defining the `*` operator, name its argument `other` 39 | CRYSTAL 40 | end 41 | 42 | it "ignores methods from #excluded_operators" do 43 | subject.excluded_operators.each do |op| 44 | expect_no_issues subject, <<-CRYSTAL 45 | def #{op}(foo); end 46 | CRYSTAL 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/constant_names_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__) 4 | it "reports constant name #{expected}", file, line do 5 | rule = Ameba::Rule::Naming::ConstantNames.new 6 | expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line 7 | %{name} = #{value} 8 | # ^{name} error: Constant name should be screaming-cased: `#{expected}`, not `#{name}` 9 | CRYSTAL 10 | end 11 | end 12 | 13 | module Ameba::Rule::Naming 14 | describe ConstantNames do 15 | subject = ConstantNames.new 16 | 17 | it "passes if type names are screaming-cased" do 18 | expect_no_issues subject, <<-CRYSTAL 19 | LUCKY_NUMBERS = [3, 7, 11] 20 | DOCUMENTATION_URL = "https://crystal-lang.org/docs" 21 | 22 | Int32 23 | 24 | s : String = "str" 25 | 26 | def works(n : Int32) 27 | end 28 | 29 | Log = ::Log.for("db") 30 | 31 | a = 1 32 | myVar = 2 33 | m_var = 3 34 | CRYSTAL 35 | end 36 | 37 | # it_reports_constant "MyBadConstant", "1", "MYBADCONSTANT" 38 | it_reports_constant "Wrong_NAME", "2", "WRONG_NAME" 39 | it_reports_constant "Wrong_Name", "3", "WRONG_NAME" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/filename_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Naming 4 | describe Filename do 5 | subject = Filename.new 6 | 7 | it "passes if filename is correct" do 8 | expect_no_issues subject, code: "", path: "src/foo.cr" 9 | expect_no_issues subject, code: "", path: "src/foo_bar.cr" 10 | end 11 | 12 | it "fails if filename is wrong" do 13 | expect_issue subject, <<-CRYSTAL, path: "src/fooBar.cr" 14 | 15 | # ^{} error: Filename should be underscore-cased: `foo_bar.cr`, not `fooBar.cr` 16 | CRYSTAL 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/method_names_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__) 4 | it "reports method name #{expected}", file, line do 5 | rule = Ameba::Rule::Naming::MethodNames.new 6 | expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line 7 | def %{name}; end 8 | # ^{name} error: Method name should be underscore-cased: `#{expected}`, not `%{name}` 9 | CRYSTAL 10 | end 11 | end 12 | 13 | module Ameba::Rule::Naming 14 | describe MethodNames do 15 | subject = MethodNames.new 16 | 17 | it "passes if method names are underscore-cased" do 18 | expect_no_issues subject, <<-CRYSTAL 19 | class Person 20 | def first_name 21 | end 22 | 23 | def date_of_birth 24 | end 25 | 26 | def homepage_url 27 | end 28 | 29 | def valid? 30 | end 31 | 32 | def name 33 | end 34 | end 35 | CRYSTAL 36 | end 37 | 38 | it_reports_method_name "firstName", "first_name" 39 | it_reports_method_name "date_of_Birth", "date_of_birth" 40 | it_reports_method_name "homepageURL", "homepage_url" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/predicate_name_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Naming 4 | describe PredicateName do 5 | subject = PredicateName.new 6 | 7 | it "passes if predicate name is correct" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | def valid?(x) 10 | end 11 | 12 | class Image 13 | def picture?(x) 14 | end 15 | end 16 | 17 | def allow_this_picture? 18 | end 19 | CRYSTAL 20 | end 21 | 22 | it "fails if predicate name is wrong" do 23 | expect_issue subject, <<-CRYSTAL 24 | class Image 25 | def self.is_valid?(x) 26 | # ^^^^^^^^^ error: Favour method name `valid?` over `is_valid?` 27 | end 28 | end 29 | 30 | def is_valid?(x) 31 | # ^^^^^^^^^ error: Favour method name `valid?` over `is_valid?` 32 | end 33 | 34 | def is_valid(x) 35 | # ^^^^^^^^ error: Favour method name `valid?` over `is_valid` 36 | end 37 | CRYSTAL 38 | end 39 | 40 | it "ignores if alternative name isn't valid syntax" do 41 | expect_no_issues subject, <<-CRYSTAL 42 | class Image 43 | def is_404?(x) 44 | true 45 | end 46 | end 47 | CRYSTAL 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/rescued_exceptions_variable_name_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Naming 4 | describe RescuedExceptionsVariableName do 5 | subject = RescuedExceptionsVariableName.new 6 | 7 | it "passes if exception handler variable name matches #allowed_names" do 8 | subject.allowed_names.each do |name| 9 | expect_no_issues subject, <<-CRYSTAL 10 | def foo 11 | raise "foo" 12 | rescue #{name} 13 | nil 14 | end 15 | CRYSTAL 16 | end 17 | end 18 | 19 | it "fails if exception handler variable name doesn't match #allowed_names" do 20 | expect_issue subject, <<-CRYSTAL 21 | def foo 22 | raise "foo" 23 | rescue wtf 24 | # ^^^^^^^^ error: Disallowed variable name, use one of these instead: 'e', 'ex', 'exception', 'error' 25 | nil 26 | end 27 | CRYSTAL 28 | end 29 | 30 | context "properties" do 31 | context "#allowed_names" do 32 | it "returns sensible defaults" do 33 | rule = RescuedExceptionsVariableName.new 34 | rule.allowed_names.should eq %w[e ex exception error] 35 | end 36 | 37 | it "allows setting custom names" do 38 | rule = RescuedExceptionsVariableName.new 39 | rule.allowed_names = %w[foo] 40 | 41 | expect_issue rule, <<-CRYSTAL 42 | def foo 43 | raise "foo" 44 | rescue e 45 | # ^^^^^^ error: Disallowed variable name, use 'foo' instead 46 | nil 47 | end 48 | CRYSTAL 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/ameba/rule/naming/type_names_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__) 4 | it "reports type name #{expected}", file, line do 5 | rule = Ameba::Rule::Naming::TypeNames.new 6 | expect_issue rule, <<-CRYSTAL, type: type, name: name, file: file, line: line 7 | %{type} %{name}; end 8 | _{type} # ^{name} error: Type name should be camelcased: `#{expected}`, not `%{name}` 9 | CRYSTAL 10 | end 11 | end 12 | 13 | module Ameba::Rule::Naming 14 | describe TypeNames do 15 | subject = TypeNames.new 16 | 17 | it "passes if type names are camelcased" do 18 | expect_no_issues subject, <<-CRYSTAL 19 | class ParseError < Exception 20 | end 21 | 22 | module HTTP 23 | class RequestHandler 24 | end 25 | end 26 | 27 | alias NumericValue = Float32 | Float64 | Int32 | Int64 28 | 29 | lib LibYAML 30 | end 31 | 32 | struct TagDirective 33 | end 34 | 35 | enum Time::DayOfWeek 36 | end 37 | CRYSTAL 38 | end 39 | 40 | it_reports_name "class", "My_class", "MyClass" 41 | it_reports_name "module", "HTT_p", "HTTP" 42 | it_reports_name "lib", "Lib_YAML", "LibYAML" 43 | it_reports_name "struct", "Tag_directive", "TagDirective" 44 | it_reports_name "enum", "Time_enum::Day_of_week", "TimeEnum::DayOfWeek" 45 | 46 | it "reports alias name" do 47 | expect_issue subject, <<-CRYSTAL 48 | alias Numeric_value = Int32 49 | # ^^^^^^^^^^^^^ error: Type name should be camelcased: `NumericValue`, not `Numeric_value` 50 | CRYSTAL 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/ameba/rule/performance/any_instead_of_empty_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Performance 4 | describe AnyInsteadOfEmpty do 5 | subject = AnyInsteadOfEmpty.new 6 | 7 | it "passes if there is no potential performance improvements" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | [1, 2, 3].any?(&.zero?) 10 | [1, 2, 3].any?(String) 11 | [1, 2, 3].any?(1..3) 12 | [1, 2, 3].any? { |e| e > 1 } 13 | CRYSTAL 14 | end 15 | 16 | it "reports if there is any? call without a block nor argument" do 17 | expect_issue subject, <<-CRYSTAL 18 | [1, 2, 3].any? 19 | # ^^^^ error: Use `!{...}.empty?` instead of `{...}.any?` 20 | CRYSTAL 21 | end 22 | 23 | it "does not report if source is a spec" do 24 | expect_no_issues subject, <<-CRYSTAL, "source_spec.cr" 25 | [1, 2, 3].any? 26 | CRYSTAL 27 | end 28 | 29 | context "macro" do 30 | it "reports in macro scope" do 31 | expect_issue subject, <<-CRYSTAL 32 | {{ [1, 2, 3].any? }} 33 | # ^^^^ error: Use `!{...}.empty?` instead of `{...}.any?` 34 | CRYSTAL 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/ameba/rule/performance/base_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Performance 4 | describe Base do 5 | subject = PerfRule.new 6 | 7 | describe "#catch" do 8 | it "ignores spec files" do 9 | source = Source.new("", "source_spec.cr") 10 | subject.catch(source).should be_valid 11 | end 12 | 13 | it "reports perf issues for non-spec files" do 14 | source = Source.new("", "source.cr") 15 | subject.catch(source).should_not be_valid 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/ameba/rule/performance/compact_after_map_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Performance 4 | describe CompactAfterMap do 5 | subject = CompactAfterMap.new 6 | 7 | it "passes if there is no potential performance improvements" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | (1..3).compact_map(&.itself) 10 | CRYSTAL 11 | end 12 | 13 | it "passes if there is map followed by a bang call" do 14 | expect_no_issues subject, <<-CRYSTAL 15 | (1..3).map(&.itself).compact! 16 | CRYSTAL 17 | end 18 | 19 | it "reports if there is map followed by compact call" do 20 | expect_issue subject, <<-CRYSTAL 21 | (1..3).map(&.itself).compact 22 | # ^^^^^^^^^^^^^^^^^^^^^ error: Use `compact_map {...}` instead of `map {...}.compact` 23 | CRYSTAL 24 | end 25 | 26 | it "does not report if source is a spec" do 27 | expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL 28 | (1..3).map(&.itself).compact 29 | CRYSTAL 30 | end 31 | 32 | context "macro" do 33 | it "doesn't report in macro scope" do 34 | expect_no_issues subject, <<-CRYSTAL 35 | {{ [1, 2, 3].map(&.to_s).compact }} 36 | CRYSTAL 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/ameba/rule/performance/flatten_after_map_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Performance 4 | describe FlattenAfterMap do 5 | subject = FlattenAfterMap.new 6 | 7 | it "passes if there is no potential performance improvements" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | %w[Alice Bob].flat_map(&.chars) 10 | CRYSTAL 11 | end 12 | 13 | it "reports if there is map followed by flatten call" do 14 | expect_issue subject, <<-CRYSTAL 15 | %w[Alice Bob].map(&.chars).flatten 16 | # ^^^^^^^^^^^^^^^^^^^^ error: Use `flat_map {...}` instead of `map {...}.flatten` 17 | CRYSTAL 18 | end 19 | 20 | it "does not report is source is a spec" do 21 | expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL 22 | %w[Alice Bob].map(&.chars).flatten 23 | CRYSTAL 24 | end 25 | 26 | context "macro" do 27 | it "doesn't report in macro scope" do 28 | expect_no_issues subject, <<-CRYSTAL 29 | {{ %w[Alice Bob].map(&.chars).flatten }} 30 | CRYSTAL 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/ameba/rule/performance/map_instead_of_block_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Performance 4 | describe MapInsteadOfBlock do 5 | subject = MapInsteadOfBlock.new 6 | 7 | it "passes if there is no potential performance improvements" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | (1..3).sum(&.*(2)) 10 | (1..3).product(&.*(2)) 11 | CRYSTAL 12 | end 13 | 14 | it "reports if there is map followed by sum without a block" do 15 | expect_issue subject, <<-CRYSTAL 16 | (1..3).map(&.to_u64).sum 17 | # ^^^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum` 18 | CRYSTAL 19 | end 20 | 21 | it "does not report if source is a spec" do 22 | expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL 23 | (1..3).map(&.to_s).join 24 | CRYSTAL 25 | end 26 | 27 | it "reports if there is map followed by sum without a block (with argument)" do 28 | expect_issue subject, <<-CRYSTAL 29 | (1..3).map(&.to_u64).sum(0) 30 | # ^^^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum` 31 | CRYSTAL 32 | end 33 | 34 | it "reports if there is map followed by sum with a block" do 35 | expect_issue subject, <<-CRYSTAL 36 | (1..3).map(&.to_u64).sum(&.itself) 37 | # ^^^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum` 38 | CRYSTAL 39 | end 40 | 41 | context "macro" do 42 | it "doesn't report in macro scope" do 43 | expect_no_issues subject, <<-CRYSTAL 44 | {{ [1, 2, 3].map(&.to_u64).sum }} 45 | CRYSTAL 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/ameba/rule/performance/minmax_after_map_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Performance 4 | describe MinMaxAfterMap do 5 | subject = MinMaxAfterMap.new 6 | 7 | it "passes if there are no potential performance improvements" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | %w[Alice Bob].map { |name| name.size }.min(2) 10 | %w[Alice Bob].map { |name| name.size }.max(2) 11 | CRYSTAL 12 | end 13 | 14 | it "reports if there is a `min/max/minmax` call followed by `map`" do 15 | source = expect_issue subject, <<-CRYSTAL 16 | %w[Alice Bob].map { |name| name.size }.min 17 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `min_of {...}` instead of `map {...}.min`. 18 | %w[Alice Bob].map(&.size).max.zero? 19 | # ^^^^^^^^^^^^^^^ error: Use `max_of {...}` instead of `map {...}.max`. 20 | %w[Alice Bob].map(&.size).minmax? 21 | # ^^^^^^^^^^^^^^^^^^^ error: Use `minmax_of? {...}` instead of `map {...}.minmax?`. 22 | CRYSTAL 23 | 24 | expect_correction source, <<-CRYSTAL 25 | %w[Alice Bob].min_of { |name| name.size } 26 | %w[Alice Bob].max_of(&.size).zero? 27 | %w[Alice Bob].minmax_of?(&.size) 28 | CRYSTAL 29 | end 30 | 31 | it "does not report if source is a spec" do 32 | expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL 33 | %w[Alice Bob].map(&.size).min 34 | CRYSTAL 35 | end 36 | 37 | context "macro" do 38 | it "doesn't report in macro scope" do 39 | expect_no_issues subject, <<-CRYSTAL 40 | {{ %w[Alice Bob].map(&.size).min }} 41 | CRYSTAL 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/ameba/rule/style/is_a_nil_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Style 4 | describe IsANil do 5 | subject = IsANil.new 6 | 7 | it "doesn't report if there are no is_a?(Nil) calls" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | a = 1 10 | a.nil? 11 | a.is_a?(NilLiteral) 12 | a.is_a?(Custom::Nil) 13 | CRYSTAL 14 | end 15 | 16 | it "reports if there is a call to is_a?(Nil) without receiver" do 17 | source = expect_issue subject, <<-CRYSTAL 18 | a = is_a?(Nil) 19 | # ^^^ error: Use `nil?` instead of `is_a?(Nil)` 20 | CRYSTAL 21 | 22 | expect_correction source, <<-CRYSTAL 23 | a = self.nil? 24 | CRYSTAL 25 | end 26 | 27 | it "reports if there is a call to is_a?(Nil) with receiver" do 28 | source = expect_issue subject, <<-CRYSTAL 29 | a.is_a?(Nil) 30 | # ^^^ error: Use `nil?` instead of `is_a?(Nil)` 31 | CRYSTAL 32 | 33 | expect_correction source, <<-CRYSTAL 34 | a.nil? 35 | CRYSTAL 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/ameba/rule/style/multiline_curly_block_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Style 4 | describe MultilineCurlyBlock do 5 | subject = MultilineCurlyBlock.new 6 | 7 | it "doesn't report if a curly block is on a single line" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | foo { :bar } 10 | CRYSTAL 11 | end 12 | 13 | it "doesn't report for `do`...`end` blocks" do 14 | expect_no_issues subject, <<-CRYSTAL 15 | foo do 16 | :bar 17 | end 18 | CRYSTAL 19 | end 20 | 21 | it "doesn't report for `do`...`end` blocks on a single line" do 22 | expect_no_issues subject, <<-CRYSTAL 23 | foo do :bar end 24 | CRYSTAL 25 | end 26 | 27 | it "reports if there is a multi-line curly block" do 28 | expect_issue subject, <<-CRYSTAL 29 | foo { 30 | # ^ error: Use `do`...`end` instead of curly brackets for multi-line blocks 31 | :bar 32 | } 33 | CRYSTAL 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/ameba/rule/style/negated_conditions_in_unless_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Style 4 | describe NegatedConditionsInUnless do 5 | subject = NegatedConditionsInUnless.new 6 | 7 | it "passes with a unless without negated condition" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | unless a 10 | :ok 11 | end 12 | 13 | :ok unless b 14 | 15 | unless s.empty? 16 | :ok 17 | end 18 | CRYSTAL 19 | end 20 | 21 | it "fails if there is a negated condition in unless" do 22 | expect_issue subject, <<-CRYSTAL 23 | unless !a 24 | # ^^^^^^^ error: Avoid negated conditions in unless blocks 25 | :nok 26 | end 27 | CRYSTAL 28 | end 29 | 30 | it "fails if one of AND conditions is negated" do 31 | expect_issue subject, <<-CRYSTAL 32 | unless a && !b 33 | # ^^^^^^^^^^^^ error: Avoid negated conditions in unless blocks 34 | :nok 35 | end 36 | CRYSTAL 37 | end 38 | 39 | it "fails if one of OR conditions is negated" do 40 | expect_issue subject, <<-CRYSTAL 41 | unless a || !b 42 | # ^^^^^^^^^^^^ error: Avoid negated conditions in unless blocks 43 | :nok 44 | end 45 | CRYSTAL 46 | end 47 | 48 | it "fails if one of inner conditions is negated" do 49 | expect_issue subject, <<-CRYSTAL 50 | unless a && (b || !c) 51 | # ^^^^^^^^^^^^^^^^^^^ error: Avoid negated conditions in unless blocks 52 | :nok 53 | end 54 | CRYSTAL 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/ameba/rule/style/unless_else_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Style 4 | describe UnlessElse do 5 | subject = UnlessElse.new 6 | 7 | it "passes if unless hasn't else" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | unless something 10 | :ok 11 | end 12 | CRYSTAL 13 | end 14 | 15 | it "fails if unless has else" do 16 | source = expect_issue subject, <<-CRYSTAL 17 | unless something 18 | # ^^^^^^^^^^^^^^ error: Favour `if` over `unless` with `else` 19 | :one 20 | else 21 | :two 22 | end 23 | CRYSTAL 24 | 25 | expect_correction source, <<-CRYSTAL 26 | if something 27 | :two 28 | else 29 | :one 30 | end 31 | CRYSTAL 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/ameba/rule/style/while_true_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Style 4 | describe WhileTrue do 5 | subject = WhileTrue.new 6 | 7 | it "passes if there is no `while true`" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | a = 1 10 | loop do 11 | a += 1 12 | break if a > 5 13 | end 14 | CRYSTAL 15 | end 16 | 17 | it "fails if there is `while true`" do 18 | source = expect_issue subject, <<-CRYSTAL 19 | a = 1 20 | while true 21 | # ^^^^^^^^ error: While statement using `true` literal as condition 22 | a += 1 23 | break if a > 5 24 | end 25 | CRYSTAL 26 | 27 | expect_correction source, <<-CRYSTAL 28 | a = 1 29 | loop do 30 | a += 1 31 | break if a > 5 32 | end 33 | CRYSTAL 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/ameba/rule/typing/proc_literal_return_type_restriction_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../../spec_helper" 2 | 3 | module Ameba::Rule::Typing 4 | describe ProcLiteralReturnTypeRestriction do 5 | subject = ProcLiteralReturnTypeRestriction.new 6 | 7 | it "passes if a proc literal has a return type restriction" do 8 | expect_no_issues subject, <<-CRYSTAL 9 | foo = -> (bar : String) : Nil { } 10 | CRYSTAL 11 | end 12 | 13 | it "fails if a proc literal doesn't have a return type restriction" do 14 | expect_issue subject, <<-CRYSTAL 15 | foo = -> (bar : String) { } 16 | # ^^^^^^^^^^^^^^^^^^^^^ error: Proc literal should have a return type restriction 17 | CRYSTAL 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/ameba/tokenizer_spec.cr: -------------------------------------------------------------------------------- 1 | require "../spec_helper" 2 | 3 | module Ameba 4 | private def it_tokenizes(str, expected, *, file = __FILE__, line = __LINE__) 5 | it "tokenizes #{str}", file, line do 6 | %w[].tap do |token_types| 7 | Tokenizer.new(Source.new str, normalize: false) 8 | .run { |token| token_types << token.type.to_s } 9 | .should be_true 10 | end.should eq(expected), file: file, line: line 11 | end 12 | end 13 | 14 | describe Tokenizer do 15 | describe "#run" do 16 | it_tokenizes %("string"), %w[DELIMITER_START STRING DELIMITER_END EOF] 17 | it_tokenizes %(100), %w[NUMBER EOF] 18 | it_tokenizes %('a'), %w[CHAR EOF] 19 | it_tokenizes %([]), %w[[] EOF] 20 | it_tokenizes %([] of String), %w[[] SPACE IDENT SPACE CONST EOF] 21 | it_tokenizes %q("str #{3}"), %w[ 22 | DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF 23 | ] 24 | 25 | it_tokenizes %(%w[1 2]), 26 | %w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF] 27 | 28 | it_tokenizes %(%i[one two]), 29 | %w[SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF] 30 | 31 | it_tokenizes %( 32 | class A 33 | def method 34 | puts "hello" 35 | end 36 | end 37 | ), %w[ 38 | NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT 39 | NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END 40 | NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF 41 | ] 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/ameba_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Ameba do 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/config.yml: -------------------------------------------------------------------------------- 1 | Version: "1.5.0" 2 | 3 | Lint/ComparisonToBoolean: 4 | Enabled: true 5 | -------------------------------------------------------------------------------- /src/ameba.cr: -------------------------------------------------------------------------------- 1 | # Ameba's entry module. 2 | # 3 | # To run the linter with default parameters: 4 | # 5 | # ``` 6 | # Ameba.run 7 | # ``` 8 | # 9 | # To configure and run it: 10 | # 11 | # ``` 12 | # config = Ameba::Config.load 13 | # config.formatter = formatter 14 | # config.files = file_paths 15 | # 16 | # Ameba.run config 17 | # ``` 18 | module Ameba 19 | extend self 20 | 21 | VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }} 22 | 23 | macro ecr_supported?(&) 24 | {% if compare_versions(Crystal::VERSION, "1.15.0") >= 0 %} 25 | {{ yield }} 26 | {% end %} 27 | end 28 | 29 | # Initializes `Ameba::Runner` and runs it. 30 | # Can be configured via `config` parameter. 31 | # 32 | # Examples: 33 | # 34 | # ``` 35 | # Ameba.run 36 | # Ameba.run config 37 | # ``` 38 | def run(config = Config.load) 39 | Runner.new(config).run 40 | end 41 | end 42 | 43 | require "./ameba/*" 44 | require "./ameba/ast/**" 45 | require "./ameba/ext/**" 46 | require "./ameba/rule/**" 47 | require "./ameba/formatter/*" 48 | require "./ameba/presenter/*" 49 | require "./ameba/source/**" 50 | -------------------------------------------------------------------------------- /src/ameba/ast/branchable.cr: -------------------------------------------------------------------------------- 1 | require "./util" 2 | 3 | module Ameba::AST 4 | # A generic entity to represent a branchable Crystal node. 5 | # For example, `Crystal::If`, `Crystal::Unless`, `Crystal::While` 6 | # are branchables. 7 | # 8 | # ``` 9 | # while a > 100 # Branchable A 10 | # if b > 2 # Branchable B 11 | # a += 1 12 | # end 13 | # end 14 | # ``` 15 | class Branchable 16 | include Util 17 | 18 | # Parent branchable (if any) 19 | getter parent : Branchable? 20 | 21 | # Array of branches 22 | getter branches = [] of Crystal::ASTNode 23 | 24 | # The actual Crystal node 25 | getter node : Crystal::ASTNode 26 | 27 | delegate to_s, to: @node 28 | delegate location, to: @node 29 | delegate end_location, to: @node 30 | 31 | # Creates a new branchable 32 | # 33 | # ``` 34 | # Branchable.new(node, parent_branchable) 35 | # ``` 36 | def initialize(@node, @parent = nil) 37 | end 38 | 39 | # Returns `true` if this node or one of the parent branchables is a loop, 40 | # `false` otherwise. 41 | def loop? 42 | loop?(node) || !!parent.try(&.loop?) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/ameba/ast/variabling/argument.cr: -------------------------------------------------------------------------------- 1 | module Ameba::AST 2 | # Represents the argument of some node. 3 | # Holds the reference to the variable, thus to scope. 4 | # 5 | # For example, all these vars are arguments: 6 | # 7 | # ``` 8 | # def method(a, b, c = 10, &block) 9 | # 3.times do |i| 10 | # end 11 | # 12 | # ->(x : Int32) { } 13 | # end 14 | # ``` 15 | class Argument 16 | # The actual node. 17 | getter node : Crystal::Var | Crystal::Arg 18 | 19 | # Variable of this argument (may be the same node) 20 | getter variable : Variable 21 | 22 | delegate location, end_location, to_s, 23 | to: @node 24 | 25 | # Creates a new argument. 26 | # 27 | # ``` 28 | # Argument.new(node, variable) 29 | # ``` 30 | def initialize(@node, @variable) 31 | end 32 | 33 | # Returns `true` if the `name` is empty, `false` otherwise. 34 | def anonymous? 35 | name.blank? 36 | end 37 | 38 | # Returns `true` if the `name` starts with '_', `false` otherwise. 39 | def ignored? 40 | name.starts_with? '_' 41 | end 42 | 43 | # Name of the argument. 44 | def name 45 | case current_node = node 46 | when Crystal::Var, Crystal::Arg 47 | current_node.name 48 | else 49 | raise ArgumentError.new "Invalid node" 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/ast/variabling/ivariable.cr: -------------------------------------------------------------------------------- 1 | module Ameba::AST 2 | class InstanceVariable 3 | getter node : Crystal::InstanceVar 4 | 5 | delegate location, end_location, name, to_s, 6 | to: @node 7 | 8 | def initialize(@node) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/ameba/ast/variabling/reference.cr: -------------------------------------------------------------------------------- 1 | require "./variable" 2 | 3 | module Ameba::AST 4 | # Represents a reference to the variable. 5 | # It behaves like a variable is used to distinguish a 6 | # the variable from its reference. 7 | class Reference < Variable 8 | property? explicit = true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/ameba/ast/variabling/type_dec_variable.cr: -------------------------------------------------------------------------------- 1 | module Ameba::AST 2 | class TypeDecVariable 3 | getter node : Crystal::TypeDeclaration 4 | 5 | delegate location, end_location, to_s, 6 | to: @node 7 | 8 | def initialize(@node) 9 | end 10 | 11 | def name 12 | case var = @node.var 13 | when Crystal::Var, Crystal::InstanceVar, Crystal::ClassVar, Crystal::Global 14 | var.name 15 | else 16 | raise "Unsupported var node type: #{var.class}" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/ameba/ast/visitors/base_visitor.cr: -------------------------------------------------------------------------------- 1 | require "compiler/crystal/syntax/*" 2 | 3 | # A module that helps to traverse Crystal AST using `Crystal::Visitor`. 4 | module Ameba::AST 5 | # An abstract base visitor that utilizes general logic for all visitors. 6 | abstract class BaseVisitor < Crystal::Visitor 7 | # A corresponding rule that uses this visitor. 8 | @rule : Rule::Base 9 | 10 | # A source that needs to be traversed. 11 | @source : Source 12 | 13 | # Creates instance of this visitor. 14 | # 15 | # ``` 16 | # visitor = Ameba::AST::NodeVisitor.new(rule, source) 17 | # ``` 18 | def initialize(@rule, @source) 19 | @source.ast.accept self 20 | end 21 | 22 | # A main visit method that accepts `Crystal::ASTNode`. 23 | # Returns `true`, meaning all child nodes will be traversed. 24 | def visit(node : Crystal::ASTNode) 25 | true 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/ameba/ast/visitors/counting_visitor.cr: -------------------------------------------------------------------------------- 1 | module Ameba::AST 2 | # AST Visitor that counts occurrences of certain keywords 3 | class CountingVisitor < Crystal::Visitor 4 | DEFAULT_COMPLEXITY = 1 5 | 6 | # Returns the number of keywords that were found in the node 7 | getter count = DEFAULT_COMPLEXITY 8 | 9 | # Returns `true` if the node is within a macro condition 10 | getter? macro_condition = false 11 | 12 | # Creates a new counting visitor 13 | def initialize(node : Crystal::ASTNode) 14 | node.accept self 15 | end 16 | 17 | # :nodoc: 18 | def visit(node : Crystal::ASTNode) 19 | true 20 | end 21 | 22 | # Uses the same logic than rubocop. See 23 | # https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21 24 | # Except "for", because crystal doesn't have a "for" loop. 25 | {% for node in %i[if unless while until rescue or and] %} 26 | # :nodoc: 27 | def visit(node : Crystal::{{ node.id.capitalize }}) 28 | @count += 1 unless macro_condition? 29 | end 30 | {% end %} 31 | 32 | # :nodoc: 33 | def visit(node : Crystal::Case) 34 | return true if macro_condition? 35 | 36 | # Count the complexity of an exhaustive `Case` as 1 37 | # Otherwise count the number of `When`s 38 | @count += node.exhaustive? ? 1 : node.whens.size 39 | 40 | true 41 | end 42 | 43 | def visit(node : Crystal::MacroIf | Crystal::MacroFor) 44 | @macro_condition = true 45 | @count = DEFAULT_COMPLEXITY 46 | 47 | false 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/ameba/ast/visitors/flow_expression_visitor.cr: -------------------------------------------------------------------------------- 1 | require "../util" 2 | require "./base_visitor" 3 | 4 | module Ameba::AST 5 | # AST Visitor that traverses all the flow expressions. 6 | class FlowExpressionVisitor < BaseVisitor 7 | include Util 8 | 9 | @loop_stack = [] of Crystal::ASTNode 10 | 11 | # :nodoc: 12 | def visit(node) 13 | if flow_expression?(node, in_loop?) 14 | @rule.test @source, node, FlowExpression.new(node, in_loop?) 15 | end 16 | true 17 | end 18 | 19 | # :nodoc: 20 | def visit(node : Crystal::While | Crystal::Until) 21 | on_loop_started(node) 22 | end 23 | 24 | # :nodoc: 25 | def visit(node : Crystal::Call) 26 | on_loop_started(node) if loop?(node) 27 | end 28 | 29 | # :nodoc: 30 | def end_visit(node : Crystal::While | Crystal::Until) 31 | on_loop_ended(node) 32 | end 33 | 34 | # :nodoc: 35 | def end_visit(node : Crystal::Call) 36 | on_loop_ended(node) if loop?(node) 37 | end 38 | 39 | private def on_loop_started(node) 40 | @loop_stack.push(node) 41 | end 42 | 43 | private def on_loop_ended(node) 44 | @loop_stack.pop? 45 | end 46 | 47 | private def in_loop? 48 | !@loop_stack.empty? 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/ameba/ast/visitors/top_level_nodes_visitor.cr: -------------------------------------------------------------------------------- 1 | module Ameba::AST 2 | # AST Visitor that visits certain nodes at a top level, which 3 | # can characterize the source (i.e. require statements, modules etc.) 4 | class TopLevelNodesVisitor < Crystal::Visitor 5 | getter require_nodes = [] of Crystal::Require 6 | 7 | # Creates a new instance of visitor 8 | def initialize(node : Crystal::ASTNode) 9 | node.accept self 10 | end 11 | 12 | # :nodoc: 13 | def visit(node : Crystal::Require) 14 | require_nodes << node 15 | true 16 | end 17 | 18 | # If a top level node is `Crystal::Expressions`, 19 | # then always traverse the children. 20 | def visit(node : Crystal::Expressions) 21 | true 22 | end 23 | 24 | # A general visit method for rest of the nodes. 25 | # Returns `false`, meaning all child nodes will not be traversed. 26 | def visit(node : Crystal::ASTNode) 27 | false 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /src/ameba/ext/location.cr: -------------------------------------------------------------------------------- 1 | # Extensions to Crystal::Location 2 | module Ameba::Ext::Location 3 | # Returns the same location as this location but with the line and/or column number(s) changed 4 | # to the given value(s). 5 | def with(line_number = @line_number, column_number = @column_number) : self 6 | self.class.new(@filename, line_number, column_number) 7 | end 8 | 9 | # Returns the same location as this location but with the line and/or column number(s) adjusted 10 | # by the given amount(s). 11 | def adjust(line_number = 0, column_number = 0) : self 12 | self.class.new(@filename, @line_number + line_number, @column_number + column_number) 13 | end 14 | 15 | # Seeks to a given *offset* relative to `self`. 16 | def seek(offset : self) : self 17 | if offset.filename.as?(String).presence && @filename != offset.filename 18 | raise ArgumentError.new <<-MSG 19 | Mismatching filenames: 20 | #{@filename} 21 | #{offset.filename} 22 | MSG 23 | end 24 | 25 | if offset.line_number == 1 26 | self.class.new(@filename, @line_number, @column_number + offset.column_number - 1) 27 | else 28 | self.class.new(@filename, @line_number + offset.line_number - 1, offset.column_number) 29 | end 30 | end 31 | end 32 | 33 | class Crystal::Location 34 | include Ameba::Ext::Location 35 | end 36 | -------------------------------------------------------------------------------- /src/ameba/formatter/base_formatter.cr: -------------------------------------------------------------------------------- 1 | require "./util" 2 | 3 | # A module that utilizes Ameba's formatters. 4 | module Ameba::Formatter 5 | # A base formatter for all formatters. It uses `output` IO 6 | # to report results and also implements stub methods for 7 | # callbacks in `Ameba::Runner#run` method. 8 | class BaseFormatter 9 | # TODO: allow other IOs 10 | getter output : IO::FileDescriptor | IO::Memory 11 | getter config = {} of Symbol => String | Bool 12 | 13 | def initialize(@output = STDOUT) 14 | end 15 | 16 | # Callback that indicates when inspecting is started. 17 | # A list of sources to inspect is passed as an argument. 18 | def started(sources) : Nil; end 19 | 20 | # Callback that indicates when source inspection is started. 21 | # A corresponding source is passed as an argument. 22 | # 23 | # WARNING: This method needs to be MT safe 24 | def source_started(source : Source) : Nil; end 25 | 26 | # Callback that indicates when source inspection is finished. 27 | # A corresponding source is passed as an argument. 28 | # 29 | # WARNING: This method needs to be MT safe 30 | def source_finished(source : Source) : Nil; end 31 | 32 | # Callback that indicates when inspection is finished. 33 | # A list of inspected sources is passed as an argument. 34 | def finished(sources) : Nil; end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/ameba/formatter/disabled_formatter.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Formatter 2 | # A formatter that shows all disabled lines by inline directives. 3 | class DisabledFormatter < BaseFormatter 4 | def finished(sources) : Nil 5 | output << "Disabled rules using inline directives:\n\n" 6 | 7 | sources.each do |source| 8 | source.issues.each do |issue| 9 | next unless issue.disabled? 10 | next unless loc = issue.location 11 | 12 | output << "#{source.path}:#{loc.line_number}".colorize(:cyan) 13 | output << " #{issue.rule.name}\n" 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/ameba/formatter/flycheck_formatter.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Formatter 2 | class FlycheckFormatter < BaseFormatter 3 | @mutex = Mutex.new 4 | 5 | def source_finished(source : Source) : Nil 6 | source.issues.each do |issue| 7 | next if issue.disabled? 8 | next if issue.correctable? && config[:autocorrect]? 9 | 10 | next unless loc = issue.location 11 | 12 | @mutex.synchronize do 13 | output.printf "%s:%d:%d: %s: [%s] %s\n", 14 | source.path, loc.line_number, loc.column_number, issue.rule.severity.symbol, 15 | issue.rule.name, issue.message.gsub('\n', " ") 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/ameba/glob_utils.cr: -------------------------------------------------------------------------------- 1 | module Ameba 2 | # Helper module that is utilizes helpers for working with globs. 3 | module GlobUtils 4 | extend self 5 | 6 | # Returns all files that match specified globs. 7 | # Globs can have wildcards or be rejected: 8 | # 9 | # ``` 10 | # find_files_by_globs(["**/*.cr", "!lib"]) 11 | # ``` 12 | def find_files_by_globs(globs) 13 | rejected = rejected_globs(globs) 14 | selected = globs - rejected 15 | 16 | expand(selected) - expand(rejected.map!(&.[1..-1])) 17 | end 18 | 19 | # Expands globs. Globs can point to files or even directories. 20 | # 21 | # ``` 22 | # expand(["spec/*.cr", "src"]) # => all files in src folder + first level specs 23 | # ``` 24 | def expand(globs) 25 | globs 26 | .flat_map do |glob| 27 | if File.directory?(glob) 28 | ext = ".cr" 29 | 30 | Ameba.ecr_supported? do 31 | ext = ".{cr,ecr}" 32 | end 33 | 34 | glob += "/**/*#{ext}" 35 | end 36 | 37 | Dir[glob] 38 | end 39 | .uniq! 40 | .select! { |path| File.file?(path) } 41 | end 42 | 43 | private def rejected_globs(globs) 44 | globs.select do |glob| 45 | glob.starts_with?('!') && !File.exists?(glob) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/ameba/issue.cr: -------------------------------------------------------------------------------- 1 | module Ameba 2 | # Represents an issue reported by Ameba. 3 | struct Issue 4 | enum Status 5 | Enabled 6 | Disabled 7 | end 8 | 9 | # The source code that triggered this issue. 10 | getter code : String 11 | 12 | # A rule that triggers this issue. 13 | getter rule : Rule::Base 14 | 15 | # Location of the issue. 16 | getter location : Crystal::Location? 17 | 18 | # End location of the issue. 19 | getter end_location : Crystal::Location? 20 | 21 | # Issue message. 22 | getter message : String 23 | 24 | # Issue status. 25 | getter status : Status 26 | 27 | delegate :enabled?, :disabled?, 28 | to: status 29 | 30 | def initialize(@code, @rule, @location, @end_location, @message, status : Status? = nil, @block : (Source::Corrector ->)? = nil) 31 | @status = status || Status::Enabled 32 | end 33 | 34 | def syntax? 35 | rule.is_a?(Rule::Lint::Syntax) 36 | end 37 | 38 | def correctable? 39 | !@block.nil? 40 | end 41 | 42 | def correct(corrector) 43 | @block.try &.call(corrector) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/ameba/presenter/base_presenter.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Presenter 2 | private ENABLED_MARK = "✓".colorize(:green) 3 | private DISABLED_MARK = "x".colorize(:red) 4 | 5 | class BasePresenter 6 | # TODO: allow other IOs 7 | getter output : IO::FileDescriptor | IO::Memory 8 | 9 | def initialize(@output = STDOUT) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/ameba/presenter/rule_collection_presenter.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Presenter 2 | class RuleCollectionPresenter < BasePresenter 3 | def run(rules) : Nil 4 | rules = rules.to_h do |rule| 5 | name = rule.name.split('/') 6 | name = "%s/%s" % { 7 | name[0...-1].join('/').colorize(:light_gray), 8 | name.last.colorize(:white), 9 | } 10 | {name, rule} 11 | end 12 | longest_name = rules.max_of(&.first.size) 13 | 14 | rules.group_by(&.last.group).each do |group, group_rules| 15 | output.puts "— %s" % group.colorize(:light_blue).underline 16 | output.puts 17 | group_rules.each do |name, rule| 18 | output.puts " %s [%s] %s %s" % { 19 | rule.enabled? ? ENABLED_MARK : DISABLED_MARK, 20 | rule.severity.symbol.to_s.colorize(:green), 21 | name.ljust(longest_name), 22 | rule.description.colorize(:dark_gray), 23 | } 24 | end 25 | output.puts 26 | end 27 | 28 | output.puts "Total rules: %s / %s enabled" % { 29 | rules.size.to_s.colorize(:light_blue), 30 | rules.count(&.last.enabled?).to_s.colorize(:light_blue), 31 | } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /src/ameba/presenter/rule_presenter.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Presenter 2 | class RulePresenter < BasePresenter 3 | def run(rule) : Nil 4 | output_title "Rule info" 5 | 6 | info = <<-INFO 7 | Name: %s 8 | Severity: %s 9 | Enabled: %s 10 | Since version: %s 11 | INFO 12 | 13 | output_paragraph info % { 14 | rule.name.colorize(:magenta), 15 | rule.severity.to_s.colorize(rule.severity.color), 16 | rule.enabled? ? ENABLED_MARK : DISABLED_MARK, 17 | (rule.since_version.try(&.to_s) || "N/A").colorize(:white), 18 | } 19 | 20 | if rule_description = colorize_code_fences(rule.description) 21 | output_title "Description" 22 | output_paragraph rule_description 23 | end 24 | 25 | if rule_doc = colorize_code_fences(rule.class.parsed_doc) 26 | output_title "Detailed description" 27 | output_paragraph rule_doc 28 | end 29 | end 30 | 31 | private def output_title(title) 32 | output.print "### %s\n\n" % title.upcase.colorize(:yellow) 33 | end 34 | 35 | private def output_paragraph(paragraph : String) 36 | output_paragraph(paragraph.lines) 37 | end 38 | 39 | private def output_paragraph(paragraph : Array) 40 | paragraph.each do |line| 41 | output.puts " #{line}" 42 | end 43 | output.puts 44 | end 45 | 46 | private def colorize_code_fences(string) 47 | return unless string 48 | string 49 | .gsub(/```(.+?)```/m, &.colorize(:dark_gray)) 50 | .gsub(/`(?!`)(.+?)`/, &.colorize(:dark_gray)) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/presenter/rule_versions_presenter.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Presenter 2 | class RuleVersionsPresenter < BasePresenter 3 | def run(rules, verbose = true) 4 | missing_version = SemanticVersion.new(0, 0, 0) 5 | versions = rules 6 | .sort_by { |rule| rule.since_version || missing_version } 7 | .group_by(&.since_version) 8 | 9 | first = true 10 | 11 | versions.each do |version, version_rules| 12 | if verbose 13 | output.puts unless first 14 | if version 15 | output.puts "- %s" % version.to_s.colorize(:green) 16 | else 17 | output.puts "- %s" % "N/A".colorize(:dark_gray) 18 | end 19 | version_rules.map(&.name).sort!.each do |name| 20 | output.puts " - %s" % name.colorize(:dark_gray) 21 | end 22 | else 23 | if version 24 | output.puts "- %s" % version.to_s.colorize(:green) 25 | end 26 | end 27 | 28 | first = false 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/ameba/rule/layout/line_length.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Layout 2 | # A rule that disallows lines longer than `max_length` number of symbols. 3 | # 4 | # YAML configuration example: 5 | # 6 | # ``` 7 | # Layout/LineLength: 8 | # Enabled: true 9 | # MaxLength: 100 10 | # ``` 11 | class LineLength < Base 12 | properties do 13 | since_version "0.1.0" 14 | enabled false 15 | description "Disallows lines longer than `MaxLength` number of symbols" 16 | max_length 140 17 | end 18 | 19 | MSG = "Line too long" 20 | 21 | def test(source) 22 | source.lines.each_with_index do |line, index| 23 | issue_for({index + 1, max_length + 1}, MSG) if line.size > max_length 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/ameba/rule/layout/trailing_blank_lines.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Layout 2 | # A rule that disallows trailing blank lines at the end of the source file. 3 | # 4 | # YAML configuration example: 5 | # 6 | # ``` 7 | # Layout/TrailingBlankLines: 8 | # Enabled: true 9 | # ``` 10 | class TrailingBlankLines < Base 11 | properties do 12 | since_version "0.1.0" 13 | description "Disallows trailing blank lines" 14 | end 15 | 16 | MSG = "Excessive trailing newline detected" 17 | MSG_FINAL_NEWLINE = "Trailing newline missing" 18 | 19 | def test(source) 20 | source_lines = source.lines 21 | return if source_lines.empty? 22 | 23 | last_source_line = source_lines.last 24 | source_lines_size = source_lines.size 25 | return if source_lines_size == 1 && last_source_line.empty? 26 | 27 | last_line_empty = last_source_line.empty? 28 | return if source_lines_size.zero? || 29 | (source_lines.last(2).join.presence && last_line_empty) 30 | 31 | if last_line_empty 32 | issue_for({source_lines_size, 1}, MSG) 33 | else 34 | issue_for({source_lines_size, 1}, MSG_FINAL_NEWLINE) do |corrector| 35 | corrector.insert_before({source_lines_size + 1, 1}, '\n') 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /src/ameba/rule/layout/trailing_whitespace.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Layout 2 | # A rule that disallows trailing whitespace. 3 | # 4 | # YAML configuration example: 5 | # 6 | # ``` 7 | # Layout/TrailingWhitespace: 8 | # Enabled: true 9 | # ``` 10 | class TrailingWhitespace < Base 11 | properties do 12 | since_version "0.1.0" 13 | description "Disallows trailing whitespace" 14 | end 15 | 16 | MSG = "Trailing whitespace detected" 17 | 18 | def test(source) 19 | source.lines.each_with_index do |line, index| 20 | next unless ws_index = line =~ /\s+$/ 21 | 22 | location = {index + 1, ws_index + 1} 23 | end_location = {index + 1, line.size} 24 | 25 | issue_for location, end_location, MSG do |corrector| 26 | corrector.remove(location, end_location) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/ambiguous_assignment.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # This rule checks for mistyped shorthand assignments. 3 | # 4 | # This is considered invalid: 5 | # 6 | # ``` 7 | # x = -y 8 | # x = +y 9 | # x = !y 10 | # ``` 11 | # 12 | # And this is valid: 13 | # 14 | # ``` 15 | # x -= y # or x = -y 16 | # x += y # or x = +y 17 | # x != y # or x = !y 18 | # ``` 19 | # 20 | # YAML configuration example: 21 | # 22 | # ``` 23 | # Lint/AmbiguousAssignment: 24 | # Enabled: true 25 | # ``` 26 | class AmbiguousAssignment < Base 27 | include AST::Util 28 | 29 | properties do 30 | since_version "1.0.0" 31 | description "Disallows ambiguous `=-/=+/=!`" 32 | end 33 | 34 | MSG = "Suspicious assignment detected. Did you mean `%s`?" 35 | 36 | MISTAKES = { 37 | "=-": "-=", 38 | "=+": "+=", 39 | "=!": "!=", 40 | } 41 | 42 | def test(source, node : Crystal::Assign) 43 | return unless op_end_location = node.value.location 44 | 45 | op_location = Crystal::Location.new( 46 | op_end_location.filename, 47 | op_end_location.line_number, 48 | op_end_location.column_number - 1 49 | ) 50 | op_text = source_between(op_location, op_end_location, source.lines) 51 | 52 | return unless op_text 53 | return unless suggestion = MISTAKES[op_text]? 54 | 55 | issue_for op_location, op_end_location, MSG % suggestion 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/comparison_to_boolean.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows comparison to booleans. 3 | # 4 | # For example, these are considered invalid: 5 | # 6 | # ``` 7 | # foo == true 8 | # bar != false 9 | # false === baz 10 | # ``` 11 | # 12 | # This is because these expressions evaluate to `true` or `false`, so you 13 | # could get the same result by using either the variable directly, 14 | # or negating the variable. 15 | # 16 | # YAML configuration example: 17 | # 18 | # ``` 19 | # Lint/ComparisonToBoolean: 20 | # Enabled: true 21 | # ``` 22 | class ComparisonToBoolean < Base 23 | include AST::Util 24 | 25 | properties do 26 | since_version "0.1.0" 27 | enabled false 28 | description "Disallows comparison to booleans" 29 | end 30 | 31 | MSG = "Comparison to a boolean is pointless" 32 | OP_NAMES = %w[== != ===] 33 | 34 | def test(source, node : Crystal::Call) 35 | return unless node.name.in?(OP_NAMES) 36 | return unless node.args.size == 1 37 | 38 | arg, obj = node.args.first, node.obj 39 | case 40 | when arg.is_a?(Crystal::BoolLiteral) 41 | bool, exp = arg, obj 42 | when obj.is_a?(Crystal::BoolLiteral) 43 | bool, exp = obj, arg 44 | end 45 | 46 | return unless bool && exp 47 | return unless exp_code = node_source(exp, source.lines) 48 | 49 | not = 50 | case node.name 51 | when "==", "===" then !bool.value # foo == false 52 | when "!=" then bool.value # foo != true 53 | end 54 | 55 | exp_code = "!#{exp_code}" if not 56 | 57 | issue_for node, MSG do |corrector| 58 | corrector.replace(node, exp_code) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/debug_calls.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows calls to debug-related methods. 3 | # 4 | # This is because we don't want debug calls accidentally being 5 | # committed into our codebase. 6 | # 7 | # YAML configuration example: 8 | # 9 | # ``` 10 | # Lint/DebugCalls: 11 | # Enabled: true 12 | # MethodNames: 13 | # - p 14 | # - p! 15 | # - pp 16 | # - pp! 17 | # ``` 18 | class DebugCalls < Base 19 | properties do 20 | since_version "1.0.0" 21 | description "Disallows debug-related calls" 22 | method_names %w[p p! pp pp!] 23 | end 24 | 25 | MSG = "Possibly forgotten debug-related `%s` call detected" 26 | 27 | def test(source, node : Crystal::Call) 28 | return unless node.name.in?(method_names) && node.obj.nil? 29 | 30 | issue_for node, MSG % node.name 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/debugger_statement.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows calls to debugger. 3 | # 4 | # This is because we don't want debugger breakpoints accidentally being 5 | # committed into our codebase. 6 | # 7 | # YAML configuration example: 8 | # 9 | # ``` 10 | # Lint/DebuggerStatement: 11 | # Enabled: true 12 | # ``` 13 | class DebuggerStatement < Base 14 | properties do 15 | since_version "0.1.0" 16 | description "Disallows calls to debugger" 17 | end 18 | 19 | MSG = "Possible forgotten debugger statement detected" 20 | 21 | def test(source, node : Crystal::Call) 22 | return unless node.name == "debugger" && 23 | node.args.empty? && 24 | node.obj.nil? 25 | 26 | issue_for node, MSG 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/duplicate_when_condition.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # Reports repeated conditions used in case `when` expressions. 3 | # 4 | # This is considered invalid: 5 | # 6 | # ``` 7 | # case x 8 | # when .nil? 9 | # do_something 10 | # when .nil? 11 | # do_something_else 12 | # end 13 | # ``` 14 | # 15 | # And this is valid: 16 | # 17 | # ``` 18 | # case x 19 | # when .nil? 20 | # do_something 21 | # when Symbol 22 | # do_something_else 23 | # end 24 | # ``` 25 | # 26 | # YAML configuration example: 27 | # 28 | # ``` 29 | # Lint/DuplicateWhenCondition: 30 | # Enabled: true 31 | # ``` 32 | class DuplicateWhenCondition < Base 33 | properties do 34 | since_version "1.7.0" 35 | description "Reports repeated conditions used in case `when` expressions" 36 | end 37 | 38 | MSG = "Duplicate `when` condition detected" 39 | 40 | def test(source, node : Crystal::Case | Crystal::Select) 41 | node.whens.each_with_object(Set(String).new) do |when_node, processed_conditions| 42 | when_node.conds.each do |cond| 43 | cond_s = cond.to_s 44 | if processed_conditions.includes?(cond_s) 45 | issue_for cond, MSG 46 | else 47 | processed_conditions << cond_s 48 | end 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/duplicated_require.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that reports duplicated require statements. 3 | # 4 | # ``` 5 | # require "./thing" 6 | # require "./stuff" 7 | # require "./thing" # duplicated require 8 | # ``` 9 | # 10 | # YAML configuration example: 11 | # 12 | # ``` 13 | # Lint/DuplicatedRequire: 14 | # Enabled: true 15 | # ``` 16 | class DuplicatedRequire < Base 17 | properties do 18 | since_version "0.14.0" 19 | description "Reports duplicated require statements" 20 | end 21 | 22 | MSG = "Duplicated require of `%s`" 23 | 24 | def test(source) 25 | nodes = AST::TopLevelNodesVisitor.new(source.ast).require_nodes 26 | nodes.each_with_object([] of String) do |node, processed_require_strings| 27 | issue_for(node, MSG % node.string) if node.string.in?(processed_require_strings) 28 | processed_require_strings << node.string 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/else_nil.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows `else` blocks with `nil` as their body, as they 3 | # have no effect and can be safely removed. 4 | # 5 | # This is considered invalid: 6 | # 7 | # ``` 8 | # if foo 9 | # do_foo 10 | # else 11 | # nil 12 | # end 13 | # ``` 14 | # 15 | # And this is valid: 16 | # 17 | # ``` 18 | # if foo 19 | # do_foo 20 | # end 21 | # ``` 22 | # 23 | # YAML configuration example: 24 | # 25 | # ``` 26 | # Lint/ElseNil: 27 | # Enabled: true 28 | # ``` 29 | class ElseNil < Base 30 | properties do 31 | since_version "1.7.0" 32 | description "Disallows `else` blocks with `nil` as their body" 33 | end 34 | 35 | MSG = "Avoid `else` blocks with `nil` as their body" 36 | 37 | def test(source, node : Crystal::Case) 38 | check_issue(source, node) unless node.exhaustive? 39 | end 40 | 41 | def test(source, node : Crystal::If) 42 | check_issue(source, node) unless node.ternary? 43 | end 44 | 45 | def test(source, node : Crystal::Unless) 46 | check_issue(source, node) 47 | end 48 | 49 | private def check_issue(source, node) 50 | return unless node_else = node.else 51 | return unless node_else.is_a?(Crystal::NilLiteral) 52 | 53 | if node.responds_to?(:else_location) && 54 | (else_location = node.else_location) && 55 | (end_location = node.end_location) 56 | issue_for node_else, MSG do |corrector| 57 | corrector.remove( 58 | else_location, 59 | end_location.adjust(column_number: -{{ "end".size }}) 60 | ) 61 | end 62 | else 63 | issue_for node_else, MSG 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/empty_ensure.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows empty ensure statement. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # def some_method 8 | # do_some_stuff 9 | # ensure 10 | # end 11 | # 12 | # begin 13 | # do_some_stuff 14 | # ensure 15 | # end 16 | # ``` 17 | # 18 | # And it should be written as this: 19 | # 20 | # ``` 21 | # def some_method 22 | # do_some_stuff 23 | # ensure 24 | # do_something_else 25 | # end 26 | # 27 | # begin 28 | # do_some_stuff 29 | # ensure 30 | # do_something_else 31 | # end 32 | # ``` 33 | # 34 | # YAML configuration example: 35 | # 36 | # ``` 37 | # Lint/EmptyEnsure 38 | # Enabled: true 39 | # ``` 40 | class EmptyEnsure < Base 41 | properties do 42 | since_version "0.3.0" 43 | description "Disallows empty ensure statement" 44 | end 45 | 46 | MSG = "Empty `ensure` block detected" 47 | 48 | def test(source, node : Crystal::ExceptionHandler) 49 | node_ensure = node.ensure 50 | return if node_ensure.nil? || !node_ensure.nop? 51 | 52 | issue_for node.ensure_location, node.end_location, MSG 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/empty_expression.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows empty expressions. 3 | # 4 | # This is considered invalid: 5 | # 6 | # ``` 7 | # foo = () 8 | # 9 | # if () 10 | # bar 11 | # end 12 | # ``` 13 | # 14 | # And this is valid: 15 | # 16 | # ``` 17 | # foo = (some_expression) 18 | # 19 | # if (some_expression) 20 | # bar 21 | # end 22 | # ``` 23 | # 24 | # YAML configuration example: 25 | # 26 | # ``` 27 | # Lint/EmptyExpression: 28 | # Enabled: true 29 | # ``` 30 | class EmptyExpression < Base 31 | properties do 32 | since_version "0.2.0" 33 | description "Disallows empty expressions" 34 | end 35 | 36 | MSG = "Avoid empty expressions" 37 | 38 | def test(source, node : Crystal::Expressions) 39 | return unless node.expressions.size == 1 && node.expressions.first.nop? 40 | issue_for node, MSG 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/empty_loop.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows empty loops. 3 | # 4 | # This is considered invalid: 5 | # 6 | # ``` 7 | # while false 8 | # end 9 | # 10 | # until 10 11 | # end 12 | # 13 | # loop do 14 | # # nothing here 15 | # end 16 | # ``` 17 | # 18 | # And this is valid: 19 | # 20 | # ``` 21 | # a = 1 22 | # while a < 10 23 | # a += 1 24 | # end 25 | # 26 | # until socket_opened? 27 | # end 28 | # 29 | # loop do 30 | # do_something_here 31 | # end 32 | # ``` 33 | # 34 | # YAML configuration example: 35 | # 36 | # ``` 37 | # Lint/EmptyLoop: 38 | # Enabled: true 39 | # ``` 40 | class EmptyLoop < Base 41 | include AST::Util 42 | 43 | properties do 44 | since_version "0.12.0" 45 | description "Disallows empty loops" 46 | end 47 | 48 | MSG = "Empty loop detected" 49 | 50 | def test(source, node : Crystal::Call) 51 | check_node(source, node, node.block) if loop?(node) 52 | end 53 | 54 | def test(source, node : Crystal::While | Crystal::Until) 55 | check_node(source, node, node.body) if literal?(node.cond) 56 | end 57 | 58 | private def check_node(source, node, loop_body) 59 | body = loop_body.is_a?(Crystal::Block) ? loop_body.body : loop_body 60 | return unless body.nil? || body.nop? 61 | 62 | issue_for node, MSG 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/formatting.cr: -------------------------------------------------------------------------------- 1 | require "compiler/crystal/formatter" 2 | 3 | module Ameba::Rule::Lint 4 | # A rule that verifies syntax formatting according to the 5 | # Crystal's built-in formatter. 6 | # 7 | # For example, this syntax is invalid: 8 | # 9 | # def foo(a,b,c=0) 10 | # #foobar 11 | # a+b+c 12 | # end 13 | # 14 | # And should be properly written: 15 | # 16 | # def foo(a, b, c = 0) 17 | # # foobar 18 | # a + b + c 19 | # end 20 | # 21 | # YAML configuration example: 22 | # 23 | # ``` 24 | # Lint/Formatting: 25 | # Enabled: true 26 | # FailOnError: false 27 | # ``` 28 | class Formatting < Base 29 | properties do 30 | since_version "1.4.0" 31 | description "Reports not formatted sources" 32 | fail_on_error false 33 | end 34 | 35 | MSG = "Use built-in formatter to format this source" 36 | MSG_ERROR = "Error while formatting: %s" 37 | 38 | private LOCATION = {1, 1} 39 | 40 | def test(source) 41 | source_code = source.code 42 | 43 | source_lines = source_code.lines 44 | return if source_lines.empty? 45 | 46 | result = Crystal.format(source_code, source.path) 47 | return if result == source_code 48 | 49 | end_location = { 50 | source_lines.size, 51 | source_lines.last.size + 1, 52 | } 53 | 54 | issue_for LOCATION, MSG do |corrector| 55 | corrector.replace(LOCATION, end_location, result) 56 | end 57 | rescue ex : Crystal::SyntaxException 58 | if fail_on_error? 59 | issue_for({ex.line_number, ex.column_number}, MSG_ERROR % ex.message) 60 | end 61 | rescue ex 62 | if fail_on_error? 63 | issue_for(LOCATION, MSG_ERROR % ex.message) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/hash_duplicated_key.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows duplicated keys in hash literals. 3 | # 4 | # This is considered invalid: 5 | # 6 | # ``` 7 | # h = {"foo" => 1, "bar" => 2, "foo" => 3} 8 | # ``` 9 | # 10 | # And it has to written as this instead: 11 | # 12 | # ``` 13 | # h = {"foo" => 1, "bar" => 2} 14 | # ``` 15 | # 16 | # YAML configuration example: 17 | # 18 | # ``` 19 | # Lint/HashDuplicatedKey: 20 | # Enabled: true 21 | # ``` 22 | class HashDuplicatedKey < Base 23 | properties do 24 | since_version "0.3.0" 25 | description "Disallows duplicated keys in hash literals" 26 | end 27 | 28 | MSG = "Duplicated keys in hash literal: %s" 29 | 30 | def test(source, node : Crystal::HashLiteral) 31 | return if (keys = duplicated_keys(node.entries)).empty? 32 | 33 | issue_for node, MSG % keys.map { |key| "`#{key}`" }.join(", ") 34 | end 35 | 36 | private def duplicated_keys(entries) 37 | entries.map(&.key) 38 | .group_by(&.itself) 39 | .select! { |_, v| v.size > 1 } 40 | .map { |k, _| k } 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/literal_assignments_in_expressions.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows assignments with literal values 3 | # in control expressions. 4 | # 5 | # For example, this is considered invalid: 6 | # 7 | # ``` 8 | # if foo = 42 9 | # do_something 10 | # end 11 | # ``` 12 | # 13 | # And most likely should be replaced by the following: 14 | # 15 | # ``` 16 | # if foo == 42 17 | # do_something 18 | # end 19 | # ``` 20 | # 21 | # YAML configuration example: 22 | # 23 | # ``` 24 | # Lint/LiteralAssignmentsInExpressions: 25 | # Enabled: true 26 | # ``` 27 | class LiteralAssignmentsInExpressions < Base 28 | include AST::Util 29 | 30 | properties do 31 | since_version "1.4.0" 32 | description "Disallows assignments with literal values in control expressions" 33 | end 34 | 35 | MSG = "Detected assignment with a literal value in control expression" 36 | 37 | def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until) 38 | return unless (cond = node.cond).is_a?(Crystal::Assign) 39 | return unless literal?(cond.value) 40 | 41 | issue_for cond, MSG 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/literal_in_condition.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows useless conditional statements that contain a literal 3 | # in place of a variable or predicate function. 4 | # 5 | # This is because a conditional construct with a literal predicate will 6 | # always result in the same behavior at run time, meaning it can be 7 | # replaced with either the body of the construct, or deleted entirely. 8 | # 9 | # This is considered invalid: 10 | # 11 | # ``` 12 | # if "something" 13 | # :ok 14 | # end 15 | # ``` 16 | # 17 | # YAML configuration example: 18 | # 19 | # ``` 20 | # Lint/LiteralInCondition: 21 | # Enabled: true 22 | # ``` 23 | class LiteralInCondition < Base 24 | include AST::Util 25 | 26 | properties do 27 | since_version "0.1.0" 28 | description "Disallows useless conditional statements that contain \ 29 | a literal in place of a variable or predicate function" 30 | end 31 | 32 | MSG = "Literal value found in conditional" 33 | 34 | def test(source, node : Crystal::If | Crystal::Unless | Crystal::Until) 35 | issue_for node.cond, MSG if literal?(node.cond) 36 | end 37 | 38 | def test(source, node : Crystal::Case) 39 | return unless cond = node.cond 40 | return unless static_literal?(cond) 41 | 42 | issue_for cond, MSG 43 | end 44 | 45 | def test(source, node : Crystal::While) 46 | return unless literal?(cond = node.cond) 47 | # allow `while true` 48 | return if cond.is_a?(Crystal::BoolLiteral) && cond.value 49 | 50 | issue_for cond, MSG 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/literal_in_interpolation.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows useless string interpolations 3 | # that contain a literal value instead of a variable or function. 4 | # 5 | # For example: 6 | # 7 | # ``` 8 | # "Hello, #{:Ary}" 9 | # "There are #{4} cats" 10 | # ``` 11 | # 12 | # YAML configuration example: 13 | # 14 | # ``` 15 | # Lint/LiteralInInterpolation 16 | # Enabled: true 17 | # ``` 18 | class LiteralInInterpolation < Base 19 | include AST::Util 20 | 21 | properties do 22 | since_version "0.1.0" 23 | description "Disallows useless string interpolations" 24 | end 25 | 26 | MSG = "Literal value found in interpolation" 27 | 28 | MAGIC_CONSTANTS = %w[__LINE__ __FILE__ __DIR__] 29 | 30 | def test(source, node : Crystal::StringInterpolation) 31 | each_literal_node(source, node) { |exp| issue_for exp, MSG } 32 | end 33 | 34 | private def each_literal_node(source, node, &) 35 | source_lines = source.lines 36 | 37 | node.expressions.each do |exp| 38 | next if exp.is_a?(Crystal::StringLiteral) 39 | next unless literal?(exp) 40 | next unless code = node_source(exp, source_lines) 41 | next if code.in?(MAGIC_CONSTANTS) 42 | 43 | yield exp 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/literals_comparison.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # This rule is used to identify comparisons between two literals. 3 | # 4 | # They usually have the same result - except for non-primitive 5 | # types like containers, range or regex. 6 | # 7 | # For example, this will be always false: 8 | # 9 | # ``` 10 | # "foo" == 42 11 | # ``` 12 | # 13 | # YAML configuration example: 14 | # 15 | # ``` 16 | # Lint/LiteralsComparison: 17 | # Enabled: true 18 | # ``` 19 | class LiteralsComparison < Base 20 | include AST::Util 21 | 22 | properties do 23 | since_version "1.3.0" 24 | description "Identifies comparisons between literals" 25 | end 26 | 27 | OP_NAMES = %w[=== == !=] 28 | 29 | MSG = "Comparison always evaluates to %s" 30 | MSG_LIKELY = "Comparison most likely evaluates to %s" 31 | 32 | def test(source, node : Crystal::Call) 33 | return unless node.name.in?(OP_NAMES) 34 | return unless (obj = node.obj) && (arg = node.args.first?) 35 | 36 | obj_is_literal, obj_is_static = literal_kind?(obj) 37 | arg_is_literal, arg_is_static = literal_kind?(arg) 38 | 39 | return unless obj_is_literal && arg_is_literal 40 | return unless obj.to_s == arg.to_s 41 | 42 | is_dynamic = !obj_is_static || !arg_is_static 43 | 44 | what = 45 | case node.name 46 | when "===" then "the same" 47 | when "==" then "true" 48 | when "!=" then "false" 49 | end 50 | 51 | issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/missing_block_argument.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows yielding method definitions without block argument. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # def foo 7 | # yield 42 8 | # end 9 | # 10 | # And has to be written as the following: 11 | # 12 | # def foo(&) 13 | # yield 42 14 | # end 15 | # 16 | # YAML configuration example: 17 | # 18 | # ``` 19 | # Lint/MissingBlockArgument: 20 | # Enabled: true 21 | # ``` 22 | class MissingBlockArgument < Base 23 | properties do 24 | since_version "1.4.0" 25 | description "Disallows yielding method definitions without block argument" 26 | end 27 | 28 | MSG = "Missing anonymous block argument. Use `&` as an argument " \ 29 | "name to indicate yielding method." 30 | 31 | def test(source) 32 | AST::ScopeVisitor.new self, source 33 | end 34 | 35 | def test(source, node : Crystal::Def, scope : AST::Scope) 36 | return if !scope.yields? || node.block_arg 37 | 38 | issue_for node, MSG, prefer_name_location: true 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/not_nil.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # This rule is used to identify usages of `not_nil!` calls. 3 | # 4 | # For example, this is considered a code smell: 5 | # 6 | # ``` 7 | # names = %w[Alice Bob] 8 | # alice = names.find { |name| name == "Alice" }.not_nil! 9 | # ``` 10 | # 11 | # And can be written as this: 12 | # 13 | # ``` 14 | # names = %w[Alice Bob] 15 | # alice = names.find { |name| name == "Alice" } 16 | # 17 | # if alice 18 | # # ... 19 | # end 20 | # ``` 21 | # 22 | # YAML configuration example: 23 | # 24 | # ``` 25 | # Lint/NotNil: 26 | # Enabled: true 27 | # ``` 28 | class NotNil < Base 29 | properties do 30 | since_version "1.3.0" 31 | description "Identifies usage of `not_nil!` calls" 32 | end 33 | 34 | MSG = "Avoid using `not_nil!`" 35 | 36 | def test(source) 37 | AST::NodeVisitor.new self, source, skip: :macro 38 | end 39 | 40 | def test(source, node : Crystal::Call) 41 | return unless node.name == "not_nil!" 42 | return unless node.obj && node.args.empty? 43 | 44 | issue_for node, MSG, prefer_name_location: true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/not_nil_after_no_bang.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # This rule is used to identify usage of `index/rindex/find/match` calls 3 | # followed by a call to `not_nil!`. 4 | # 5 | # For example, this is considered a code smell: 6 | # 7 | # ``` 8 | # %w[Alice Bob].find(&.chars.any?(&.in?('o', 'b'))).not_nil! 9 | # ``` 10 | # 11 | # And can be written as this: 12 | # 13 | # ``` 14 | # %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b'))) 15 | # ``` 16 | # 17 | # YAML configuration example: 18 | # 19 | # ``` 20 | # Lint/NotNilAfterNoBang: 21 | # Enabled: true 22 | # ``` 23 | class NotNilAfterNoBang < Base 24 | include AST::Util 25 | 26 | properties do 27 | since_version "1.3.0" 28 | description "Identifies usage of `index/rindex/find/match` calls followed by `not_nil!`" 29 | end 30 | 31 | MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`" 32 | 33 | BLOCK_CALL_NAMES = %w[index rindex find] 34 | CALL_NAMES = %w[index rindex match] 35 | 36 | def test(source) 37 | AST::NodeVisitor.new self, source, skip: :macro 38 | end 39 | 40 | def test(source, node : Crystal::Call) 41 | return unless node.name == "not_nil!" && node.args.empty? 42 | return unless (obj = node.obj).is_a?(Crystal::Call) 43 | return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES) 44 | 45 | return unless name_location = name_location(obj) 46 | return unless name_location_end = name_end_location(obj) 47 | return unless end_location = name_end_location(node) 48 | 49 | msg = MSG % {obj.name, obj.name} 50 | 51 | issue_for name_location, end_location, msg do |corrector| 52 | corrector.insert_after(name_location_end, '!') 53 | corrector.remove_trailing(node, {{ ".not_nil!".size }}) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/rand_zero.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows `rand(0)` and `rand(1)` calls. 3 | # Such calls always return `0`. 4 | # 5 | # For example: 6 | # 7 | # ``` 8 | # rand(1) 9 | # ``` 10 | # 11 | # Should be written as: 12 | # 13 | # ``` 14 | # rand 15 | # # or 16 | # rand(2) 17 | # ``` 18 | # 19 | # YAML configuration example: 20 | # 21 | # ``` 22 | # Lint/RandZero: 23 | # Enabled: true 24 | # ``` 25 | class RandZero < Base 26 | properties do 27 | since_version "0.5.1" 28 | description "Disallows rand zero calls" 29 | end 30 | 31 | MSG = "`%s` always returns `0`" 32 | 33 | def test(source, node : Crystal::Call) 34 | return unless node.name == "rand" && 35 | node.args.size == 1 && 36 | (arg = node.args.first).is_a?(Crystal::NumberLiteral) && 37 | arg.value.in?("0", "1") 38 | 39 | issue_for node, MSG % node 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/redundant_string_coercion.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows string conversion in string interpolation, 3 | # which is redundant. 4 | # 5 | # For example, this is considered invalid: 6 | # 7 | # ``` 8 | # "Hello, #{name.to_s}" 9 | # ``` 10 | # 11 | # And this is valid: 12 | # 13 | # ``` 14 | # "Hello, #{name}" 15 | # ``` 16 | # 17 | # YAML configuration example: 18 | # 19 | # ``` 20 | # Lint/RedundantStringCoercion 21 | # Enabled: true 22 | # ``` 23 | class RedundantStringCoercion < Base 24 | include AST::Util 25 | 26 | properties do 27 | since_version "0.12.0" 28 | description "Disallows redundant string conversions in interpolation" 29 | end 30 | 31 | MSG = "Redundant use of `Object#to_s` in interpolation" 32 | 33 | def test(source, node : Crystal::StringInterpolation) 34 | each_string_coercion_node(node) do |expr| 35 | issue_for name_location(expr), expr.end_location, MSG 36 | end 37 | end 38 | 39 | private def each_string_coercion_node(node, &) 40 | node.expressions.each do |exp| 41 | yield exp if exp.is_a?(Crystal::Call) && 42 | exp.name == "to_s" && 43 | exp.args.size.zero? && 44 | exp.named_args.nil? && 45 | exp.obj 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/redundant_with_index.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows redundant `with_index` calls. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # collection.each.with_index do |e| 8 | # # ... 9 | # end 10 | # 11 | # collection.each_with_index do |e, _| 12 | # # ... 13 | # end 14 | # ``` 15 | # 16 | # and it should be written as follows: 17 | # 18 | # ``` 19 | # collection.each do |e| 20 | # # ... 21 | # end 22 | # ``` 23 | # 24 | # YAML configuration example: 25 | # 26 | # ``` 27 | # Lint/RedundantWithIndex: 28 | # Enabled: true 29 | # ``` 30 | class RedundantWithIndex < Base 31 | properties do 32 | since_version "0.11.0" 33 | description "Disallows redundant `with_index` calls" 34 | end 35 | 36 | def test(source, node : Crystal::Call) 37 | args, block = node.args, node.block 38 | 39 | return if block.nil? || args.size > 1 40 | return if with_index_arg?(block) 41 | 42 | case node.name 43 | when "with_index" 44 | report source, node, "Remove redundant with_index" 45 | when "each_with_index" 46 | report source, node, "Use each instead of each_with_index" 47 | end 48 | end 49 | 50 | private def with_index_arg?(block : Crystal::Block) 51 | block.args.size >= 2 && block.args.last.name != "_" 52 | end 53 | 54 | private def report(source, node, msg) 55 | issue_for node, msg, prefer_name_location: true 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/redundant_with_object.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows redundant `each_with_object` calls. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # collection.each_with_object(0) do |e| 8 | # # ... 9 | # end 10 | # 11 | # collection.each_with_object(0) do |e, _| 12 | # # ... 13 | # end 14 | # ``` 15 | # 16 | # and it should be written as follows: 17 | # 18 | # ``` 19 | # collection.each do |e| 20 | # # ... 21 | # end 22 | # ``` 23 | # 24 | # YAML configuration example: 25 | # 26 | # ``` 27 | # Lint/RedundantWithObject: 28 | # Enabled: true 29 | # ``` 30 | class RedundantWithObject < Base 31 | properties do 32 | since_version "0.11.0" 33 | description "Disallows redundant `with_object` calls" 34 | end 35 | 36 | MSG = "Use `each` instead of `each_with_object`" 37 | 38 | def test(source, node : Crystal::Call) 39 | return if node.name != "each_with_object" || 40 | node.args.size != 1 || 41 | !(block = node.block) || 42 | with_index_arg?(block) 43 | 44 | issue_for node, MSG, prefer_name_location: true 45 | end 46 | 47 | private def with_index_arg?(block : Crystal::Block) 48 | block.args.size >= 2 && block.args.last.name != "_" 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/require_parentheses.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows method calls with at least one argument, where no 3 | # parentheses are used around the argument list, and a logical operator 4 | # (`&&` or `||`) is used within the argument list. 5 | # 6 | # For example, this is considered invalid: 7 | # 8 | # ``` 9 | # if foo.includes? "bar" || foo.includes? "baz" 10 | # # ... 11 | # end 12 | # ``` 13 | # 14 | # And need to be written as: 15 | # 16 | # ``` 17 | # if foo.includes?("bar") || foo.includes?("baz") 18 | # # ... 19 | # end 20 | # ``` 21 | # 22 | # YAML configuration example: 23 | # 24 | # ``` 25 | # Lint/RequireParentheses: 26 | # Enabled: true 27 | # ``` 28 | class RequireParentheses < Base 29 | properties do 30 | since_version "1.7.0" 31 | description "Disallows method calls with no parentheses and a logical operator in the argument list" 32 | end 33 | 34 | MSG = "Use parentheses in the method call to avoid confusion about precedence" 35 | 36 | ALLOWED_CALL_NAMES = %w[[]? []] 37 | 38 | def test(source, node : Crystal::Call) 39 | return if node.args.empty? || 40 | node.has_parentheses? || 41 | node.name.ends_with?('=') || 42 | node.name.in?(ALLOWED_CALL_NAMES) 43 | 44 | node.args.each do |arg| 45 | next unless arg.is_a?(Crystal::BinaryOp) 46 | next unless (right = arg.right).is_a?(Crystal::Call) 47 | next if right.args.empty? 48 | 49 | issue_for node, MSG 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/shadowed_argument.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows shadowed arguments. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # do_something do |foo| 8 | # foo = 1 # shadows block argument 9 | # foo 10 | # end 11 | # 12 | # def do_something(foo) 13 | # foo = 1 # shadows method argument 14 | # foo 15 | # end 16 | # ``` 17 | # 18 | # and it should be written as follows: 19 | # 20 | # ``` 21 | # do_something do |foo| 22 | # foo = foo + 42 23 | # foo 24 | # end 25 | # 26 | # def do_something(foo) 27 | # foo = foo + 42 28 | # foo 29 | # end 30 | # ``` 31 | # 32 | # YAML configuration example: 33 | # 34 | # ``` 35 | # Lint/ShadowedArgument: 36 | # Enabled: true 37 | # ``` 38 | class ShadowedArgument < Base 39 | properties do 40 | since_version "0.7.0" 41 | description "Disallows shadowed arguments" 42 | end 43 | 44 | MSG = "Argument `%s` is assigned before it is used" 45 | 46 | def test(source) 47 | AST::ScopeVisitor.new self, source 48 | end 49 | 50 | def test(source, node, scope : AST::Scope) 51 | scope.arguments.each do |arg| 52 | next unless assign = arg.variable.assign_before_reference 53 | 54 | issue_for assign, MSG % arg.name 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/signal_trap.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that reports when `Signal::INT/HUP/TERM.trap` is used, 3 | # which should be replaced with `Process.on_terminate` instead - 4 | # a more portable alternative. 5 | # 6 | # For example, this is considered invalid: 7 | # 8 | # ``` 9 | # Signal::INT.trap do 10 | # shutdown 11 | # end 12 | # ``` 13 | # 14 | # And it should be written as this: 15 | # 16 | # ``` 17 | # Process.on_terminate do 18 | # shutdown 19 | # end 20 | # ``` 21 | # 22 | # YAML configuration example: 23 | # 24 | # ``` 25 | # Lint/SignalTrap: 26 | # Enabled: true 27 | # ``` 28 | class SignalTrap < Base 29 | include AST::Util 30 | 31 | properties do 32 | since_version "1.7.0" 33 | description "Disallows `Signal::INT/HUP/TERM.trap` in favor of `Process.on_terminate`" 34 | end 35 | 36 | MSG = "Use `Process.on_terminate` instead of `%s.trap`" 37 | 38 | def test(source, node : Crystal::Call) 39 | return unless (obj = node.obj).is_a?(Crystal::Path) 40 | return unless path_named?(obj, "Signal::INT", "Signal::HUP", "Signal::TERM") 41 | return unless node.name == "trap" 42 | 43 | if (name_location = name_location(node)) && (name_end_location = name_end_location(node)) 44 | issue_for node.location, name_end_location, MSG % obj do |corrector| 45 | corrector.replace obj, "Process" 46 | corrector.replace name_location, name_end_location, "on_terminate" 47 | end 48 | else 49 | issue_for node, MSG % obj 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/spec_eq_with_bool_or_nil_literal.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # Reports `eq(true|false|nil)` expectations in specs. 3 | # 4 | # This is considered bad: 5 | # 6 | # ``` 7 | # it "works" do 8 | # foo.is_a?(String).should eq true 9 | # foo.is_a?(Int32).should eq false 10 | # foo.as?(Symbol).should eq nil 11 | # end 12 | # ``` 13 | # 14 | # And it should be written as the following: 15 | # 16 | # ``` 17 | # it "works" do 18 | # foo.is_a?(String).should be_true 19 | # foo.is_a?(Int32).should be_false 20 | # foo.as?(Symbol).should be_nil 21 | # end 22 | # ``` 23 | # 24 | # YAML configuration example: 25 | # 26 | # ``` 27 | # Lint/SpecEqWithBoolOrNilLiteral: 28 | # Enabled: true 29 | # ``` 30 | class SpecEqWithBoolOrNilLiteral < Base 31 | properties do 32 | since_version "1.7.0" 33 | description "Reports `eq(true|false|nil)` expectations in specs" 34 | end 35 | 36 | MSG = "Use `%s` instead of `%s` expectation" 37 | 38 | def test(source) 39 | return super if source.spec? 40 | end 41 | 42 | def test(source, node : Crystal::Call) 43 | return unless node.name.in?("should", "should_not") 44 | return unless node.block.nil? && node.args.size == 1 45 | 46 | return unless (matcher = node.args.first).is_a?(Crystal::Call) 47 | return unless matcher.name == "eq" 48 | return unless matcher.block.nil? && matcher.args.size == 1 49 | 50 | replacement = 51 | case arg = matcher.args.first 52 | when Crystal::BoolLiteral then arg.value ? "be_true" : "be_false" 53 | when Crystal::NilLiteral then "be_nil" 54 | end 55 | return unless replacement 56 | 57 | issue_for matcher, MSG % {replacement, matcher.to_s} do |corrector| 58 | corrector.replace(matcher, replacement) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/spec_filename.cr: -------------------------------------------------------------------------------- 1 | require "file_utils" 2 | 3 | module Ameba::Rule::Lint 4 | # A rule that enforces spec filenames to have `_spec` suffix. 5 | # 6 | # YAML configuration example: 7 | # 8 | # ``` 9 | # Lint/SpecFilename: 10 | # Enabled: true 11 | # IgnoredDirs: [spec/support spec/fixtures spec/data] 12 | # IgnoredFilenames: [spec_helper] 13 | # ``` 14 | class SpecFilename < Base 15 | properties do 16 | since_version "1.6.0" 17 | description "Enforces spec filenames to have `_spec` suffix" 18 | ignored_dirs %w[spec/support spec/fixtures spec/data] 19 | ignored_filenames %w[spec_helper] 20 | end 21 | 22 | MSG = "Spec filename should have `_spec` suffix: `%s.cr`, not `%s.cr`" 23 | 24 | private LOCATION = {1, 1} 25 | 26 | # TODO: fix the assumption that *source.path* contains relative path 27 | def test(source : Source) 28 | path_ = Path[source.path].to_posix 29 | name = path_.stem 30 | path = path_.to_s 31 | 32 | # check only files within spec/ directory 33 | return unless path.starts_with?("spec/") 34 | # check only files with `.cr` extension 35 | return unless path.ends_with?(".cr") 36 | # ignore files having `_spec` suffix 37 | return if name.ends_with?("_spec") 38 | 39 | # ignore known false-positives 40 | ignored_dirs.each do |substr| 41 | return if path.starts_with?("#{substr}/") 42 | end 43 | return if name.in?(ignored_filenames) 44 | 45 | expected = "#{name}_spec" 46 | 47 | issue_for LOCATION, MSG % {expected, name} do 48 | new_path = 49 | path_.sibling(expected + path_.extension) 50 | 51 | FileUtils.mv(path, new_path) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/spec_focus.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # Checks if specs are focused. 3 | # 4 | # In specs `focus: true` is mainly used to focus on a spec 5 | # item locally during development. However, if such change 6 | # is committed, it silently runs only focused spec on all 7 | # other environment, which is undesired. 8 | # 9 | # This is considered bad: 10 | # 11 | # ``` 12 | # describe MyClass, focus: true do 13 | # end 14 | # 15 | # describe ".new", focus: true do 16 | # end 17 | # 18 | # context "my context", focus: true do 19 | # end 20 | # 21 | # it "works", focus: true do 22 | # end 23 | # ``` 24 | # 25 | # And it should be written as the following: 26 | # 27 | # ``` 28 | # describe MyClass do 29 | # end 30 | # 31 | # describe ".new" do 32 | # end 33 | # 34 | # context "my context" do 35 | # end 36 | # 37 | # it "works" do 38 | # end 39 | # ``` 40 | # 41 | # YAML configuration example: 42 | # 43 | # ``` 44 | # Lint/SpecFocus: 45 | # Enabled: true 46 | # ``` 47 | class SpecFocus < Base 48 | properties do 49 | since_version "0.14.0" 50 | description "Reports focused spec items" 51 | end 52 | 53 | MSG = "Focused spec item detected" 54 | 55 | SPEC_ITEM_NAMES = %w[describe context it pending] 56 | 57 | def test(source) 58 | return unless source.spec? 59 | 60 | AST::NodeVisitor.new self, source 61 | end 62 | 63 | def test(source, node : Crystal::Call) 64 | return unless node.name.in?(SPEC_ITEM_NAMES) 65 | return unless node.block 66 | 67 | arg = node.named_args.try &.find(&.name.== "focus") 68 | return if arg.nil? || 69 | arg.value.is_a?(Crystal::Call) || 70 | arg.value.is_a?(Crystal::Var) 71 | 72 | issue_for arg, MSG 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/syntax.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that reports invalid Crystal syntax. 3 | # 4 | # For example, this syntax is invalid: 5 | # 6 | # ``` 7 | # def hello 8 | # do_something 9 | # rescue Exception => e 10 | # end 11 | # ``` 12 | # 13 | # And should be properly written: 14 | # 15 | # ``` 16 | # def hello 17 | # do_something 18 | # rescue e : Exception 19 | # end 20 | # ``` 21 | class Syntax < Base 22 | properties do 23 | since_version "0.4.2" 24 | description "Reports invalid Crystal syntax" 25 | severity :error 26 | end 27 | 28 | def test(source) 29 | source.ast 30 | rescue e : Crystal::SyntaxException 31 | issue_for({e.line_number, e.column_number}, e.message.to_s) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/top_level_operator_definition.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows top level operator method definitions, since these cannot be called. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # def +(other) 8 | # end 9 | # ``` 10 | # 11 | # And has to be written within a class, struct, or module: 12 | # 13 | # ``` 14 | # class Foo 15 | # def +(other) 16 | # end 17 | # end 18 | # ``` 19 | # 20 | # YAML configuration example: 21 | # 22 | # ``` 23 | # Lint/TopLevelOperatorDefinition: 24 | # Enabled: true 25 | # ``` 26 | class TopLevelOperatorDefinition < Base 27 | properties do 28 | since_version "1.7.0" 29 | description "Disallows top level operator method definitions" 30 | end 31 | 32 | MSG = "Top level operator method definitions cannot be called" 33 | 34 | def test(source) 35 | AST::NodeVisitor.new self, source, skip: [ 36 | Crystal::ClassDef, 37 | Crystal::EnumDef, 38 | Crystal::ModuleDef, 39 | Crystal::Call, 40 | ] 41 | end 42 | 43 | def test(source, node : Crystal::Def) 44 | return if node.receiver || node.name == "->" 45 | return if node.name.chars.any?(&.alphanumeric?) 46 | 47 | issue_for node, MSG 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/trailing_rescue_exception.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that prohibits the misconception about how trailing `rescue` statements work, 3 | # preventing Paths (exception class names or otherwise) from being used as the 4 | # trailing value. The value after the trailing `rescue` statement is the value 5 | # to use if an exception occurs, not the exception class to rescue from. 6 | # 7 | # For example, this is considered invalid - if an exception occurs, 8 | # `response` will be assigned with the value of `IO::Error` instead of `nil`: 9 | # 10 | # ``` 11 | # response = HTTP::Client.get("http://www.example.com") rescue IO::Error 12 | # ``` 13 | # 14 | # And should instead be written as this in order to capture only `IO::Error` exceptions: 15 | # 16 | # ``` 17 | # response = begin 18 | # HTTP::Client.get("http://www.example.com") 19 | # rescue IO::Error 20 | # "default value" 21 | # end 22 | # ``` 23 | # 24 | # Or to rescue all exceptions (instead of just `IO::Error`): 25 | # 26 | # ``` 27 | # response = HTTP::Client.get("http://www.example.com") rescue "default value" 28 | # ``` 29 | # 30 | # YAML configuration example: 31 | # 32 | # ``` 33 | # Lint/TrailingRescueException: 34 | # Enabled: true 35 | # ``` 36 | class TrailingRescueException < Base 37 | properties do 38 | since_version "1.7.0" 39 | description "Disallows trailing `rescue` with a path" 40 | end 41 | 42 | MSG = "Use a block variant of `rescue` to filter by the exception type" 43 | 44 | def test(source, node : Crystal::ExceptionHandler) 45 | return unless node.suffix && 46 | (rescues = node.rescues) && 47 | (resc = rescues.first?) && 48 | resc.body.is_a?(Crystal::Path) 49 | 50 | issue_for resc.body, MSG, prefer_name_location: true 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unreachable_code.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that reports unreachable code. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # def method(a) 8 | # return 42 9 | # a + 1 10 | # end 11 | # ``` 12 | # 13 | # ``` 14 | # a = 1 15 | # loop do 16 | # break 17 | # a += 1 18 | # end 19 | # ``` 20 | # 21 | # And has to be written as the following: 22 | # 23 | # ``` 24 | # def method(a) 25 | # return 42 if a == 0 26 | # a + 1 27 | # end 28 | # ``` 29 | # 30 | # ``` 31 | # a = 1 32 | # loop do 33 | # break a > 3 34 | # a += 1 35 | # end 36 | # ``` 37 | # 38 | # YAML configuration example: 39 | # 40 | # ``` 41 | # Lint/UnreachableCode: 42 | # Enabled: true 43 | # ``` 44 | class UnreachableCode < Base 45 | properties do 46 | since_version "0.9.0" 47 | description "Reports unreachable code" 48 | end 49 | 50 | MSG = "Unreachable code detected" 51 | 52 | def test(source) 53 | AST::FlowExpressionVisitor.new self, source 54 | end 55 | 56 | def test(source, node, flow_expression : AST::FlowExpression) 57 | return unless unreachable_node = flow_expression.unreachable_nodes.first? 58 | issue_for unreachable_node, MSG 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_class_variable_access.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused class variable access. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # class MyClass 8 | # @@my_var : String = "hello" 9 | # 10 | # @@my_var 11 | # 12 | # def hello : String 13 | # @@my_var 14 | # 15 | # "hello, world!" 16 | # end 17 | # end 18 | # ``` 19 | # 20 | # And these are considered valid: 21 | # 22 | # ``` 23 | # class MyClass 24 | # @@my_var : String = "hello" 25 | # 26 | # @@my_other_var = @@my_var 27 | # 28 | # def hello : String 29 | # return @@my_var if @@my_var == "hello" 30 | # 31 | # "hello, world!" 32 | # end 33 | # end 34 | # ``` 35 | # 36 | # YAML configuration example: 37 | # 38 | # ``` 39 | # Lint/UnusedClassVariableAccess: 40 | # Enabled: true 41 | # ``` 42 | class UnusedClassVariableAccess < Base 43 | properties do 44 | since_version "1.7.0" 45 | description "Disallows unused access to class variables" 46 | end 47 | 48 | MSG = "Value from class variable access is unused" 49 | 50 | def test(source : Source) 51 | AST::ImplicitReturnVisitor.new(self, source) 52 | end 53 | 54 | def test(source, node : Crystal::ClassVar, in_macro : Bool) 55 | # Class variables aren't supported in macros 56 | return if in_macro 57 | 58 | issue_for node, MSG 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_comparison.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused comparisons. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # a = obj.method do |x| 8 | # x == 1 # => Comparison operation has no effect 9 | # puts x 10 | # end 11 | # 12 | # b = if a >= 0 13 | # c < 1 # => Comparison operation has no effect 14 | # "hello world" 15 | # end 16 | # ``` 17 | # 18 | # And these are considered valid: 19 | # 20 | # ``` 21 | # a = obj.method do |x| 22 | # x == 1 23 | # end 24 | # 25 | # b = if a >= 0 && 26 | # c < 1 27 | # "hello world" 28 | # end 29 | # ``` 30 | # 31 | # YAML configuration example: 32 | # 33 | # ``` 34 | # Lint/UnusedComparison: 35 | # Enabled: true 36 | # ``` 37 | class UnusedComparison < Base 38 | properties do 39 | since_version "1.7.0" 40 | description "Disallows unused comparison operations" 41 | end 42 | 43 | MSG = "Comparison operation is unused" 44 | 45 | COMPARISON_OPERATORS = %w[== != < <= > >= <=>] 46 | 47 | def test(source : Source) 48 | AST::ImplicitReturnVisitor.new(self, source) 49 | end 50 | 51 | def test(source, node : Crystal::Call, in_macro : Bool) 52 | if node.name.in?(COMPARISON_OPERATORS) && node.args.size == 1 53 | issue_for node, MSG 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_instance_variable_access.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused instance variable access. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # class MyClass 8 | # @my_var : String = "hello" 9 | # 10 | # @my_var 11 | # 12 | # def hello : String 13 | # @my_var 14 | # 15 | # "hello, world!" 16 | # end 17 | # end 18 | # ``` 19 | # 20 | # And these are considered valid: 21 | # 22 | # ``` 23 | # class MyClass 24 | # @my_var : String = "hello" 25 | # 26 | # @my_other_var = @my_var 27 | # 28 | # def hello : String 29 | # return @my_var if @my_var == "hello" 30 | # 31 | # "hello, world!" 32 | # end 33 | # end 34 | # ``` 35 | # 36 | # YAML configuration example: 37 | # 38 | # ``` 39 | # Lint/UnusedInstanceVariableAccess: 40 | # Enabled: true 41 | # ``` 42 | class UnusedInstanceVariableAccess < Base 43 | properties do 44 | since_version "1.7.0" 45 | description "Disallows unused access to instance variables" 46 | end 47 | 48 | MSG = "Value from instance variable access is unused" 49 | 50 | def test(source : Source) 51 | AST::ImplicitReturnVisitor.new(self, source) 52 | end 53 | 54 | def test(source, node : Crystal::InstanceVar, in_macro : Bool) 55 | # Handle special case when using `@type` within a method body has side-effects 56 | return if in_macro && node.name == "@type" 57 | 58 | issue_for node, MSG 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_literal.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused literal values (strings, symbols, integers, etc). 3 | # 4 | # For example, these are considered invalid: 5 | # 6 | # ``` 7 | # 1234_f32 8 | # 9 | # "hello world" 10 | # 11 | # if check? 12 | # true 13 | # else 14 | # false 15 | # end 16 | # 17 | # def method 18 | # if guard? 19 | # false 20 | # end 21 | # 22 | # true 23 | # end 24 | # ``` 25 | # 26 | # And these are considered valid: 27 | # 28 | # ``` 29 | # a = 1234_f32 30 | # 31 | # def method 32 | # if guard? 33 | # false 34 | # else 35 | # true 36 | # end 37 | # end 38 | # ``` 39 | # 40 | # YAML configuration example: 41 | # 42 | # ``` 43 | # Lint/UnusedLiteral: 44 | # Enabled: true 45 | # ``` 46 | class UnusedLiteral < Base 47 | properties do 48 | since_version "1.7.0" 49 | description "Disallows unused literal values" 50 | end 51 | 52 | MSG = "Literal value is not used" 53 | 54 | def test(source : Source) 55 | AST::ImplicitReturnVisitor.new(self, source) 56 | end 57 | 58 | def test(source, node : Crystal::RegexLiteral, in_macro : Bool) 59 | # Locations for Regex literals were added in Crystal v1.15.0 60 | {% if compare_versions(Crystal::VERSION, "1.15.0") >= 0 %} 61 | issue_for node, MSG 62 | {% end %} 63 | end 64 | 65 | def test( 66 | source, 67 | node : Crystal::BoolLiteral | Crystal::CharLiteral | Crystal::HashLiteral | 68 | Crystal::ProcLiteral | Crystal::ArrayLiteral | Crystal::RangeLiteral | 69 | Crystal::TupleLiteral | Crystal::NumberLiteral | 70 | Crystal::StringLiteral | Crystal::SymbolLiteral | 71 | Crystal::NamedTupleLiteral | Crystal::StringInterpolation, 72 | in_macro : Bool, 73 | ) 74 | issue_for node, MSG 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_local_variable_access.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused local variable access. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # a = 1 8 | # b = "2" 9 | # c = :3 10 | # 11 | # case method_call 12 | # when Int32 13 | # a 14 | # when String 15 | # b 16 | # else 17 | # c 18 | # end 19 | # 20 | # def hello(name) 21 | # if name.size < 10 22 | # name 23 | # end 24 | # 25 | # name[...10] 26 | # end 27 | # ``` 28 | # 29 | # And these are considered valid: 30 | # 31 | # ``` 32 | # a = 1 33 | # b = "2" 34 | # c = :3 35 | # 36 | # d = case method_call 37 | # when Int32 38 | # a 39 | # when String 40 | # b 41 | # else 42 | # c 43 | # end 44 | # 45 | # def hello(name) 46 | # if name.size < 10 47 | # return name 48 | # end 49 | # 50 | # name[...10] 51 | # end 52 | # ``` 53 | # 54 | # YAML configuration example: 55 | # 56 | # ``` 57 | # Lint/UnusedLocalVariableAccess: 58 | # Enabled: true 59 | # ``` 60 | class UnusedLocalVariableAccess < Base 61 | properties do 62 | since_version "1.7.0" 63 | description "Disallows unused access to local variables" 64 | end 65 | 66 | MSG = "Value from local variable access is unused" 67 | 68 | def test(source : Source) 69 | AST::ImplicitReturnVisitor.new(self, source) 70 | end 71 | 72 | def test(source, node : Crystal::Var, in_macro : Bool) 73 | return if node.name == "self" # This case will be reported by `Lint/UnusedSelf` rule 74 | return if in_macro && node.name.in?("debug", "skip_file") 75 | 76 | issue_for node, MSG 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_pseudo_method_call.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused pseudo method calls (is_a?, sizeof, etc). 3 | # 4 | # For example, these are considered invalid: 5 | # 6 | # ``` 7 | # pointerof(foo) 8 | # sizeof(Bar) 9 | # 10 | # def method 11 | # !!valid? if guard? 12 | # nil 13 | # end 14 | # ``` 15 | # 16 | # YAML configuration example: 17 | # 18 | # ``` 19 | # Lint/UnusedPseudoMethodCall: 20 | # Enabled: true 21 | # ``` 22 | class UnusedPseudoMethodCall < Base 23 | properties do 24 | since_version "1.7.0" 25 | description "Disallows unused pseudo-method calls" 26 | end 27 | 28 | MSG = "Pseudo-method call is not used" 29 | 30 | def test(source : Source) 31 | AST::ImplicitReturnVisitor.new(self, source) 32 | end 33 | 34 | def test( 35 | source, 36 | node : Crystal::PointerOf | Crystal::SizeOf | Crystal::InstanceSizeOf | 37 | Crystal::AlignOf | Crystal::InstanceAlignOf | Crystal::OffsetOf | 38 | Crystal::IsA | Crystal::NilableCast | Crystal::RespondsTo | Crystal::Not, 39 | in_macro : Bool, 40 | ) 41 | issue_for node, MSG 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/unused_self_access.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows unused accesses of `self`. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # class MyClass 8 | # self 9 | # 10 | # def self.foo 11 | # self 12 | # puts "Hello, world!" 13 | # end 14 | # end 15 | # ``` 16 | # 17 | # YAML configuration example: 18 | # 19 | # ``` 20 | # Lint/UnusedSelfAccess: 21 | # Enabled: true 22 | # ``` 23 | class UnusedSelfAccess < Base 24 | properties do 25 | since_version "1.7.0" 26 | description "Disallows unused self" 27 | end 28 | 29 | MSG = "`self` is not used" 30 | 31 | def test(source : Source) 32 | AST::ImplicitReturnVisitor.new(self, source) 33 | end 34 | 35 | def test(source, node : Crystal::Self, in_macro : Bool) 36 | issue_for node, MSG 37 | end 38 | 39 | def test(source, node : Crystal::Var, in_macro : Bool) 40 | issue_for node, MSG if node.name == "self" 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/ameba/rule/lint/useless_assign.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Lint 2 | # A rule that disallows useless assignments. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # def method 8 | # var = 1 9 | # do_something 10 | # end 11 | # ``` 12 | # 13 | # And has to be written as the following: 14 | # 15 | # ``` 16 | # def method 17 | # var = 1 18 | # do_something(var) 19 | # end 20 | # ``` 21 | # 22 | # YAML configuration example: 23 | # 24 | # ``` 25 | # Lint/UselessAssign: 26 | # Enabled: true 27 | # ExcludeTypeDeclarations: false 28 | # ``` 29 | class UselessAssign < Base 30 | properties do 31 | since_version "0.6.0" 32 | description "Disallows useless variable assignments" 33 | exclude_type_declarations false 34 | end 35 | 36 | MSG = "Useless assignment to variable `%s`" 37 | 38 | def test(source) 39 | AST::ScopeVisitor.new self, source 40 | end 41 | 42 | def test(source, node, scope : AST::Scope) 43 | return if scope.lib_def?(check_outer_scopes: true) 44 | 45 | scope.variables.each do |var| 46 | next if var.ignored? || var.used_in_macro? || var.captured_by_block? 47 | next if exclude_type_declarations? && scope.assigns_type_dec?(var.name) 48 | 49 | var.assignments.each do |assign| 50 | check_assignment(source, assign, var) 51 | end 52 | end 53 | end 54 | 55 | private def check_assignment(source, assign, var) 56 | return if assign.referenced? 57 | 58 | case target_node = assign.target_node 59 | when Crystal::TypeDeclaration 60 | issue_for target_node.var, MSG % var.name 61 | else 62 | issue_for target_node, MSG % var.name 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/ameba/rule/metrics/cyclomatic_complexity.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Metrics 2 | # A rule that disallows methods with a cyclomatic complexity higher than `MaxComplexity` 3 | # 4 | # YAML configuration example: 5 | # 6 | # ``` 7 | # Metrics/CyclomaticComplexity: 8 | # Enabled: true 9 | # MaxComplexity: 12 10 | # ``` 11 | class CyclomaticComplexity < Base 12 | properties do 13 | since_version "0.9.1" 14 | description "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`" 15 | max_complexity 12 16 | end 17 | 18 | MSG = "Cyclomatic complexity too high [%d/%d]" 19 | 20 | def test(source, node : Crystal::Def) 21 | complexity = AST::CountingVisitor.new(node).count 22 | return unless complexity > max_complexity 23 | 24 | issue_for node, MSG % {complexity, max_complexity}, prefer_name_location: true 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/binary_operator_parameter_name.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that enforces that certain binary operator methods have 3 | # their sole parameter named `other`. 4 | # 5 | # For example, this is considered valid: 6 | # 7 | # ``` 8 | # class Money 9 | # def +(other) 10 | # end 11 | # end 12 | # ``` 13 | # 14 | # And this is invalid parameter name: 15 | # 16 | # ``` 17 | # class Money 18 | # def +(amount) 19 | # end 20 | # end 21 | # ``` 22 | # 23 | # YAML configuration example: 24 | # 25 | # ``` 26 | # Naming/BinaryOperatorParameterName: 27 | # Enabled: true 28 | # ExcludedOperators: ["[]", "[]?", "[]=", "<<", ">>", "=~", "!~"] 29 | # ``` 30 | class BinaryOperatorParameterName < Base 31 | properties do 32 | since_version "1.6.0" 33 | description "Enforces that certain binary operator methods have " \ 34 | "their sole parameter named `other`" 35 | excluded_operators %w[[] []? []= << >> ` =~ !~] 36 | end 37 | 38 | MSG = "When defining the `%s` operator, name its argument `other`" 39 | 40 | def test(source, node : Crystal::Def) 41 | name = node.name 42 | 43 | return if name == "->" || name.in?(excluded_operators) 44 | return if name.chars.any?(&.alphanumeric?) 45 | return unless node.args.size == 1 46 | return if (arg = node.args.first).name == "other" 47 | 48 | issue_for arg, MSG % name 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/block_parameter_name.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that reports non-descriptive block parameter names. 3 | # 4 | # Favour this: 5 | # 6 | # ``` 7 | # tokens.each { |token| token.last_accessed_at = Time.utc } 8 | # ``` 9 | # 10 | # Over this: 11 | # 12 | # ``` 13 | # tokens.each { |t| t.last_accessed_at = Time.utc } 14 | # ``` 15 | # 16 | # YAML configuration example: 17 | # 18 | # ``` 19 | # Naming/BlockParameterName: 20 | # Enabled: true 21 | # MinNameLength: 3 22 | # AllowNamesEndingInNumbers: true 23 | # AllowedNames: [e, i, j, k, v, x, y, ex, io, ws, op, tx, id, ip, k1, k2, v1, v2, wg] 24 | # ForbiddenNames: [] 25 | # ``` 26 | class BlockParameterName < Base 27 | properties do 28 | since_version "1.6.0" 29 | description "Disallows non-descriptive block parameter names" 30 | min_name_length 3 31 | allow_names_ending_in_numbers true 32 | allowed_names %w[e i j k v x y ex io ws op tx id ip k1 k2 v1 v2 wg] 33 | forbidden_names %w[] 34 | end 35 | 36 | MSG = "Disallowed block parameter name found" 37 | 38 | def test(source, node : Crystal::Call) 39 | node.try(&.block).try(&.args).try &.each do |arg| 40 | issue_for arg, MSG unless valid_name?(arg.name) 41 | end 42 | end 43 | 44 | private def valid_name?(name) 45 | return true if name.blank? # TODO: handle unpacked variables 46 | return true if name.starts_with?('_') || name.in?(allowed_names) 47 | 48 | return false if name.in?(forbidden_names) 49 | return false if name.size < min_name_length 50 | return false if name[-1].ascii_number? && !allow_names_ending_in_numbers? 51 | 52 | true 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/constant_names.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that enforces constant names to be in screaming case. 3 | # 4 | # For example, these constant names are considered valid: 5 | # 6 | # ``` 7 | # LUCKY_NUMBERS = [3, 7, 11] 8 | # DOCUMENTATION_URL = "http://crystal-lang.org/docs" 9 | # ``` 10 | # 11 | # And these are invalid names: 12 | # 13 | # ``` 14 | # myBadConstant = 1 15 | # Wrong_NAME = 2 16 | # ``` 17 | # 18 | # YAML configuration example: 19 | # 20 | # ``` 21 | # Naming/ConstantNames: 22 | # Enabled: true 23 | # ``` 24 | class ConstantNames < Base 25 | properties do 26 | since_version "0.2.0" 27 | description "Enforces constant names to be in screaming case" 28 | end 29 | 30 | MSG = "Constant name should be screaming-cased: `%s`, not `%s`" 31 | 32 | def test(source, node : Crystal::Assign) 33 | return unless (target = node.target).is_a?(Crystal::Path) 34 | 35 | name = target.to_s 36 | expected = name.upcase 37 | 38 | return if name.in?(expected, name.camelcase) 39 | 40 | issue_for target, MSG % {expected, name} 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/filename.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that enforces file names to be in underscored case. 3 | # 4 | # YAML configuration example: 5 | # 6 | # ``` 7 | # Naming/Filename: 8 | # Enabled: true 9 | # ``` 10 | class Filename < Base 11 | properties do 12 | since_version "1.6.0" 13 | description "Enforces file names to be in underscored case" 14 | end 15 | 16 | MSG = "Filename should be underscore-cased: `%s`, not `%s`" 17 | 18 | private LOCATION = {1, 1} 19 | 20 | def test(source : Source) 21 | path = Path[source.path] 22 | name = path.basename 23 | 24 | return if (expected = name.underscore) == name 25 | 26 | issue_for LOCATION, MSG % {expected, name} 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/method_names.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that enforces method names to be in underscored case. 3 | # 4 | # For example, these are considered valid: 5 | # 6 | # ``` 7 | # class Person 8 | # def first_name 9 | # end 10 | # 11 | # def date_of_birth 12 | # end 13 | # 14 | # def homepage_url 15 | # end 16 | # end 17 | # ``` 18 | # 19 | # And these are invalid method names: 20 | # 21 | # ``` 22 | # class Person 23 | # def firstName 24 | # end 25 | # 26 | # def date_of_Birth 27 | # end 28 | # 29 | # def homepageURL 30 | # end 31 | # end 32 | # ``` 33 | # 34 | # YAML configuration example: 35 | # 36 | # ``` 37 | # Naming/MethodNames: 38 | # Enabled: true 39 | # ``` 40 | class MethodNames < Base 41 | properties do 42 | since_version "0.2.0" 43 | description "Enforces method names to be in underscored case" 44 | end 45 | 46 | MSG = "Method name should be underscore-cased: `%s`, not `%s`" 47 | 48 | def test(source, node : Crystal::Def) 49 | name = node.name.to_s 50 | 51 | return if (expected = name.underscore) == name 52 | 53 | issue_for node, MSG % {expected, name}, prefer_name_location: true 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/predicate_name.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that disallows tautological predicate names - 3 | # meaning those that start with the prefix `is_`, except for 4 | # the ones that are not valid Crystal code (e.g. `is_404?`). 5 | # 6 | # Favour this: 7 | # 8 | # ``` 9 | # def valid?(x) 10 | # end 11 | # ``` 12 | # 13 | # Over this: 14 | # 15 | # ``` 16 | # def is_valid?(x) 17 | # end 18 | # ``` 19 | # 20 | # YAML configuration example: 21 | # 22 | # ``` 23 | # Naming/PredicateName: 24 | # Enabled: true 25 | # ``` 26 | class PredicateName < Base 27 | properties do 28 | since_version "0.2.0" 29 | description "Disallows tautological predicate names" 30 | end 31 | 32 | MSG = "Favour method name `%s?` over `%s`" 33 | 34 | def test(source, node : Crystal::Def) 35 | return unless node.name =~ /^is_([a-z]\w*)\??$/ 36 | alternative = $1 37 | 38 | issue_for node, MSG % {alternative, node.name}, prefer_name_location: true 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/rescued_exceptions_variable_name.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that makes sure that rescued exceptions variables are named as expected. 3 | # 4 | # For example, these are considered valid: 5 | # 6 | # def foo 7 | # # potentially raising computations 8 | # rescue e 9 | # Log.error(exception: e) { "Error" } 10 | # end 11 | # 12 | # And these are invalid variable names: 13 | # 14 | # def foo 15 | # # potentially raising computations 16 | # rescue wtf 17 | # Log.error(exception: wtf) { "Error" } 18 | # end 19 | # 20 | # YAML configuration example: 21 | # 22 | # ``` 23 | # Naming/RescuedExceptionsVariableName: 24 | # Enabled: true 25 | # AllowedNames: [e, ex, exception, error] 26 | # ``` 27 | class RescuedExceptionsVariableName < Base 28 | properties do 29 | since_version "1.6.0" 30 | description "Makes sure that rescued exceptions variables are named as expected" 31 | allowed_names %w[e ex exception error] 32 | end 33 | 34 | MSG = "Disallowed variable name, use one of these instead: '%s'" 35 | MSG_SINGULAR = "Disallowed variable name, use '%s' instead" 36 | 37 | def test(source, node : Crystal::ExceptionHandler) 38 | node.rescues.try &.each do |rescue_node| 39 | next if valid_name?(rescue_node.name) 40 | 41 | message = 42 | allowed_names.size == 1 ? MSG_SINGULAR : MSG 43 | 44 | issue_for rescue_node, message % allowed_names.join("', '") 45 | end 46 | end 47 | 48 | private def valid_name?(name) 49 | !name || name.in?(allowed_names) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/type_names.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that enforces type names in camelcase manner. 3 | # 4 | # For example, these are considered valid: 5 | # 6 | # ``` 7 | # class ParseError < Exception 8 | # end 9 | # 10 | # module HTTP 11 | # class RequestHandler 12 | # end 13 | # end 14 | # 15 | # alias NumericValue = Float32 | Float64 | Int32 | Int64 16 | # 17 | # lib LibYAML 18 | # end 19 | # 20 | # struct TagDirective 21 | # end 22 | # 23 | # enum Time::DayOfWeek 24 | # end 25 | # ``` 26 | # 27 | # And these are invalid type names 28 | # 29 | # ``` 30 | # class My_class 31 | # end 32 | # 33 | # module HTT_p 34 | # end 35 | # 36 | # alias Numeric_value = Int32 37 | # 38 | # lib Lib_YAML 39 | # end 40 | # 41 | # struct Tag_directive 42 | # end 43 | # 44 | # enum Time_enum::Day_of_week 45 | # end 46 | # ``` 47 | # 48 | # YAML configuration example: 49 | # 50 | # ``` 51 | # Naming/TypeNames: 52 | # Enabled: true 53 | # ``` 54 | class TypeNames < Base 55 | properties do 56 | since_version "0.2.0" 57 | description "Enforces type names in camelcase manner" 58 | end 59 | 60 | MSG = "Type name should be camelcased: `%s`, not `%s`" 61 | 62 | def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef) 63 | name = node.name.to_s 64 | 65 | return if (expected = name.camelcase) == name 66 | 67 | issue_for node.name, MSG % {expected, name} 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /src/ameba/rule/naming/variable_names.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Naming 2 | # A rule that enforces variable names to be in underscored case. 3 | # 4 | # For example, these variable names are considered valid: 5 | # 6 | # ``` 7 | # var_name = 1 8 | # name = 2 9 | # _another_good_name = 3 10 | # ``` 11 | # 12 | # And these are invalid variable names: 13 | # 14 | # ``` 15 | # myBadNamedVar = 1 16 | # wrong_Name = 2 17 | # ``` 18 | # 19 | # YAML configuration example: 20 | # 21 | # ``` 22 | # Naming/VariableNames: 23 | # Enabled: true 24 | # ``` 25 | class VariableNames < Base 26 | properties do 27 | since_version "0.2.0" 28 | description "Enforces variable names to be in underscored case" 29 | end 30 | 31 | MSG = "Variable name should be underscore-cased: `%s`, not `%s`" 32 | 33 | def test(source : Source) 34 | VarVisitor.new self, source 35 | end 36 | 37 | def test(source, node : Crystal::Var | Crystal::InstanceVar | Crystal::ClassVar) 38 | name = node.name.to_s 39 | 40 | return if (expected = name.underscore) == name 41 | 42 | issue_for node, MSG % {expected, name} 43 | end 44 | 45 | private class VarVisitor < AST::NodeVisitor 46 | private getter var_locations = [] of Crystal::Location 47 | 48 | def visit(node : Crystal::Var) 49 | !node.location.in?(var_locations) && super 50 | end 51 | 52 | def visit(node : Crystal::InstanceVar | Crystal::ClassVar) 53 | if location = node.location 54 | var_locations << location 55 | end 56 | super 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/any_after_filter.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of `any?` calls that follow filters. 5 | # 6 | # For example, this is considered invalid: 7 | # 8 | # ``` 9 | # [1, 2, 3].select { |e| e > 2 }.any? 10 | # [1, 2, 3].reject { |e| e >= 2 }.any? 11 | # ``` 12 | # 13 | # And it should be written as this: 14 | # 15 | # ``` 16 | # [1, 2, 3].any? { |e| e > 2 } 17 | # [1, 2, 3].any? { |e| e < 2 } 18 | # ``` 19 | # 20 | # YAML configuration example: 21 | # 22 | # ``` 23 | # Performance/AnyAfterFilter: 24 | # Enabled: true 25 | # FilterNames: 26 | # - select 27 | # - reject 28 | # ``` 29 | class AnyAfterFilter < Base 30 | include AST::Util 31 | 32 | properties do 33 | since_version "0.8.1" 34 | description "Identifies usage of `any?` calls that follow filters" 35 | filter_names %w[select reject] 36 | end 37 | 38 | MSG = "Use `any? {...}` instead of `%s {...}.any?`" 39 | 40 | def test(source, node : Crystal::Call) 41 | return unless node.name == "any?" && (obj = node.obj) 42 | return unless obj.is_a?(Crystal::Call) && obj.block && node.block.nil? 43 | return unless obj.name.in?(filter_names) 44 | 45 | issue_for name_location(obj), name_end_location(node), MSG % obj.name 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/any_instead_of_empty.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of arg-less `Enumerable#any?` calls. 5 | # 6 | # Using `Enumerable#any?` instead of `Enumerable#empty?` might lead to an 7 | # unexpected results (like `[nil, false].any? # => false`). In some cases 8 | # it also might be less efficient, since it iterates until the block will 9 | # return a _truthy_ value, instead of just checking if there's at least 10 | # one value present. 11 | # 12 | # For example, this is considered invalid: 13 | # 14 | # ``` 15 | # [1, 2, 3].any? 16 | # ``` 17 | # 18 | # And it should be written as this: 19 | # 20 | # ``` 21 | # ![1, 2, 3].empty? 22 | # ``` 23 | # 24 | # YAML configuration example: 25 | # 26 | # ``` 27 | # Performance/AnyInsteadOfEmpty: 28 | # Enabled: true 29 | # ``` 30 | class AnyInsteadOfEmpty < Base 31 | properties do 32 | since_version "0.14.0" 33 | description "Identifies usage of arg-less `any?` calls" 34 | end 35 | 36 | MSG = "Use `!{...}.empty?` instead of `{...}.any?`" 37 | 38 | def test(source, node : Crystal::Call) 39 | return unless node.name == "any?" 40 | return unless node.block.nil? && node.args.empty? 41 | return unless node.obj 42 | 43 | issue_for node, MSG, prefer_name_location: true 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/base.cr: -------------------------------------------------------------------------------- 1 | require "../base" 2 | 3 | module Ameba::Rule::Performance 4 | # A general base class for performance rules. 5 | abstract class Base < Ameba::Rule::Base 6 | def catch(source : Source) 7 | source.spec? ? source : super 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/compact_after_map.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of `compact` calls that follow `map`. 5 | # 6 | # For example, this is considered inefficient: 7 | # 8 | # ``` 9 | # %w[Alice Bob].map(&.match(/^A./)).compact 10 | # ``` 11 | # 12 | # And can be written as this: 13 | # 14 | # ``` 15 | # %w[Alice Bob].compact_map(&.match(/^A./)) 16 | # ``` 17 | # 18 | # YAML configuration example: 19 | # 20 | # ``` 21 | # Performance/CompactAfterMap: 22 | # Enabled: true 23 | # ``` 24 | class CompactAfterMap < Base 25 | include AST::Util 26 | 27 | properties do 28 | since_version "0.14.0" 29 | description "Identifies usage of `compact` calls that follow `map`" 30 | end 31 | 32 | MSG = "Use `compact_map {...}` instead of `map {...}.compact`" 33 | 34 | def test(source) 35 | AST::NodeVisitor.new self, source, skip: :macro 36 | end 37 | 38 | def test(source, node : Crystal::Call) 39 | return unless node.name == "compact" && (obj = node.obj) 40 | return unless obj.is_a?(Crystal::Call) && obj.block 41 | return unless obj.name == "map" 42 | 43 | issue_for name_location(obj), name_end_location(node), MSG 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/first_last_after_filter.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of `first/last/first?/last?` calls that follow filters. 5 | # 6 | # For example, this is considered inefficient: 7 | # 8 | # ``` 9 | # [-1, 0, 1, 2].select { |e| e > 0 }.first? 10 | # [-1, 0, 1, 2].select { |e| e > 0 }.last? 11 | # ``` 12 | # 13 | # And can be written as this: 14 | # 15 | # ``` 16 | # [-1, 0, 1, 2].find { |e| e > 0 } 17 | # [-1, 0, 1, 2].reverse_each.find { |e| e > 0 } 18 | # ``` 19 | # 20 | # YAML configuration example: 21 | # 22 | # ``` 23 | # Performance/FirstLastAfterFilter 24 | # Enabled: true 25 | # FilterNames: 26 | # - select 27 | # ``` 28 | class FirstLastAfterFilter < Base 29 | include AST::Util 30 | 31 | properties do 32 | since_version "0.8.1" 33 | description "Identifies usage of `first/last/first?/last?` calls that follow filters" 34 | filter_names %w[select] 35 | end 36 | 37 | MSG = "Use `find {...}` instead of `%s {...}.%s`" 38 | MSG_REVERSE = "Use `reverse_each.find {...}` instead of `%s {...}.%s`" 39 | 40 | CALL_NAMES = %w[first last first? last?] 41 | 42 | def test(source) 43 | AST::NodeVisitor.new self, source, skip: :macro 44 | end 45 | 46 | def test(source, node : Crystal::Call) 47 | return unless node.name.in?(CALL_NAMES) && (obj = node.obj) 48 | return unless obj.is_a?(Crystal::Call) && obj.block 49 | return unless node.block.nil? && node.args.empty? 50 | return unless obj.name.in?(filter_names) 51 | 52 | message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE 53 | 54 | issue_for name_location(obj), name_end_location(node), 55 | message % {obj.name, node.name} 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/flatten_after_map.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of `flatten` calls that follow `map`. 5 | # 6 | # For example, this is considered inefficient: 7 | # 8 | # ``` 9 | # %w[Alice Bob].map(&.chars).flatten 10 | # ``` 11 | # 12 | # And can be written as this: 13 | # 14 | # ``` 15 | # %w[Alice Bob].flat_map(&.chars) 16 | # ``` 17 | # 18 | # YAML configuration example: 19 | # 20 | # ``` 21 | # Performance/FlattenAfterMap: 22 | # Enabled: true 23 | # ``` 24 | class FlattenAfterMap < Base 25 | include AST::Util 26 | 27 | properties do 28 | since_version "0.14.0" 29 | description "Identifies usage of `flatten` calls that follow `map`" 30 | end 31 | 32 | MSG = "Use `flat_map {...}` instead of `map {...}.flatten`" 33 | 34 | def test(source) 35 | AST::NodeVisitor.new self, source, skip: :macro 36 | end 37 | 38 | def test(source, node : Crystal::Call) 39 | return unless node.name == "flatten" && (obj = node.obj) 40 | return unless obj.is_a?(Crystal::Call) && obj.block 41 | return unless obj.name == "map" 42 | 43 | issue_for name_location(obj), name_end_location(node), MSG 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/map_instead_of_block.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of `sum/product` calls 5 | # that follow `map`. 6 | # 7 | # For example, this is considered inefficient: 8 | # 9 | # ``` 10 | # (1..3).map(&.*(2)).sum 11 | # ``` 12 | # 13 | # And can be written as this: 14 | # 15 | # ``` 16 | # (1..3).sum(&.*(2)) 17 | # ``` 18 | # 19 | # YAML configuration example: 20 | # 21 | # ``` 22 | # Performance/MapInsteadOfBlock: 23 | # Enabled: true 24 | # ``` 25 | class MapInsteadOfBlock < Base 26 | include AST::Util 27 | 28 | properties do 29 | since_version "0.14.0" 30 | description "Identifies usage of `sum/product` calls that follow `map`" 31 | end 32 | 33 | MSG = "Use `%s {...}` instead of `map {...}.%s`" 34 | 35 | CALL_NAMES = %w[sum product] 36 | 37 | def test(source) 38 | AST::NodeVisitor.new self, source, skip: :macro 39 | end 40 | 41 | def test(source, node : Crystal::Call) 42 | return unless node.name.in?(CALL_NAMES) && (obj = node.obj) 43 | return unless obj.is_a?(Crystal::Call) && obj.block 44 | return unless obj.name == "map" 45 | 46 | issue_for name_location(obj), name_end_location(node), 47 | MSG % {node.name, node.name} 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/ameba/rule/performance/size_after_filter.cr: -------------------------------------------------------------------------------- 1 | require "./base" 2 | 3 | module Ameba::Rule::Performance 4 | # This rule is used to identify usage of `size` calls that follow filter. 5 | # 6 | # For example, this is considered invalid: 7 | # 8 | # ``` 9 | # [1, 2, 3].select { |e| e > 2 }.size 10 | # [1, 2, 3].reject { |e| e < 2 }.size 11 | # [1, 2, 3].select(&.< 2).size 12 | # [0, 1, 2].select(&.zero?).size 13 | # [0, 1, 2].reject(&.zero?).size 14 | # ``` 15 | # 16 | # And it should be written as this: 17 | # 18 | # ``` 19 | # [1, 2, 3].count { |e| e > 2 } 20 | # [1, 2, 3].count { |e| e >= 2 } 21 | # [1, 2, 3].count(&.< 2) 22 | # [0, 1, 2].count(&.zero?) 23 | # [0, 1, 2].count(&.!= 0) 24 | # ``` 25 | # 26 | # YAML configuration example: 27 | # 28 | # ``` 29 | # Performance/SizeAfterFilter: 30 | # Enabled: true 31 | # FilterNames: 32 | # - select 33 | # - reject 34 | # ``` 35 | class SizeAfterFilter < Base 36 | include AST::Util 37 | 38 | properties do 39 | since_version "0.8.1" 40 | description "Identifies usage of `size` calls that follow filter" 41 | filter_names %w[select reject] 42 | end 43 | 44 | MSG = "Use `count {...}` instead of `%s {...}.size`." 45 | 46 | def test(source) 47 | AST::NodeVisitor.new self, source, skip: :macro 48 | end 49 | 50 | def test(source, node : Crystal::Call) 51 | return unless node.name == "size" && (obj = node.obj) 52 | return unless obj.is_a?(Crystal::Call) && obj.block 53 | return unless obj.name.in?(filter_names) 54 | 55 | issue_for name_location(obj), name_end_location(node), MSG % obj.name 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /src/ameba/rule/style/heredoc_indent.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Style 2 | # A rule that enforces _Heredoc_ bodies be indented one level above the indentation of the 3 | # line they're used on. 4 | # 5 | # For example, this is considered invalid: 6 | # 7 | # ``` 8 | # <<-HERERDOC 9 | # hello world 10 | # HEREDOC 11 | # 12 | # <<-HERERDOC 13 | # hello world 14 | # HEREDOC 15 | # ``` 16 | # 17 | # And should be written as: 18 | # 19 | # ``` 20 | # <<-HERERDOC 21 | # hello world 22 | # HEREDOC 23 | # 24 | # <<-HERERDOC 25 | # hello world 26 | # HEREDOC 27 | # ``` 28 | # 29 | # The `IndentBy` configuration option changes the enforced indentation level of the _heredoc_. 30 | # 31 | # ``` 32 | # Style/HeredocIndent: 33 | # Enabled: true 34 | # IndentBy: 2 35 | # ``` 36 | class HeredocIndent < Base 37 | properties do 38 | since_version "1.7.0" 39 | description "Recommends heredoc bodies are indented consistently" 40 | indent_by 2 41 | end 42 | 43 | MSG = "Heredoc body should be indented by %s spaces" 44 | 45 | def test(source, node : Crystal::StringInterpolation) 46 | return unless start_location = node.location 47 | 48 | start_location_pos = source.pos(start_location) 49 | return unless source.code[start_location_pos..(start_location_pos + 2)]? == "<<-" 50 | 51 | correct_indent = line_indent(source, start_location) + indent_by 52 | 53 | unless node.heredoc_indent == correct_indent 54 | issue_for node, MSG % indent_by 55 | end 56 | end 57 | 58 | private def line_indent(source, start_location) : Int32 59 | line_location = Crystal::Location.new(nil, start_location.line_number, 1) 60 | line_location_pos = source.pos(line_location) 61 | line = source.code[line_location_pos..(line_location_pos + start_location.column_number)] 62 | 63 | line.size - line.lstrip.size 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /src/ameba/rule/style/is_a_nil.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Style 2 | # A rule that disallows calls to `is_a?(Nil)` in favor of `nil?`. 3 | # 4 | # This is considered bad: 5 | # 6 | # ``` 7 | # var.is_a?(Nil) 8 | # ``` 9 | # 10 | # And needs to be written as: 11 | # 12 | # ``` 13 | # var.nil? 14 | # ``` 15 | # 16 | # YAML configuration example: 17 | # 18 | # ``` 19 | # Style/IsANil: 20 | # Enabled: true 21 | # ``` 22 | class IsANil < Base 23 | include AST::Util 24 | 25 | properties do 26 | since_version "0.13.0" 27 | description "Disallows calls to `is_a?(Nil)` in favor of `nil?`" 28 | end 29 | 30 | MSG = "Use `nil?` instead of `is_a?(Nil)`" 31 | 32 | def test(source, node : Crystal::IsA) 33 | return if node.nil_check? 34 | 35 | const = node.const 36 | return unless path_named?(const, "Nil") 37 | 38 | issue_for const, MSG do |corrector| 39 | corrector.replace(node, "#{node.obj}.nil?") 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/ameba/rule/style/multiline_curly_block.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Style 2 | # A rule that disallows multi-line blocks that use curly brackets 3 | # instead of `do`...`end`. 4 | # 5 | # For example, this is considered invalid: 6 | # 7 | # ``` 8 | # (0..10).map { |i| 9 | # i * 2 10 | # } 11 | # ``` 12 | # 13 | # And should be rewritten to the following: 14 | # 15 | # ``` 16 | # (0..10).map do |i| 17 | # i * 2 18 | # end 19 | # ``` 20 | # 21 | # YAML configuration example: 22 | # 23 | # ``` 24 | # Style/MultilineCurlyBlock: 25 | # Enabled: true 26 | # ``` 27 | class MultilineCurlyBlock < Base 28 | include AST::Util 29 | 30 | properties do 31 | since_version "1.7.0" 32 | description "Disallows multi-line blocks using curly block syntax" 33 | end 34 | 35 | MSG = "Use `do`...`end` instead of curly brackets for multi-line blocks" 36 | 37 | def test(source, node : Crystal::Block) 38 | return unless start_location = node.location 39 | return unless end_location = node.end_location 40 | return if start_location.line_number == end_location.line_number 41 | return unless source.code[source.pos(start_location)]? == '{' 42 | 43 | issue_for node, MSG 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/ameba/rule/style/negated_conditions_in_unless.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Style 2 | # A rule that disallows negated conditions in `unless`. 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # unless !s.empty? 8 | # :ok 9 | # end 10 | # ``` 11 | # 12 | # And should be rewritten to the following: 13 | # 14 | # ``` 15 | # if s.empty? 16 | # :ok 17 | # end 18 | # ``` 19 | # 20 | # It is pretty difficult to wrap your head around a block of code 21 | # that is executed if a negated condition is NOT met. 22 | # 23 | # YAML configuration example: 24 | # 25 | # ``` 26 | # Style/NegatedConditionsInUnless: 27 | # Enabled: true 28 | # ``` 29 | class NegatedConditionsInUnless < Base 30 | properties do 31 | since_version "0.2.0" 32 | description "Disallows negated conditions in unless" 33 | end 34 | 35 | MSG = "Avoid negated conditions in unless blocks" 36 | 37 | def test(source, node : Crystal::Unless) 38 | issue_for node, MSG if negated_condition?(node.cond) 39 | end 40 | 41 | private def negated_condition?(node) 42 | case node 43 | when Crystal::BinaryOp 44 | negated_condition?(node.left) || negated_condition?(node.right) 45 | when Crystal::Expressions 46 | node.expressions.any? { |exp| negated_condition?(exp) } 47 | when Crystal::Not 48 | true 49 | else 50 | false 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /src/ameba/rule/style/while_true.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Style 2 | # A rule that disallows the use of `while true` instead of using the idiomatic `loop` 3 | # 4 | # For example, this is considered invalid: 5 | # 6 | # ``` 7 | # while true 8 | # do_something 9 | # break if some_condition 10 | # end 11 | # ``` 12 | # 13 | # And should be replaced by the following: 14 | # 15 | # ``` 16 | # loop do 17 | # do_something 18 | # break if some_condition 19 | # end 20 | # ``` 21 | # 22 | # YAML configuration example: 23 | # 24 | # ``` 25 | # Style/WhileTrue: 26 | # Enabled: true 27 | # ``` 28 | class WhileTrue < Base 29 | properties do 30 | since_version "0.3.0" 31 | description "Disallows while statements with a true literal as condition" 32 | end 33 | 34 | MSG = "While statement using `true` literal as condition" 35 | 36 | def test(source, node : Crystal::While) 37 | return unless node.cond.true_literal? 38 | 39 | return unless location = node.location 40 | return unless end_location = node.cond.end_location 41 | 42 | issue_for node, MSG do |corrector| 43 | corrector.replace(location, end_location, "loop do") 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /src/ameba/rule/typing/method_return_type_restriction.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Typing 2 | # A rule that enforces method definitions have a return type restriction. 3 | # 4 | # For example, this are considered invalid: 5 | # 6 | # ``` 7 | # def hello(name = "World") 8 | # "Hello #{name}" 9 | # end 10 | # ``` 11 | # 12 | # And this is valid: 13 | # 14 | # ``` 15 | # def hello(name = "World") : String 16 | # "Hello #{name}" 17 | # end 18 | # ``` 19 | # 20 | # When the config options `PrivateMethods` and `ProtectedMethods` 21 | # are true, this rule is also applied to private and protected methods, respectively. 22 | # 23 | # The `NodocMethods` configuration option controls whether this rule applies to 24 | # methods with a `:nodoc:` directive. 25 | # 26 | # YAML configuration example: 27 | # 28 | # ``` 29 | # Typing/MethodReturnTypeRestriction: 30 | # Enabled: true 31 | # PrivateMethods: false 32 | # ProtectedMethods: false 33 | # NodocMethods: false 34 | # ``` 35 | class MethodReturnTypeRestriction < Base 36 | include AST::Util 37 | 38 | properties do 39 | since_version "1.7.0" 40 | description "Recommends that methods have a return type restriction" 41 | enabled false 42 | private_methods false 43 | protected_methods false 44 | nodoc_methods false 45 | end 46 | 47 | MSG = "Method should have a return type restriction" 48 | 49 | def test(source, node : Crystal::Def) 50 | issue_for node, MSG unless valid_return_type?(node) 51 | end 52 | 53 | private def valid_return_type?(node : Crystal::ASTNode) : Bool 54 | !!node.return_type || 55 | (node.visibility.private? && !private_methods?) || 56 | (node.visibility.protected? && !protected_methods?) || 57 | (!nodoc_methods? && nodoc?(node)) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /src/ameba/rule/typing/proc_literal_return_type_restriction.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Rule::Typing 2 | # A rule that enforces that `Proc` literals have a return type. 3 | # 4 | # For example, these are considered invalid: 5 | # 6 | # ``` 7 | # greeter = ->(name : String) { "Hello #{name}" } 8 | # ``` 9 | # 10 | # ``` 11 | # task = -> { Task.new("execute this command") } 12 | # ``` 13 | # 14 | # And these are valid: 15 | # 16 | # ``` 17 | # greeter = ->(name : String) : String { "Hello #{name}" } 18 | # ``` 19 | # 20 | # ``` 21 | # task = -> : Task { Task.new("execute this command") } 22 | # ``` 23 | # 24 | # YAML configuration example: 25 | # 26 | # ``` 27 | # Typing/ProcLiteralReturnTypeRestriction: 28 | # Enabled: true 29 | # ``` 30 | class ProcLiteralReturnTypeRestriction < Base 31 | properties do 32 | since_version "1.7.0" 33 | description "Disallows proc literals without return type restriction" 34 | enabled false 35 | end 36 | 37 | MSG = "Proc literal should have a return type restriction" 38 | 39 | def test(source, node : Crystal::ProcLiteral) 40 | issue_for node, MSG unless node.def.return_type 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /src/ameba/spec/be_valid.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Spec 2 | module BeValid 3 | def be_valid 4 | BeValidExpectation.new 5 | end 6 | end 7 | 8 | struct BeValidExpectation 9 | def match(source) 10 | source.valid? 11 | end 12 | 13 | def failure_message(source) 14 | String.build do |str| 15 | str << "Source expected to be valid, but there are issues: \n\n" 16 | source.issues.reject(&.disabled?).each do |issue| 17 | str << " * #{issue.rule.name}: #{issue.message}\n" 18 | end 19 | end 20 | end 21 | 22 | def negative_failure_message(source) 23 | "Source expected to be invalid, but it is valid." 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/ameba/spec/support.cr: -------------------------------------------------------------------------------- 1 | # Require this file to load code that supports testing Ameba rules. 2 | 3 | require "./be_valid" 4 | require "./expect_issue" 5 | require "./util" 6 | 7 | module Ameba 8 | class Source 9 | include Spec::Util 10 | 11 | def initialize(code : String, @path = "", normalize = true) 12 | @code = normalize ? normalize_code(code) : code 13 | end 14 | end 15 | end 16 | 17 | include Ameba::Spec::BeValid 18 | include Ameba::Spec::ExpectIssue 19 | -------------------------------------------------------------------------------- /src/ameba/spec/util.cr: -------------------------------------------------------------------------------- 1 | module Ameba::Spec::Util 2 | def normalize_code(code, separator = '\n') 3 | lines = code.split(separator) 4 | 5 | # remove unneeded first blank lines if any 6 | lines.shift if lines[0].blank? && lines.size > 1 7 | 8 | # find the minimum indentation 9 | min_indent = lines.min_of do |line| 10 | line.blank? ? code.size : line.size - line.lstrip.size 11 | end 12 | 13 | # remove the width of minimum indentation in each line 14 | lines.join(separator) do |line| 15 | line.blank? ? line : line[min_indent..] 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/cli.cr: -------------------------------------------------------------------------------- 1 | require "./ameba/cli/cmd" 2 | 3 | Ameba::Cli.run 4 | -------------------------------------------------------------------------------- /src/contrib/read_type_doc.cr: -------------------------------------------------------------------------------- 1 | require "compiler/crystal/syntax/*" 2 | 3 | private class DocFinder < Crystal::Visitor 4 | getter type_name : String 5 | getter doc : String? 6 | 7 | def initialize(nodes, @type_name) 8 | accept(nodes) 9 | end 10 | 11 | def visit(node : Crystal::ASTNode) 12 | return false if @doc 13 | 14 | if node.responds_to?(:name) && (name = node.name).is_a?(Crystal::Path) 15 | @doc = node.doc if name.names.last? == @type_name 16 | end 17 | 18 | true 19 | end 20 | end 21 | 22 | type_name, path_to_source_file = ARGV 23 | 24 | source = File.read(path_to_source_file) 25 | nodes = Crystal::Parser.new(source) 26 | .tap(&.wants_doc = true) 27 | .parse 28 | 29 | puts DocFinder.new(nodes, type_name).doc 30 | --------------------------------------------------------------------------------