Repository: veelenga/ameba
Branch: master
Commit: 77bf37bb5d3f
Files: 362
Total size: 1.0 MB
Directory structure:
gitextract_e8xowmtu/
├── .ameba.yml
├── .ameba.yml.schema.json
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── add-docs-version-to-json-index.yml
│ ├── build-and-deploy-docs.yml
│ ├── ci.yml
│ ├── compare-json-schema.yml
│ ├── docker-image.yml
│ ├── docs.yml
│ ├── release.yml
│ ├── typos.yml
│ └── update-json-schema.yml
├── .gitignore
├── .typos.toml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── bench/
│ └── check_sources.cr
├── bin/
│ └── ameba.cr
├── shard.yml
├── spec/
│ ├── ameba/
│ │ ├── ast/
│ │ │ ├── flow_expression_spec.cr
│ │ │ ├── liveness_analyzer_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
│ │ │ ├── elseif_aware_node_visitor_spec.cr
│ │ │ ├── flow_expression_visitor_spec.cr
│ │ │ ├── implicit_return_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/
│ │ │ │ ├── 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
│ │ │ │ ├── assignment_in_call_argument_spec.cr
│ │ │ │ ├── bad_directive_spec.cr
│ │ │ │ ├── comparison_to_boolean_spec.cr
│ │ │ │ ├── debug_calls_spec.cr
│ │ │ │ ├── debugger_statement_spec.cr
│ │ │ │ ├── duplicate_branch_spec.cr
│ │ │ │ ├── duplicate_enum_value_spec.cr
│ │ │ │ ├── duplicate_method_signature_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
│ │ │ │ ├── enum_member_name_conflict_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
│ │ │ │ ├── non_existent_rule_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
│ │ │ │ ├── self_initialize_definition_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_expression_spec.cr
│ │ │ │ ├── unused_rescue_variable_spec.cr
│ │ │ │ ├── useless_assign_spec.cr
│ │ │ │ ├── useless_condition_in_when_spec.cr
│ │ │ │ ├── useless_visibility_modifier_spec.cr
│ │ │ │ ├── void_outside_lib_spec.cr
│ │ │ │ └── whitespace_around_macro_expression_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_present_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
│ │ │ │ └── times_map_spec.cr
│ │ │ ├── style/
│ │ │ │ ├── array_literal_syntax_spec.cr
│ │ │ │ ├── call_parentheses_spec.cr
│ │ │ │ ├── elsif_spec.cr
│ │ │ │ ├── guard_clause_spec.cr
│ │ │ │ ├── hash_literal_syntax_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
│ │ │ │ ├── multiline_string_literal_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_nil_in_control_expression_spec.cr
│ │ │ │ ├── redundant_return_spec.cr
│ │ │ │ ├── redundant_self_spec.cr
│ │ │ │ ├── unless_else_spec.cr
│ │ │ │ ├── verbose_block_spec.cr
│ │ │ │ ├── verbose_nil_type_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
│ │ └── version_spec.cr
│ ├── ameba_spec.cr
│ ├── fixtures/
│ │ └── .ameba.yml
│ └── spec_helper.cr
└── src/
├── ameba/
│ ├── ast/
│ │ ├── flow_expression.cr
│ │ ├── liveness_analyzer.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
│ │ ├── elseif_aware_node_visitor.cr
│ │ ├── flow_expression_visitor.cr
│ │ ├── implicit_return_visitor.cr
│ │ ├── macro_reference_finder.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/
│ │ ├── loader.cr
│ │ └── rule_config.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
│ ├── json_schema/
│ │ └── builder.cr
│ ├── presenter/
│ │ ├── base_presenter.cr
│ │ ├── rule_collection_presenter.cr
│ │ ├── rule_presenter.cr
│ │ └── rule_versions_presenter.cr
│ ├── reportable.cr
│ ├── rule/
│ │ ├── base.cr
│ │ ├── documentation/
│ │ │ ├── admonition.cr
│ │ │ └── documentation.cr
│ │ ├── layout/
│ │ │ ├── line_length.cr
│ │ │ ├── trailing_blank_lines.cr
│ │ │ └── trailing_whitespace.cr
│ │ ├── lint/
│ │ │ ├── ambiguous_assignment.cr
│ │ │ ├── assignment_in_call_argument.cr
│ │ │ ├── bad_directive.cr
│ │ │ ├── comparison_to_boolean.cr
│ │ │ ├── debug_calls.cr
│ │ │ ├── debugger_statement.cr
│ │ │ ├── duplicate_branch.cr
│ │ │ ├── duplicate_enum_value.cr
│ │ │ ├── duplicate_method_signature.cr
│ │ │ ├── duplicate_when_condition.cr
│ │ │ ├── duplicated_require.cr
│ │ │ ├── else_nil.cr
│ │ │ ├── empty_ensure.cr
│ │ │ ├── empty_expression.cr
│ │ │ ├── empty_loop.cr
│ │ │ ├── enum_member_name_conflict.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
│ │ │ ├── non_existent_rule.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
│ │ │ ├── self_initialize_definition.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_expression.cr
│ │ │ ├── unused_rescue_variable.cr
│ │ │ ├── useless_assign.cr
│ │ │ ├── useless_condition_in_when.cr
│ │ │ ├── useless_visibility_modifier.cr
│ │ │ ├── void_outside_lib.cr
│ │ │ └── whitespace_around_macro_expression.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_present.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
│ │ │ └── times_map.cr
│ │ ├── style/
│ │ │ ├── array_literal_syntax.cr
│ │ │ ├── call_parentheses.cr
│ │ │ ├── elsif.cr
│ │ │ ├── guard_clause.cr
│ │ │ ├── hash_literal_syntax.cr
│ │ │ ├── heredoc_escape.cr
│ │ │ ├── heredoc_indent.cr
│ │ │ ├── is_a_filter.cr
│ │ │ ├── is_a_nil.cr
│ │ │ ├── large_numbers.cr
│ │ │ ├── multiline_curly_block.cr
│ │ │ ├── multiline_string_literal.cr
│ │ │ ├── negated_conditions_in_unless.cr
│ │ │ ├── parentheses_around_condition.cr
│ │ │ ├── percent_literal_delimiters.cr
│ │ │ ├── redundant_begin.cr
│ │ │ ├── redundant_next.cr
│ │ │ ├── redundant_nil_in_control_expression.cr
│ │ │ ├── redundant_return.cr
│ │ │ ├── redundant_self.cr
│ │ │ ├── unless_else.cr
│ │ │ ├── verbose_block.cr
│ │ │ ├── verbose_nil_type.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/
│ │ ├── corrector.cr
│ │ ├── rewriter/
│ │ │ └── action.cr
│ │ └── rewriter.cr
│ ├── source.cr
│ ├── spec/
│ │ ├── annotated_source.cr
│ │ ├── be_valid.cr
│ │ ├── expect_issue.cr
│ │ ├── support.cr
│ │ └── util.cr
│ ├── tokenizer.cr
│ └── version.cr
├── ameba.cr
├── cli.cr
├── contrib/
│ └── read_type_doc.cr
└── json-schema-builder.cr
================================================
FILE CONTENTS
================================================
================================================
FILE: .ameba.yml
================================================
Lint/Typos:
Enabled: true
Excluded:
- spec/ameba/rule/lint/typos_spec.cr
Style/Elsif:
Enabled: true
================================================
FILE: .ameba.yml.schema.json
================================================
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://crystal-ameba.github.io/.ameba.yml.schema.json",
"title": ".ameba.yml",
"description": "Configuration rules for the Crystal language Ameba linter",
"type": "object",
"additionalProperties": false,
"$defs": {
"Severity": {
"type": "string",
"enum": [
"Error",
"Warning",
"Convention"
]
},
"Globs": {
"type": "array",
"title": "Globbed files and paths",
"description": "An array of wildcards (or paths) to include to the inspection",
"items": {
"type": "string",
"examples": [
"src/**/*.{cr,ecr}",
"!lib"
]
}
},
"Excluded": {
"type": "array",
"title": "Excluded files and paths",
"description": "An array of wildcards (or paths) to exclude from the source list",
"items": {
"type": "string",
"examples": [
"spec/fixtures/**",
"spec/**/*.manual_spec.cr"
]
}
},
"BaseRule": {
"type": "object",
"properties": {
"SinceVersion": {
"type": "string"
},
"Enabled": {
"type": "boolean",
"default": true
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Convention"
},
"Excluded": {
"$ref": "#/$defs/Excluded"
}
}
}
},
"properties": {
"Version": {
"type": "string",
"description": "The version of Ameba to limit rules to",
"examples": [
"1.7.0",
"1.6.4"
]
},
"Formatter": {
"type": "object",
"description": "The formatter to use for Ameba",
"properties": {
"Name": {
"type": "string",
"enum": [
"progress",
"todo",
"flycheck",
"silent",
"disabled",
"json",
"github-actions"
]
}
}
},
"Globs": {
"$ref": "#/$defs/Globs"
},
"Excluded": {
"$ref": "#/$defs/Excluded"
},
"Documentation/Admonition": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Documentation/Admonition.html",
"title": "Documentation/Admonition",
"description": "Reports documentation admonitions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
},
"Admonitions": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"TODO",
"FIXME",
"BUG"
]
},
"Timezone": {
"type": "string",
"default": "UTC"
}
}
},
"Documentation/Documentation": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Documentation/Documentation.html",
"title": "Documentation/Documentation",
"description": "Enforces public types to be documented",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.5.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"IgnoreClasses": {
"type": "boolean",
"default": false
},
"IgnoreModules": {
"type": "boolean",
"default": true
},
"IgnoreEnums": {
"type": "boolean",
"default": false
},
"IgnoreDefs": {
"type": "boolean",
"default": true
},
"IgnoreMacros": {
"type": "boolean",
"default": false
},
"IgnoreMacroHooks": {
"type": "boolean",
"default": true
},
"RequireExample": {
"type": "boolean",
"default": false
}
}
},
"Layout/LineLength": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Layout/LineLength.html",
"title": "Layout/LineLength",
"description": "Disallows lines longer than `MaxLength` number of symbols",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"MaxLength": {
"type": "number",
"default": 140
}
}
},
"Layout/TrailingBlankLines": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Layout/TrailingBlankLines.html",
"title": "Layout/TrailingBlankLines",
"description": "Disallows trailing blank lines",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
}
}
},
"Layout/TrailingWhitespace": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Layout/TrailingWhitespace.html",
"title": "Layout/TrailingWhitespace",
"description": "Disallows trailing whitespace",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
}
}
},
"Lint/AmbiguousAssignment": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/AmbiguousAssignment.html",
"title": "Lint/AmbiguousAssignment",
"description": "Disallows ambiguous `=-/=+/=!`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.0.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/AssignmentInCallArgument": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/AssignmentInCallArgument.html",
"title": "Lint/AssignmentInCallArgument",
"description": "Disallows variable assignment in call arguments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/BadDirective": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/BadDirective.html",
"title": "Lint/BadDirective",
"description": "Reports bad comment directives",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.13.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/ComparisonToBoolean": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/ComparisonToBoolean.html",
"title": "Lint/ComparisonToBoolean",
"description": "Disallows comparison to booleans",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DebugCalls": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DebugCalls.html",
"title": "Lint/DebugCalls",
"description": "Disallows debug-related calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.0.0"
},
"MethodNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"p",
"p!",
"pp",
"pp!"
]
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DebuggerStatement": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DebuggerStatement.html",
"title": "Lint/DebuggerStatement",
"description": "Disallows calls to `debugger`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DuplicateBranch": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DuplicateBranch.html",
"title": "Lint/DuplicateBranch",
"description": "Reports duplicated branch bodies",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"IgnoreLiteralBranches": {
"type": "boolean",
"default": false
},
"IgnoreConstantBranches": {
"type": "boolean",
"default": false
},
"IgnoreDuplicateElseBranch": {
"type": "boolean",
"default": false
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DuplicateEnumValue": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DuplicateEnumValue.html",
"title": "Lint/DuplicateEnumValue",
"description": "Reports duplicated `enum` member values",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DuplicateMethodSignature": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DuplicateMethodSignature.html",
"title": "Lint/DuplicateMethodSignature",
"description": "Reports repeated method signatures",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DuplicateWhenCondition": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DuplicateWhenCondition.html",
"title": "Lint/DuplicateWhenCondition",
"description": "Reports repeated conditions used in case `when` expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/DuplicatedRequire": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/DuplicatedRequire.html",
"title": "Lint/DuplicatedRequire",
"description": "Reports duplicated `require` statements",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/ElseNil": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/ElseNil.html",
"title": "Lint/ElseNil",
"description": "Disallows `else` blocks with `nil` as their body",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/EmptyEnsure": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/EmptyEnsure.html",
"title": "Lint/EmptyEnsure",
"description": "Disallows empty `ensure` statement",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/EmptyExpression": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/EmptyExpression.html",
"title": "Lint/EmptyExpression",
"description": "Disallows empty expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/EmptyLoop": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/EmptyLoop.html",
"title": "Lint/EmptyLoop",
"description": "Disallows empty loops",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.12.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/EnumMemberNameConflict": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/EnumMemberNameConflict.html",
"title": "Lint/EnumMemberNameConflict",
"description": "Reports conflicting enum member names",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/Formatting": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/Formatting.html",
"title": "Lint/Formatting",
"description": "Reports not formatted sources",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.4.0"
},
"FailOnError": {
"type": "boolean",
"default": false
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/HashDuplicatedKey": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/HashDuplicatedKey.html",
"title": "Lint/HashDuplicatedKey",
"description": "Disallows duplicated keys in hash literals",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/LiteralAssignmentsInExpressions": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/LiteralAssignmentsInExpressions.html",
"title": "Lint/LiteralAssignmentsInExpressions",
"description": "Disallows assignments with literal values in control expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.4.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/LiteralInCondition": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/LiteralInCondition.html",
"title": "Lint/LiteralInCondition",
"description": "Disallows useless conditional statements that contain a literal in place of a variable or predicate function",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/LiteralInInterpolation": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/LiteralInInterpolation.html",
"title": "Lint/LiteralInInterpolation",
"description": "Disallows useless string interpolations",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/LiteralsComparison": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/LiteralsComparison.html",
"title": "Lint/LiteralsComparison",
"description": "Identifies comparisons between literals",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/MissingBlockArgument": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/MissingBlockArgument.html",
"title": "Lint/MissingBlockArgument",
"description": "Disallows yielding method definitions without block argument",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.4.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/NonExistentRule": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/NonExistentRule.html",
"title": "Lint/NonExistentRule",
"description": "Reports non-existent rules in comment directives",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.13.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/NotNil": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/NotNil.html",
"title": "Lint/NotNil",
"description": "Identifies usage of `not_nil!` calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/NotNilAfterNoBang": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/NotNilAfterNoBang.html",
"title": "Lint/NotNilAfterNoBang",
"description": "Identifies usage of `index/rindex/find/match` calls followed by `not_nil!`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/PercentArrays": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/PercentArrays.html",
"title": "Lint/PercentArrays",
"description": "Disallows some unwanted symbols in percent array literals",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
},
"StringArrayUnwantedSymbols": {
"type": "string",
"default": ",\""
},
"SymbolArrayUnwantedSymbols": {
"type": "string",
"default": ",:"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/RandZero": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/RandZero.html",
"title": "Lint/RandZero",
"description": "Disallows `rand` zero calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.5.1"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/RedundantStringCoercion": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/RedundantStringCoercion.html",
"title": "Lint/RedundantStringCoercion",
"description": "Disallows redundant string conversions in interpolation",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.12.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/RedundantWithIndex": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/RedundantWithIndex.html",
"title": "Lint/RedundantWithIndex",
"description": "Disallows redundant `with_index` calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.11.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/RedundantWithObject": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/RedundantWithObject.html",
"title": "Lint/RedundantWithObject",
"description": "Disallows redundant `with_object` calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.11.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/RequireParentheses": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/RequireParentheses.html",
"title": "Lint/RequireParentheses",
"description": "Disallows method calls with no parentheses and a logical operator in the argument list",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/SelfInitializeDefinition": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/SelfInitializeDefinition.html",
"title": "Lint/SelfInitializeDefinition",
"description": "Reports `initialize` method definitions with a `self` receiver",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/ShadowedArgument": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/ShadowedArgument.html",
"title": "Lint/ShadowedArgument",
"description": "Disallows shadowed arguments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/ShadowedException": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/ShadowedException.html",
"title": "Lint/ShadowedException",
"description": "Disallows rescued exception that get shadowed",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/ShadowingOuterLocalVar": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/ShadowingOuterLocalVar.html",
"title": "Lint/ShadowingOuterLocalVar",
"description": "Disallows the usage of the same name as outer local variables for block or proc arguments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/SharedVarInFiber": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/SharedVarInFiber.html",
"title": "Lint/SharedVarInFiber",
"description": "Disallows shared variables in fibers",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.12.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/SignalTrap": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/SignalTrap.html",
"title": "Lint/SignalTrap",
"description": "Disallows `Signal::INT/HUP/TERM.trap` in favor of `Process.on_terminate`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/SpecEqWithBoolOrNilLiteral": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/SpecEqWithBoolOrNilLiteral.html",
"title": "Lint/SpecEqWithBoolOrNilLiteral",
"description": "Reports `eq(true|false|nil)` expectations in specs",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/SpecFilename": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/SpecFilename.html",
"title": "Lint/SpecFilename",
"description": "Enforces spec filenames to have `_spec` suffix",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"IgnoredDirs": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"spec/support",
"spec/fixtures",
"spec/data"
]
},
"IgnoredFilenames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"spec_helper"
]
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/SpecFocus": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/SpecFocus.html",
"title": "Lint/SpecFocus",
"description": "Reports focused spec items",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/Syntax": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/Syntax.html",
"title": "Lint/Syntax",
"description": "Reports invalid Crystal syntax",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.4.2"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Error"
}
}
},
"Lint/TopLevelOperatorDefinition": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/TopLevelOperatorDefinition.html",
"title": "Lint/TopLevelOperatorDefinition",
"description": "Disallows top level operator method definitions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/TrailingRescueException": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/TrailingRescueException.html",
"title": "Lint/TrailingRescueException",
"description": "Disallows trailing `rescue` with a path",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/Typos": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/Typos.html",
"title": "Lint/Typos",
"description": "Reports typos found in source files",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"BinPath": {
"type": [
"string",
"null"
],
"default": null
},
"FailOnMissingBin": {
"type": "boolean",
"default": false
},
"FailOnError": {
"type": "boolean",
"default": true
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UnneededDisableDirective": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UnneededDisableDirective.html",
"title": "Lint/UnneededDisableDirective",
"description": "Reports unneeded disable directives in comments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.5.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UnreachableCode": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UnreachableCode.html",
"title": "Lint/UnreachableCode",
"description": "Reports unreachable code",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.9.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UnusedArgument": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UnusedArgument.html",
"title": "Lint/UnusedArgument",
"description": "Disallows unused arguments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.6.0"
},
"IgnoreDefs": {
"type": "boolean",
"default": true
},
"IgnoreBlocks": {
"type": "boolean",
"default": false
},
"IgnoreProcs": {
"type": "boolean",
"default": false
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UnusedBlockArgument": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UnusedBlockArgument.html",
"title": "Lint/UnusedBlockArgument",
"description": "Disallows unused block arguments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.4.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UnusedExpression": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UnusedExpression.html",
"title": "Lint/UnusedExpression",
"description": "Disallows unused expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UnusedRescueVariable": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UnusedRescueVariable.html",
"title": "Lint/UnusedRescueVariable",
"description": "Disallows unused `rescue` variables",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UselessAssign": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UselessAssign.html",
"title": "Lint/UselessAssign",
"description": "Disallows useless variable assignments",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.6.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UselessConditionInWhen": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UselessConditionInWhen.html",
"title": "Lint/UselessConditionInWhen",
"description": "Disallows useless conditions in `when`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/UselessVisibilityModifier": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/UselessVisibilityModifier.html",
"title": "Lint/UselessVisibilityModifier",
"description": "Disallows top level `protected` method visibility modifier",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/VoidOutsideLib": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/VoidOutsideLib.html",
"title": "Lint/VoidOutsideLib",
"description": "Disallows use of `Void` outside C lib bindings and `Pointer(Void)`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Lint/WhitespaceAroundMacroExpression": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/WhitespaceAroundMacroExpression.html",
"title": "Lint/WhitespaceAroundMacroExpression",
"description": "Reports missing spaces around macro expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Metrics/CyclomaticComplexity": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Metrics/CyclomaticComplexity.html",
"title": "Metrics/CyclomaticComplexity",
"description": "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.9.1"
},
"MaxComplexity": {
"type": "number",
"default": 12
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Naming/AccessorMethodName": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/AccessorMethodName.html",
"title": "Naming/AccessorMethodName",
"description": "Makes sure that accessor methods are named properly",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
}
}
},
"Naming/AsciiIdentifiers": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/AsciiIdentifiers.html",
"title": "Naming/AsciiIdentifiers",
"description": "Disallows non-ascii characters in identifiers",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"IgnoreSymbols": {
"type": "boolean",
"default": false
}
}
},
"Naming/BinaryOperatorParameterName": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/BinaryOperatorParameterName.html",
"title": "Naming/BinaryOperatorParameterName",
"description": "Enforces that certain binary operator methods have their sole parameter name standardized",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"ExcludedOperators": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"[]",
"[]?",
"[]=",
"<<",
">>",
"`",
"=~",
"!~"
]
},
"AllowedNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"other"
]
}
}
},
"Naming/BlockParameterName": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/BlockParameterName.html",
"title": "Naming/BlockParameterName",
"description": "Disallows non-descriptive block parameter names",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"MinNameLength": {
"type": "number",
"default": 3
},
"AllowNamesEndingInNumbers": {
"type": "boolean",
"default": true
},
"AllowedNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"a",
"b",
"e",
"i",
"j",
"k",
"v",
"x",
"y",
"k1",
"k2",
"v1",
"v2",
"db",
"ex",
"id",
"io",
"ip",
"op",
"tx",
"wg",
"ws"
]
},
"ForbiddenNames": {
"type": "array",
"items": {
"type": "string"
},
"default": []
}
}
},
"Naming/ConstantNames": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/ConstantNames.html",
"title": "Naming/ConstantNames",
"description": "Enforces constant names to be in screaming case",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
}
}
},
"Naming/Filename": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/Filename.html",
"title": "Naming/Filename",
"description": "Enforces file names to be in underscored case",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
}
}
},
"Naming/MethodNames": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/MethodNames.html",
"title": "Naming/MethodNames",
"description": "Enforces method names to be in underscored case",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
}
}
},
"Naming/PredicateName": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/PredicateName.html",
"title": "Naming/PredicateName",
"description": "Disallows tautological predicate names",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
}
}
},
"Naming/QueryBoolMethods": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/QueryBoolMethods.html",
"title": "Naming/QueryBoolMethods",
"description": "Reports boolean properties without the `?` suffix",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.4.0"
}
}
},
"Naming/RescuedExceptionsVariableName": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/RescuedExceptionsVariableName.html",
"title": "Naming/RescuedExceptionsVariableName",
"description": "Makes sure that rescued exceptions variables are named as expected",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.6.0"
},
"AllowedNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"e",
"ex",
"exception",
"err",
"error"
]
}
}
},
"Naming/TypeNames": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/TypeNames.html",
"title": "Naming/TypeNames",
"description": "Enforces type names in camelcase manner",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
}
}
},
"Naming/VariableNames": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Naming/VariableNames.html",
"title": "Naming/VariableNames",
"description": "Enforces variable names to be in underscored case",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
}
}
},
"Performance/AnyAfterFilter": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/AnyAfterFilter.html",
"title": "Performance/AnyAfterFilter",
"description": "Identifies usage of `any?` calls that follow filters",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.8.1"
},
"FilterNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"select",
"reject"
]
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/AnyInsteadOfPresent": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/AnyInsteadOfPresent.html",
"title": "Performance/AnyInsteadOfPresent",
"description": "Identifies usage of arg-less `any?` calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/ChainedCallWithNoBang": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/ChainedCallWithNoBang.html",
"title": "Performance/ChainedCallWithNoBang",
"description": "Identifies usage of chained calls not utilizing the bang method variants",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"CallNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"uniq",
"unstable_sort",
"sort",
"sort_by",
"shuffle",
"reverse"
]
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/CompactAfterMap": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/CompactAfterMap.html",
"title": "Performance/CompactAfterMap",
"description": "Identifies usage of `compact` calls that follow `map`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/ExcessiveAllocations": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/ExcessiveAllocations.html",
"title": "Performance/ExcessiveAllocations",
"description": "Identifies usage of excessive collection allocations",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.5.0"
},
"CallNames": {
"type": "object",
"properties": {
"codepoints": {
"type": "string",
"default": "each_codepoint"
},
"graphemes": {
"type": "string",
"default": "each_grapheme"
},
"chars": {
"type": "string",
"default": "each_char"
},
"lines": {
"type": "string",
"default": "each_line"
}
}
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/FirstLastAfterFilter": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/FirstLastAfterFilter.html",
"title": "Performance/FirstLastAfterFilter",
"description": "Identifies usage of `first/last/first?/last?` calls that follow filters",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.8.1"
},
"FilterNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"select"
]
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/FlattenAfterMap": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/FlattenAfterMap.html",
"title": "Performance/FlattenAfterMap",
"description": "Identifies usage of `flatten` calls that follow `map`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/MapInsteadOfBlock": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/MapInsteadOfBlock.html",
"title": "Performance/MapInsteadOfBlock",
"description": "Identifies usage of `sum/product` calls that follow `map`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/MinMaxAfterMap": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/MinMaxAfterMap.html",
"title": "Performance/MinMaxAfterMap",
"description": "Identifies usage of `min/max/minmax` calls that follow `map`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.5.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/SizeAfterFilter": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/SizeAfterFilter.html",
"title": "Performance/SizeAfterFilter",
"description": "Identifies usage of `size` calls that follow filter",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.8.1"
},
"FilterNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"select",
"reject"
]
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Performance/TimesMap": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Performance/TimesMap.html",
"title": "Performance/TimesMap",
"description": "Identifies usage of `times.map { ... }.to_a` calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Severity": {
"$ref": "#/$defs/Severity",
"default": "Warning"
}
}
},
"Style/ArrayLiteralSyntax": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/ArrayLiteralSyntax.html",
"title": "Style/ArrayLiteralSyntax",
"description": "Encourages the use of `Array(T).new` over `[] of T`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
}
}
},
"Style/CallParentheses": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/CallParentheses.html",
"title": "Style/CallParentheses",
"description": "Enforces usage of parentheses in method calls",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"ExcludeTypeDeclarations": {
"type": "boolean",
"default": true
},
"ExcludeHeredocs": {
"type": "boolean",
"default": false
},
"ExcludedToplevelCallNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"spawn",
"raise",
"super",
"previous_def",
"exit",
"abort",
"sleep",
"print",
"printf",
"puts",
"p",
"p!",
"pp",
"pp!",
"record",
"class_getter",
"class_getter?",
"class_getter!",
"class_property",
"class_property?",
"class_property!",
"class_setter",
"getter",
"getter?",
"getter!",
"property",
"property?",
"property!",
"setter",
"def_equals_and_hash",
"def_equals",
"def_hash",
"delegate",
"forward_missing_to",
"describe",
"context",
"it",
"pending",
"fail",
"use_json_discriminator"
]
},
"ExcludedCallNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"should",
"should_not"
]
}
}
},
"Style/Elsif": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/Elsif.html",
"title": "Style/Elsif",
"description": "Encourages the use of `case/when` syntax over `if/elsif`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"IgnoreSuffix": {
"type": "boolean",
"default": true
},
"MaxBranches": {
"type": "number",
"default": 0
}
}
},
"Style/GuardClause": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/GuardClause.html",
"title": "Style/GuardClause",
"description": "Check for conditionals that can be replaced with guard clauses",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.0.0"
},
"Enabled": {
"type": "boolean",
"default": false
}
}
},
"Style/HashLiteralSyntax": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/HashLiteralSyntax.html",
"title": "Style/HashLiteralSyntax",
"description": "Encourages the use of `Hash(K, V).new` over `{} of K => V`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
}
}
},
"Style/HeredocEscape": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/HeredocEscape.html",
"title": "Style/HeredocEscape",
"description": "Recommends using the heredoc variant that escapes interpolation or control chars in a heredoc body",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
}
}
},
"Style/HeredocIndent": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/HeredocIndent.html",
"title": "Style/HeredocIndent",
"description": "Recommends heredoc bodies are indented consistently",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"IndentBy": {
"type": "number",
"default": 2
},
"BodyAutoDedent": {
"type": "boolean",
"default": true
}
}
},
"Style/IsAFilter": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/IsAFilter.html",
"title": "Style/IsAFilter",
"description": "Identifies usage of `is_a?/nil?` calls within filters",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"FilterNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"select",
"reject",
"any?",
"all?",
"none?",
"one?"
]
}
}
},
"Style/IsANil": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/IsANil.html",
"title": "Style/IsANil",
"description": "Disallows calls to `is_a?(Nil)` in favor of `nil?`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.13.0"
}
}
},
"Style/LargeNumbers": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/LargeNumbers.html",
"title": "Style/LargeNumbers",
"description": "Disallows usage of large numbers without underscore",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"IntMinDigits": {
"type": "number",
"default": 6
}
}
},
"Style/MultilineCurlyBlock": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/MultilineCurlyBlock.html",
"title": "Style/MultilineCurlyBlock",
"description": "Disallows multi-line blocks using curly block syntax",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
}
}
},
"Style/MultilineStringLiteral": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/MultilineStringLiteral.html",
"title": "Style/MultilineStringLiteral",
"description": "Disallows multiline string literals not using `<<-HEREDOC` markers",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"AllowBackslashSplitStrings": {
"type": "boolean",
"default": true
}
}
},
"Style/NegatedConditionsInUnless": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/NegatedConditionsInUnless.html",
"title": "Style/NegatedConditionsInUnless",
"description": "Disallows negated conditions in `unless`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.2.0"
}
}
},
"Style/ParenthesesAroundCondition": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/ParenthesesAroundCondition.html",
"title": "Style/ParenthesesAroundCondition",
"description": "Disallows redundant parentheses around control expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.4.0"
},
"ExcludeTernary": {
"type": "boolean",
"default": false
},
"ExcludeMultiline": {
"type": "boolean",
"default": false
},
"AllowSafeAssignment": {
"type": "boolean",
"default": false
}
}
},
"Style/PercentLiteralDelimiters": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/PercentLiteralDelimiters.html",
"title": "Style/PercentLiteralDelimiters",
"description": "Enforces the consistent usage of `%`-literal delimiters",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"DefaultDelimiters": {
"type": [
"string",
"null"
],
"default": "()"
},
"PreferredDelimiters": {
"type": "object",
"properties": {
"%w": {
"type": "string",
"default": "[]"
},
"%i": {
"type": "string",
"default": "[]"
},
"%r": {
"type": "string",
"default": "{}"
}
}
},
"IgnoreLiteralsContainingDelimiters": {
"type": "boolean",
"default": false
}
}
},
"Style/RedundantBegin": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/RedundantBegin.html",
"title": "Style/RedundantBegin",
"description": "Disallows redundant `begin` blocks",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
}
}
},
"Style/RedundantNext": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/RedundantNext.html",
"title": "Style/RedundantNext",
"description": "Reports redundant `next` expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.12.0"
},
"AllowMultiNext": {
"type": "boolean",
"default": true
},
"AllowEmptyNext": {
"type": "boolean",
"default": true
}
}
},
"Style/RedundantNilInControlExpression": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/RedundantNilInControlExpression.html",
"title": "Style/RedundantNilInControlExpression",
"description": "Disallows control expressions with `nil` argument",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
}
}
},
"Style/RedundantReturn": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/RedundantReturn.html",
"title": "Style/RedundantReturn",
"description": "Reports redundant `return` expressions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.9.0"
},
"AllowMultiReturn": {
"type": "boolean",
"default": true
},
"AllowEmptyReturn": {
"type": "boolean",
"default": true
}
}
},
"Style/RedundantSelf": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/RedundantSelf.html",
"title": "Style/RedundantSelf",
"description": "Disallows redundant uses of `self`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"AllowedMethodNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"in?",
"inspect",
"not_nil!"
]
}
}
},
"Style/UnlessElse": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/UnlessElse.html",
"title": "Style/UnlessElse",
"description": "Disallows the use of an `else` block with the `unless`",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.1.0"
}
}
},
"Style/VerboseBlock": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/VerboseBlock.html",
"title": "Style/VerboseBlock",
"description": "Identifies usage of collapsible single expression blocks",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.14.0"
},
"ExcludeMultipleLineBlocks": {
"type": "boolean",
"default": true
},
"ExcludeCallsWithBlock": {
"type": "boolean",
"default": true
},
"ExcludePrefixOperators": {
"type": "boolean",
"default": true
},
"ExcludeOperators": {
"type": "boolean",
"default": true
},
"ExcludeSetters": {
"type": "boolean",
"default": false
},
"MaxLineLength": {
"type": [
"number",
"null"
],
"default": null
},
"MaxLength": {
"type": [
"number",
"null"
],
"default": 50
}
}
},
"Style/VerboseNilType": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/VerboseNilType.html",
"title": "Style/VerboseNilType",
"description": "Enforces consistent naming of `Nil` in type unions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"ExplicitNil": {
"type": "boolean",
"default": false
}
}
},
"Style/WhileTrue": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Style/WhileTrue.html",
"title": "Style/WhileTrue",
"description": "Disallows `while` statements with a `true` literal as condition",
"properties": {
"SinceVersion": {
"type": "string",
"default": "0.3.0"
}
}
},
"Typing/MacroCallArgumentTypeRestriction": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Typing/MacroCallArgumentTypeRestriction.html",
"title": "Typing/MacroCallArgumentTypeRestriction",
"description": "Recommends that call arguments to certain macros have type restrictions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"DefaultValue": {
"type": "boolean",
"default": false
},
"MacroNames": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"getter",
"getter?",
"getter!",
"class_getter",
"class_getter?",
"class_getter!",
"setter",
"setter?",
"setter!",
"class_setter",
"class_setter?",
"class_setter!",
"property",
"property?",
"property!",
"class_property",
"class_property?",
"class_property!",
"record"
]
}
}
},
"Typing/MethodParameterTypeRestriction": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Typing/MethodParameterTypeRestriction.html",
"title": "Typing/MethodParameterTypeRestriction",
"description": "Recommends that method parameters have type restrictions",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"DefaultValue": {
"type": "boolean",
"default": false
},
"BlockParameters": {
"type": "boolean",
"default": false
},
"PrivateMethods": {
"type": "boolean",
"default": false
},
"ProtectedMethods": {
"type": "boolean",
"default": false
},
"NodocMethods": {
"type": "boolean",
"default": false
}
}
},
"Typing/MethodReturnTypeRestriction": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Typing/MethodReturnTypeRestriction.html",
"title": "Typing/MethodReturnTypeRestriction",
"description": "Recommends that methods have a return type restriction",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
},
"PrivateMethods": {
"type": "boolean",
"default": false
},
"ProtectedMethods": {
"type": "boolean",
"default": false
},
"NodocMethods": {
"type": "boolean",
"default": false
}
}
},
"Typing/ProcLiteralReturnTypeRestriction": {
"$ref": "#/$defs/BaseRule",
"$comment": "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Typing/ProcLiteralReturnTypeRestriction.html",
"title": "Typing/ProcLiteralReturnTypeRestriction",
"description": "Disallows proc literals without return type restriction",
"properties": {
"SinceVersion": {
"type": "string",
"default": "1.7.0"
},
"Enabled": {
"type": "boolean",
"default": false
}
}
}
}
}
================================================
FILE: .dockerignore
================================================
.*
!LICENSE
!Dockerfile
!Makefile
!shard.yml
!src
================================================
FILE: .editorconfig
================================================
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
## Generated files
.ameba.yml.schema.json linguist-generated
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
================================================
FILE: .github/workflows/add-docs-version-to-json-index.yml
================================================
name: Add docs version to JSON index
on:
workflow_call:
inputs:
version:
required: true
type: string
git-branch:
type: string
default: gh-pages
permissions:
contents: write
jobs:
add-version-to-json-index:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v6
with:
ref: ${{ inputs.git-branch }}
- name: Update version list
run: |
VERSION="${{ inputs.version }}"
if [[ $VERSION =~ ^[0-9] ]]; then
RELEASED="true"
else
RELEASED="false"
fi
JQ_EXPRESSION="
.versions |= (
if ( . | map(.name) | index(\"${VERSION}\") | not ) then
. += [ {\"name\": \"${VERSION}\", \"url\": \"/ameba/${VERSION}/\", \"released\": ${RELEASED}} ]
else
.
end
)"
cat versions.json | \
jq "$JQ_EXPRESSION" > versions.json.tmp && \
mv versions.json.tmp versions.json
- name: Check for changes
id: git-diff-changes
run: |
git diff --color
if [[ -n "$(git diff --exit-code)" ]]; then
echo "Changes detected."
echo "has-changes=true" >> $GITHUB_OUTPUT
else
echo "No changes detected."
echo "has-changes=false" >> $GITHUB_OUTPUT
fi
- name: Configure Git identity
if: steps.git-diff-changes.outputs.has-changes == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Commit changes to Git
if: steps.git-diff-changes.outputs.has-changes == 'true'
run: |
git commit -am "Update versions.json"
git push -u origin HEAD
================================================
FILE: .github/workflows/build-and-deploy-docs.yml
================================================
name: Build and deploy docs
on:
workflow_dispatch:
inputs: &inputs
ref:
required: true
type: string
description: The tag or branch to deploy
version:
type: string
description: Version to deploy
git-branch:
type: string
description: Git branch to deploy to
default: gh-pages
workflow_call:
inputs: *inputs
permissions:
contents: write
jobs:
build-and-deploy:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v5
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
- name: Download source
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Get ref SHA
id: ref-sha
run: |
REF_SHA="$(git rev-parse --short HEAD)"
echo "REF_SHA: ${REF_SHA}"
echo "sha=${REF_SHA}" >> $GITHUB_OUTPUT
- name: Install dependencies
run: shards install
- name: Build docs
run: |
crystal docs \
--project-version="${{ inputs.version || inputs.ref }}" \
--source-refname="${{ steps.ref-sha.outputs.sha }}" \
--json-config-url="/ameba/versions.json"
- name: Deploy docs 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: ${{ inputs.git-branch }}
folder: docs
target-folder: ${{ inputs.version || inputs.ref }}
clean: true
add-version-to-json-index:
uses: ./.github/workflows/add-docs-version-to-json-index.yml
with:
git-branch: ${{ inputs.git-branch }}
version: ${{ inputs.version || inputs.ref }}
needs:
- build-and-deploy
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
workflow_dispatch:
push:
branches: [master]
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: "0 3 * * 1" # Every monday at 3 AM
permissions:
contents: read
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
crystal: [latest, nightly]
runs-on: ${{ matrix.os }}
steps:
- name: Set timezone to UTC
uses: szenius/set-timezone@v2.0
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
with:
crystal: ${{ matrix.crystal }}
- name: Download source
uses: actions/checkout@v6
- name: Install dependencies
run: shards install
- name: Install typos-cli
if: matrix.os == 'macos-latest'
run: brew install typos-cli
- name: Run specs
run: make spec
- name: Build ameba binary
run: make build
- name: Run ameba linter
run: make lint
================================================
FILE: .github/workflows/compare-json-schema.yml
================================================
name: Compare JSON Schema
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- ".github/workflows/compare-json-schema.yml"
- "**/*.cr"
permissions:
contents: read
jobs:
check-for-changes:
runs-on: ubuntu-latest
steps:
- name: Set timezone to UTC
uses: szenius/set-timezone@v2.0
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
- name: Download source
uses: actions/checkout@v6
- name: Install dependencies
run: shards install
- name: Run JSON Schema builder
run: shards run json-schema-builder
- name: Check for changes
run: git diff --color --exit-code
continue-on-error: true
================================================
FILE: .github/workflows/docker-image.yml
================================================
name: Docker image build and deploy
on:
workflow_dispatch:
push:
branches: [master]
release:
types: [published]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as /
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v4
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into ${{ env.REGISTRY }} registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag
type=ref,event=branch
type=ref,event=pr
type=sha,format=long
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
# Build and push Docker image with Buildx
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v7
with:
context: .
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
CRFLAGS=-Dpreview_mt --release
platforms: |
linux/amd64
linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
================================================
FILE: .github/workflows/docs.yml
================================================
name: Docs
on:
push:
branches:
- master
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
setup:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.variables.outputs.version }}
ref: ${{ steps.variables.outputs.ref }}
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v5
- name: Set output variables
id: variables
run: |
VERSION="${GITHUB_REF_NAME}"
if [[ $VERSION =~ ^v[0-9].* ]]; then
VERSION=${VERSION#"v"}
fi
echo "VERSION: ${VERSION}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
REF="${GITHUB_REF_POINT}"
echo "REF: ${REF}"
echo "ref=${REF}" >> $GITHUB_OUTPUT
build-and-deploy:
uses: ./.github/workflows/build-and-deploy-docs.yml
with:
version: ${{ needs.setup.outputs.version }}
ref: ${{ needs.setup.outputs.ref }}
needs:
- setup
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
jobs:
draft:
runs-on: ubuntu-latest
steps:
- run: gh release -R ${{ github.repository }} create ${{ github.ref_name }} --draft --generate-notes
linux-musl:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- run: docker run --rm -v $PWD:/mnt -w /mnt crystallang/crystal:latest-alpine shards build ameba --static -Dpreview_mt --release
- run: tar zcf ameba-${{ github.ref_name }}-$(uname -m)-linux-musl.tar.gz -C bin ameba
- run: gh release -R ${{ github.repository }} upload ${{ github.ref_name }} ameba-${{ github.ref_name }}-$(uname -m)-linux-musl.tar.gz
darwin:
strategy:
fail-fast: false
matrix:
include:
- {os: macos-15, arch: aarch64}
- {os: macos-15-intel, arch: x86_64}
runs-on: ${{ matrix.os }}
steps:
- uses: crystal-lang/install-crystal@v1
- uses: actions/checkout@v6
- run: shards build ameba -Dpreview_mt --release
- run: tar zcf ameba-${{ github.ref_name }}-${{ matrix.arch }}-darwin.tar.gz -C bin ameba ameba.dwarf
- run: gh release -R ${{ github.repository }} upload ${{ github.ref_name }} ameba-${{ github.ref_name }}-${{ matrix.arch }}-darwin.tar.gz
x86_64-windows-msvc:
runs-on: windows-2025
steps:
- uses: crystal-lang/install-crystal@v1
- uses: actions/checkout@v6
- run: shards build ameba --static -Dpreview_mt --release
- run: cd bin; 7z a ../ameba-${{ github.ref_name }}-x86_64-windows-msvc.zip ameba.exe ameba.pdb
- run: gh release -R ${{ github.repository }} upload ${{ github.ref_name }} ameba-${{ github.ref_name }}-x86_64-windows-msvc.zip
================================================
FILE: .github/workflows/typos.yml
================================================
name: Spell checker
on:
push:
branches: [master]
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
typos:
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v6
- name: Run `typos` spell checker
uses: crate-ci/typos@v1
================================================
FILE: .github/workflows/update-json-schema.yml
================================================
name: Update JSON Schema
on:
workflow_dispatch:
push:
branches: [master]
paths:
- ".github/workflows/update-json-schema.yml"
- "shard.yml"
- "**/*.cr"
permissions:
contents: write
jobs:
update-json-schema:
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
steps:
- name: Set timezone to UTC
uses: szenius/set-timezone@v2.0
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
- name: Download source
uses: actions/checkout@v6
with:
ssh-key: ${{ secrets.MASTER_PUSHER_SSH_KEY }}
- name: Install dependencies
run: shards install
- name: Run JSON Schema builder
run: shards run json-schema-builder
- name: Check for changes
id: git-diff-changes
run: |
if [[ -n "$(git diff --exit-code)" ]]; then
echo "Changes detected."
echo "has-changes=true" >> $GITHUB_OUTPUT
else
echo "No changes detected."
echo "has-changes=false" >> $GITHUB_OUTPUT
fi
- name: Configure Git identity
if: steps.git-diff-changes.outputs.has-changes == 'true'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Commit and push changes
if: steps.git-diff-changes.outputs.has-changes == 'true'
run: |
git commit -am "Update JSON Schema"
git push -u origin HEAD
================================================
FILE: .gitignore
================================================
# Documentation
/docs/
# Installed shards
/lib/
# Built binaries
/bin/**/*
!/bin/ameba.cr
# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock
# Workspace settings used by common text-editors
/.vscode
/.zed
================================================
FILE: .typos.toml
================================================
[default]
extend-ignore-re = [
# numeric literals
'0x[0-9a-fA-F_\.\+]+([fiu](8|16|32|64|128))?',
'\\u\{[0-9a-fA-F]+\}',
# fixed test values
'[Ff][Oo][Oo]+',
# words including a number are likely some kind of identifier
"[a-zA-Z]+[0-9]+",
]
[files]
extend-exclude = [
# git and dependencies
".git/**",
"lib/**",
# individual files to exclude
"spec/ameba/rule/lint/typos_spec.cr",
]
================================================
FILE: Dockerfile
================================================
FROM alpine:edge AS builder
ARG CRFLAGS="-Dpreview_mt"
RUN apk add --update crystal shards yaml-dev musl-dev make
RUN mkdir /ameba
WORKDIR /ameba
COPY . /ameba/
RUN crystal -v
RUN make clean && make CRFLAGS="$CRFLAGS"
FROM alpine:latest
RUN apk add --update yaml pcre2 gc libevent libgcc
RUN mkdir /src
WORKDIR /src
COPY --from=builder /ameba/bin/ameba /usr/bin/
RUN ameba -v
ENTRYPOINT [ "/usr/bin/ameba" ]
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2018-2020 Vitalii Elenhaupt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Makefile
================================================
.POSIX:
all:
# Recipes
## Build ameba
## $ make
##
## Run tests
## $ make test
##
## Install ameba
## $ sudo make install
-include Makefile.local # for optional local options
BUILD_TARGET := bin/ameba
DESTDIR ?= ## Install destination dir
PREFIX ?= /usr/local## Install path prefix
BINDIR ?= $(DESTDIR)$(PREFIX)/bin
# The crystal command to use
CRYSTAL_BIN ?= crystal
# The shards command to use
SHARDS_BIN ?= shards
# The install command to use
INSTALL_BIN ?= /usr/bin/install
CRFLAGS ?= -Dpreview_mt
SRC_SOURCES := $(shell find src -name '*.cr' 2>/dev/null)
.PHONY: all
all: build
.PHONY: build
build: ## Build the application binary
build: $(BUILD_TARGET)
$(BUILD_TARGET): $(SRC_SOURCES)
$(SHARDS_BIN) build ameba $(CRFLAGS)
.PHONY: docs
docs: ## Generate API docs
docs: $(SRC_SOURCES)
$(CRYSTAL_BIN) docs
.PHONY: spec
spec: ## Run the spec suite
spec:
$(CRYSTAL_BIN) spec
.PHONY: schema
schema: ## Build the latest schema
schema:
$(SHARDS_BIN) run schema
.PHONY: lint
lint: ## Run ameba on its own code base
lint: $(BUILD_TARGET)
$(BUILD_TARGET)
.PHONY: test
test: ## Run the spec suite and linter
test: spec lint
.PHONY: clean
clean: ## Remove application binary and API docs
clean:
@rm -f "$(BUILD_TARGET)" "$(BUILD_TARGET).dwarf"
@rm -rf docs
.PHONY: install
install: ## Install application binary into $DESTDIR
install: $(BUILD_TARGET)
mkdir -p "$(BINDIR)"
$(INSTALL_BIN) -m 0755 "$(BUILD_TARGET)" "$(BINDIR)/ameba"
.PHONY: help
help: ## Show this help
@printf '\033[34mtargets:\033[0m\n'
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\
sort |\
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
@echo
@printf '\033[34moptional variables:\033[0m\n'
@grep -hE '^[a-zA-Z_-]+ \?=.*?## .*$$' $(MAKEFILE_LIST) |\
sort |\
awk 'BEGIN {FS = " \\?=.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
@echo
@printf '\033[34mrecipes:\033[0m\n'
@grep -hE '^##.*$$' $(MAKEFILE_LIST) |\
awk 'BEGIN {FS = "## "}; /^## [a-zA-Z_-]/ {printf " \033[36m%s\033[0m\n", $$2}; /^## / {printf " %s\n", $$2}'
================================================
FILE: README.md
================================================
Ameba
Code style linter for Crystal
(a single-celled animal that catches food and moves about by extending fingerlike projections of protoplasm)
- [About](#about)
- [Usage](#usage)
- [Watch a tutorial](#watch-a-tutorial)
- [Autocorrection](#autocorrection)
- [Explain issues](#explain-issues)
- [Run in parallel](#run-in-parallel)
- [Installation](#installation)
- [As a project dependency:](#as-a-project-dependency)
- [OS X](#os-x)
- [Docker](#docker)
- [From sources](#from-sources)
- [Configuration](#configuration)
- [Sources](#sources)
- [Rules](#rules)
- [Inline disabling](#inline-disabling)
- [Editors \& integrations](#editors--integrations)
- [Credits \& inspirations](#credits--inspirations)
- [Contributors](#contributors)
## About
Ameba is a static code analysis tool for the Crystal language.
It enforces a consistent [Crystal code style](https://crystal-lang.org/reference/conventions/coding_style.html),
also catches code smells and wrong code constructions.
See also [Roadmap](https://github.com/crystal-ameba/ameba/wiki).
## Usage
Run `ameba` binary within your project directory to catch code issues:
```sh
$ ameba
Inspecting 107 files
...............F.....................FF....................................................................
src/ameba/formatter/flycheck_formatter.cr:6:37
[W] Lint/UnusedArgument: Unused argument `location`. If it's necessary, use `_` as an argument name to indicate that it won't be used.
> source.issues.each do |issue, location|
^
src/ameba/formatter/base_formatter.cr:16:14
[W] Lint/UselessAssign: Useless assignment to variable `s`
> return s += issues.size
^
src/ameba/formatter/base_formatter.cr:16:7 [Correctable]
[C] Style/RedundantReturn: Redundant `return` detected
> return s += issues.size
^---------------------^
Finished in 389.45 milliseconds
107 inspected, 3 failures
```
### Watch a tutorial
[🎬 Watch the LuckyCast showing how to use Ameba](https://luckycasts.com/videos/ameba)
### Autocorrection
Rules that are marked as `[Correctable]` in the output can be automatically corrected using `--fix` flag:
```sh
$ ameba --fix
```
### Explain issues
Ameba allows you to dig deeper into an issue, by showing you details about the issue
and the reasoning by it being reported.
To be convenient, you can just copy-paste the `PATH:line:column` string from the
report and paste behind the `ameba` command to check it out.
```sh
$ ameba crystal/command/format.cr:26:83 # show explanation for the issue
$ ameba --explain crystal/command/format.cr:26:83 # same thing
```
### Run in parallel
Some quick benchmark results measured while running Ameba on Crystal repo:
```sh
$ CRYSTAL_WORKERS=1 ameba #=> 29.11 seconds
$ CRYSTAL_WORKERS=2 ameba #=> 19.49 seconds
$ CRYSTAL_WORKERS=4 ameba #=> 13.48 seconds
$ CRYSTAL_WORKERS=8 ameba #=> 10.14 seconds
```
## Installation
### As a project dependency
Add this to your application's `shard.yml`:
```yaml
development_dependencies:
ameba:
github: crystal-ameba/ameba
```
To prioritize runtime performance over compilation time, you can add `ameba`
target to the `shard.yml` file:
```yaml
targets:
ameba:
main: lib/ameba/bin/ameba.cr
```
And then run:
```sh
$ shards build ameba -Dpreview_mt
```
Alternatively, skip adding `ameba` target and use `crystal build` command directly:
```sh
$ crystal build -Dpreview_mt -o bin/ameba lib/ameba/bin/ameba.cr
```
Both of these will result in a compiled binary placed under `bin/ameba` path.
You can also just run the `lib/ameba/bin/ameba.cr` file, compiling it on the fly,
which is the slowest option:
```sh
$ lib/ameba/bin/ameba.cr
```
### OS X
```sh
$ brew tap crystal-ameba/ameba
$ brew install ameba
```
### Docker
Build the image:
```sh
$ docker build -t ghcr.io/crystal-ameba/ameba .
```
To use the resulting image on a local source folder, mount the current (or target) directory into `/src`:
```sh
$ docker run -v $(pwd):/src ghcr.io/crystal-ameba/ameba
```
Also available on GitHub: https://github.com/crystal-ameba/ameba/pkgs/container/ameba
### From sources
```sh
$ git clone https://github.com/crystal-ameba/ameba && cd ameba
$ make install
```
## Configuration
Default configuration file is `.ameba.yml`.
It allows to configure rule properties, disable specific rules and exclude sources from the rules.
Generate new file by running `ameba --gen-config`.
### Sources
**List of sources to run Ameba on can be configured globally via:**
- `Globs` section - an array of wildcards (or paths) to include to the
inspection. Defaults to `%w[**/*.cr **/*.ecr]`, meaning it includes all project
files with `*.cr` and `*.ecr` extensions.
- `Excluded` section - an array of wildcards (or paths) to exclude from the
source list defined by `Globs`. Defaults to `%w[lib]`, meaning it excludes the
`lib` folder.
In this example we define default globs and exclude `lib` and `src/compiler` folders:
``` yaml
Globs:
- "**/*.cr"
- "**/*.ecr"
Excluded:
- lib
- src/compiler
```
**Specific sources can be excluded at rule level**:
``` yaml
Style/RedundantBegin:
Excluded:
- src/server/processor.cr
- src/server/api.cr
```
### Rules
One or more rules, or a one or more group of rules can be included or excluded
via command line arguments:
```sh
$ ameba --only Lint/Syntax # runs only Lint/Syntax rule
$ ameba --only Style,Lint # runs only rules from Style and Lint groups
$ ameba --except Lint/Syntax # runs all rules except Lint/Syntax
$ ameba --except Style,Lint # runs all rules except rules in Style and Lint groups
```
Or through the configuration file:
``` yaml
Style/RedundantBegin:
Enabled: false
```
### Inline disabling
One or more rules or one or more group of rules can be disabled using inline directives:
```crystal
# ameba:disable Style/LargeNumbers
time = Time.epoch(1483859302)
time = Time.epoch(1483859302) # ameba:disable Style/LargeNumbers, Lint/UselessAssign
time = Time.epoch(1483859302) # ameba:disable Style, Lint
```
## Editors & integrations
- Vim: [vim-crystal](https://github.com/rhysd/vim-crystal), [Ale](https://github.com/w0rp/ale)
- Emacs: [ameba.el](https://github.com/crystal-ameba/ameba.el)
- Sublime Text: [Sublime Linter Ameba](https://github.com/epergo/SublimeLinter-contrib-ameba)
- VSCode: [vscode-crystal-ameba](https://github.com/crystal-ameba/vscode-crystal-ameba)
- Codacy: [codacy-ameba](https://github.com/codacy/codacy-ameba)
- GitHub Actions: [github-action](https://github.com/crystal-ameba/github-action)
## Credits & inspirations
- [Crystal Language](https://crystal-lang.org)
- [Rubocop](https://rubocop.org)
- [Credo](http://credo-ci.org)
- [Dogma](https://github.com/lpil/dogma)
## Contributors
- [veelenga](https://github.com/veelenga) Vitalii Elenhaupt - creator, maintainer
- [Sija](https://github.com/Sija) Sijawusz Pur Rahnama - contributor, maintainer
================================================
FILE: bench/check_sources.cr
================================================
require "../src/ameba"
require "benchmark"
private def get_files(n)
Dir["src/**/*.cr"].first(n)
end
puts "== Compare:"
Benchmark.ips do |x|
[
1,
3,
5,
10,
20,
30,
40,
].each do |n| # ameba:disable Naming/BlockParameterName
config = Ameba::Config.load
config.formatter = Ameba::Formatter::BaseFormatter.new
config.globs = get_files(n)
s = n == 1 ? "" : "s"
x.report("#{n} source#{s}") { Ameba.run config }
end
end
puts "== Measure:"
config = Ameba::Config.load
config.formatter = Ameba::Formatter::BaseFormatter.new
puts Benchmark.measure { Ameba.run config }
================================================
FILE: bin/ameba.cr
================================================
#!/usr/bin/env crystal
# Require ameba extensions here which are added as project dependencies.
# Example:
#
# require "ameba-performance"
# Require ameba cli which starts the inspection.
require "ameba/cli"
================================================
FILE: shard.yml
================================================
name: ameba
version: 1.7.0-dev
authors:
- Vitalii Elenhaupt
- Sijawusz Pur Rahnama
targets:
ameba:
main: src/cli.cr
json-schema-builder:
main: src/json-schema-builder.cr
crystal: ~> 1.19
license: MIT
================================================
FILE: spec/ameba/ast/flow_expression_spec.cr
================================================
require "../../spec_helper"
module Ameba::AST
describe FlowExpression do
describe "#initialize" do
it "creates a new flow expression" do
node = as_node("return 22")
flow_expression = FlowExpression.new node, false
flow_expression.node.should_not be_nil
flow_expression.in_loop?.should be_false
end
describe "#delegation" do
it "delegates to_s to @node" do
node = as_node("return 22")
flow_expression = FlowExpression.new node, false
flow_expression.to_s.should eq node.to_s
end
it "delegates locations to @node" do
node = as_node("break if true")
flow_expression = FlowExpression.new node, false
flow_expression.location.should eq node.location
flow_expression.end_location.should eq node.end_location
end
end
describe "#unreachable_nodes" do
it "returns unreachable nodes" do
nodes = as_nodes <<-CRYSTAL
def foobar
return
a = 1
a = 2
end
CRYSTAL
node = nodes.expressions_nodes.first
flow_expression = FlowExpression.new node, false
flow_expression.unreachable_nodes.should eq nodes.assign_nodes
end
it "returns nil if there is no unreachable node after loop" do
nodes = as_nodes <<-CRYSTAL
def run
idx = items.size - 1
while 0 <= idx
return
end
puts "foo"
end
CRYSTAL
node = nodes.expressions_nodes.first
flow_expression = FlowExpression.new node, false
flow_expression.unreachable_nodes.empty?.should be_true
end
it "returns nil if there is no unreachable node" do
nodes = as_nodes <<-CRYSTAL
def foobar
a = 1
return a
end
CRYSTAL
node = nodes.expressions_nodes.first
flow_expression = FlowExpression.new node, false
flow_expression.unreachable_nodes.empty?.should be_true
end
end
end
end
end
================================================
FILE: spec/ameba/ast/liveness_analyzer_spec.cr
================================================
require "../../spec_helper"
private def scopes_for(code)
rule = Ameba::ScopeRule.new
source = Ameba::Source.new(code)
Ameba::AST::ScopeVisitor.new(rule, source)
rule.scopes
end
private def def_scope(code)
scopes_for(code).find! { |scope| scope.node.is_a?(Crystal::Def) }
end
private def top_scope(code)
scopes_for(code).find! { |scope| scope.node.is_a?(Crystal::Expressions) }
end
private def block_scope(code)
scopes_for(code).find! { |scope| scope.node.is_a?(Crystal::Block) }
end
private def dead_store_names(scope)
Ameba::AST::LivenessAnalyzer.new(scope).dead_stores.map(&.variable.name)
end
module Ameba::AST
describe LivenessAnalyzer do
context "basic assignments" do
it "detects unused assignment as dead store" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report assignment that is used" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "detects first assignment as dead when overwritten before use" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a = 2
a
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "detects all assignments as dead when none are used" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a = 2
end
CRYSTAL
dead_store_names(scope).should eq ["a", "a"]
end
it "does not report assignment used in a condition" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
if a
nil
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports second assignment when value is not used" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a = a + 1
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report assignment used in another assignment" do
scope = def_scope <<-CRYSTAL
def foo
if f = get_something
@f = f
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports last assignment when not used after reassignment" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
puts a
a = 2
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
end
context "op assignments" do
it "does not report op-assign when result is used" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a += 1
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports op-assign when result is not used" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a += 1
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report chained op-assigns when result is used" do
scope = def_scope <<-CRYSTAL
def foo
a = 1
a += 1
a += 1
a = a + 1
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "if/unless branches" do
it "reports initial assignment as dead when overwritten in both branches" do
scope = def_scope <<-CRYSTAL
def foo
a = 0
if something
a = 1
else
a = 2
end
a
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report when assigned in one branch and used after" do
scope = def_scope <<-CRYSTAL
def foo
a = 0
if something
a = 1
end
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report when assigned in one branch with else nil and used after" do
scope = def_scope <<-CRYSTAL
def foo
a = 0
if something
a = 1
else
nil
end
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports useless assignment in branch when not used after" do
scope = def_scope <<-CRYSTAL
def foo(a)
if a
a = 2
end
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "reports first dead assignment in branch when overwritten" do
scope = def_scope <<-CRYSTAL
def foo(a)
a = 1
if a
a = 2
a = 3
end
a
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "reports initial assignment as dead when overwritten in all branches" do
scope = def_scope <<-CRYSTAL
def foo
has_newline = false
if something
do_something unless false
has_newline = false
else
do_something if true
has_newline = true
end
has_newline
end
CRYSTAL
dead_store_names(scope).should eq ["has_newline"]
end
it "does not report unless with consumed branches" do
scope = def_scope <<-CRYSTAL
def foo
a = 0
unless something
a = 1
else
a = 2
end
a
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "reports dead assignment in unless branch" do
scope = def_scope <<-CRYSTAL
def foo
a = 0
unless something
a = 1
a = 2
else
a = 2
end
a
end
CRYSTAL
dead_store_names(scope).should eq ["a", "a"]
end
it "does not report one-line if assignment used after" do
scope = def_scope <<-CRYSTAL
def foo
a = 0
a = 1 if something
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "while loops" do
it "does not report assignment used across iterations" do
scope = def_scope <<-CRYSTAL
def foo(a)
while a < 10
a = a + 1
end
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports assignment not used outside loop" do
scope = def_scope <<-CRYSTAL
def foo(a)
while a < 10
b = a
end
end
CRYSTAL
dead_store_names(scope).should eq ["b"]
end
it "does not report assignment used in loop with accumulator" do
scope = def_scope <<-CRYSTAL
def foo
a = 3
result = 0
while result < 10
result += a
a = a + 1
end
result
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report parameter assignment used in loop" do
scope = def_scope <<-CRYSTAL
def foo(a)
result = 0
while result < 10
result += a
a = a + 1
end
result
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment in loop with inner branch" do
scope = def_scope <<-CRYSTAL
def foo(a)
result = 0
while result < 10
result += a
if result > 0
a = a + 1
else
a = 3
end
end
result
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "handles branch with blank node in loop" do
scope = def_scope <<-CRYSTAL
def foo
count = 0
while true
break if count == 1
case something
when :any
else
:anything_else
end
count += 1
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment used after break" do
scope = def_scope <<-CRYSTAL
def foo
found = false
while true
if something
found = true
break
end
end
found
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment before next used in subsequent iteration" do
scope = def_scope <<-CRYSTAL
def foo
atomic = parse_atomic
while true
if @token.instance_var?
atomic = parse_ivar(atomic)
next
end
break
end
atomic
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment used after break in nested loops" do
scope = def_scope <<-CRYSTAL
def foo
found = false
while outer_cond
while inner_cond
if something
found = true
break
end
end
end
found
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment inside conditional break" do
scope = def_scope <<-CRYSTAL
def foo
options = 0
while true
if done?
options = compute_options
break
end
process
end
options
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "until loops" do
it "does not report assignment used across until iterations" do
scope = def_scope <<-CRYSTAL
def foo(a)
until a > 10
a = a + 1
end
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports useless assignment in until loop" do
scope = def_scope <<-CRYSTAL
def foo(a)
until a > 10
b = a + 1
end
end
CRYSTAL
dead_store_names(scope).should eq ["b"]
end
end
context "exception handlers" do
it "does not report assignment used in rescue" do
scope = def_scope <<-CRYSTAL
def foo(a)
a = 2
rescue
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment used in ensure" do
scope = def_scope <<-CRYSTAL
def foo(a)
a = 2
ensure
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment used in else" do
scope = def_scope <<-CRYSTAL
def foo(a)
a = 2
rescue
else
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports useless assignment in rescue" do
scope = def_scope <<-CRYSTAL
def foo(a)
rescue
a = 2
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report assignment used in rescue when body has break" do
scope = block_scope <<-CRYSTAL
3.times do
start = 1
begin
perform_foo
break
rescue
start
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment used in rescue when body has return" do
scope = def_scope <<-CRYSTAL
def foo
start = 1
begin
perform_foo
return
rescue
start
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not report assignment used in rescue when body has next" do
scope = block_scope <<-CRYSTAL
3.times do
start = 1
begin
perform_foo
next
rescue
start
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "binary operators" do
it "does not report when both sides of && are used" do
scope = def_scope <<-CRYSTAL
def foo(a)
(a = 1) && (b = 1)
a + b
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports unused side of ||" do
scope = def_scope <<-CRYSTAL
def foo(a)
(a = 1) || (b = 1)
a
end
CRYSTAL
dead_store_names(scope).should eq ["b"]
end
end
context "case" do
it "does not report when used after case" do
scope = def_scope <<-CRYSTAL
def foo(a)
case a
when /foo/
a = 1
when /bar/
a = 2
end
puts a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports when not used after case" do
scope = def_scope <<-CRYSTAL
def foo(a)
case a
when /foo/
a = 1
when /bar/
a = 2
end
end
CRYSTAL
dead_store_names(scope).should eq ["a", "a"]
end
it "does not report assignment used in case condition" do
scope = def_scope <<-CRYSTAL
def foo
a = 2
case a
when /foo/
end
end
CRYSTAL
dead_store_names(scope).should be_empty
end
context "when" do
it "does not report when assignment in when condition is used" do
scope = def_scope <<-CRYSTAL
def foo(a)
case
when a = foo_call
when a = bar_call
end
puts a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports when assignment in when condition is not used" do
scope = def_scope <<-CRYSTAL
def foo(a)
case
when a = foo_call
when a = bar_call
end
end
CRYSTAL
dead_store_names(scope).should eq ["a", "a"]
end
end
end
context "multi assignments" do
it "does not report when all targets are used" do
scope = def_scope <<-CRYSTAL
def foo
a, b = {1, 2}
a + b
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports unused multi-assign target" do
scope = def_scope <<-CRYSTAL
def foo
a, b = {1, 2}
a
end
CRYSTAL
dead_store_names(scope).should eq ["b"]
end
it "reports all unused multi-assign targets" do
scope = def_scope <<-CRYSTAL
def foo
a, b = {1, 2}
end
CRYSTAL
dead_store_names(scope).should eq ["b", "a"]
end
it "reports reassigned multi-assign targets" do
scope = def_scope <<-CRYSTAL
def foo
a, b = {1, 2}
a, b = {3, 4}
end
CRYSTAL
dead_store_names(scope).should eq ["b", "a", "b", "a"]
end
it "reports multi-assign target overwritten at loop start" do
scope = def_scope <<-CRYSTAL
def foo
while true
word = get_word
if (word & 0xFF) == 0
word, success = compare_and_set(word, word + 1)
return if success
end
end
end
CRYSTAL
dead_store_names(scope).should eq ["word"]
end
it "does not report multi-assign target used in next loop iteration" do
scope = def_scope <<-CRYSTAL
def foo
while true
word = get_word
if (word & 0xFF) == 0
word, success = compare_and_set(word, word + 1)
if success
return
end
else
puts word
end
end
word
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "top level scope" do
it "detects dead stores at top level" do
scope = top_scope <<-CRYSTAL
a = 1
a = 2
CRYSTAL
dead_store_names(scope).should eq ["a", "a"]
end
it "does not report referenced top-level assignments" do
scope = top_scope <<-CRYSTAL
a = 1
a += 1
a
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "type declarations" do
it "does not report unused type declaration without value" do
scope = def_scope <<-CRYSTAL
def foo
a : String?
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "reports unused type declaration with value" do
scope = def_scope <<-CRYSTAL
def foo
a : String? = "foo"
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report used type declaration" do
scope = def_scope <<-CRYSTAL
def foo
a : String?
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "uninitialized" do
it "reports unused uninitialized assignment" do
scope = def_scope <<-CRYSTAL
def foo
a = uninitialized UInt8
end
CRYSTAL
dead_store_names(scope).should eq ["a"]
end
it "does not report used uninitialized assignment" do
scope = def_scope <<-CRYSTAL
def foo
a = uninitialized UInt8
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
end
context "super and previous_def" do
it "treats bare super as reading all arguments" do
scope = def_scope <<-CRYSTAL
def foo(a, b)
a = super
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "treats bare previous_def as reading all arguments" do
scope = def_scope <<-CRYSTAL
def foo(a)
a = previous_def
a
end
CRYSTAL
dead_store_names(scope).should be_empty
end
it "does not treat super() with parens as reading arguments" do
scope = def_scope <<-CRYSTAL
def foo(a)
b = super()
end
CRYSTAL
dead_store_names(scope).should eq ["b"]
end
it "does not treat super with explicit args as reading all arguments" do
scope = def_scope <<-CRYSTAL
def foo(a)
b = super(1)
end
CRYSTAL
dead_store_names(scope).should eq ["b"]
end
end
end
end
================================================
FILE: spec/ameba/ast/scope_spec.cr
================================================
require "../../spec_helper"
module Ameba::AST
describe Scope do
describe "#initialize" do
source = "a = 2"
it "assigns outer scope" do
root = Scope.new as_node(source)
child = Scope.new as_node(source), root
child.outer_scope.should_not be_nil
end
it "assigns node" do
scope = Scope.new as_node(source)
scope.node.should_not be_nil
end
end
end
describe "delegation" do
it "delegates to_s to node" do
node = as_node("def foo; end")
scope = Scope.new node
scope.to_s.should eq node.to_s
end
it "delegates locations to node" do
node = as_node("def foo; end")
scope = Scope.new node
scope.location.should eq node.location
scope.end_location.should eq node.end_location
end
end
describe "#references" do
it "can return an empty list of references" do
scope = Scope.new as_node("")
scope.references.should be_empty
end
it "allows to add variable references" do
scope = Scope.new as_node("")
nodes = as_nodes "a = 2"
scope.references << Reference.new(nodes.var_nodes.first, scope)
scope.references.size.should eq 1
end
end
describe "#references?" do
it "returns true if current scope references variable" do
nodes = as_nodes <<-CRYSTAL
def method
a = 2
block do
3.times { |i| a = a + i }
end
end
CRYSTAL
var_node = nodes.var_nodes.first
scope = Scope.new nodes.def_nodes.first
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
variable.reference(nodes.var_nodes.first, scope.inner_scopes.first)
scope.references?(variable).should be_true
end
it "returns false if inner scopes are not checked" do
nodes = as_nodes <<-CRYSTAL
def method
a = 2
block do
3.times { |i| a = a + i }
end
end
CRYSTAL
var_node = nodes.var_nodes.first
scope = Scope.new nodes.def_nodes.first
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
variable.reference(nodes.var_nodes.first, scope.inner_scopes.first)
scope.references?(variable, check_inner_scopes: false).should be_false
end
it "returns false if current scope does not reference variable" do
nodes = as_nodes <<-CRYSTAL
def method
a = 2
block do
b = 3
3.times { |i| b = b + i }
end
end
CRYSTAL
var_node = nodes.var_nodes.first
scope = Scope.new nodes.def_nodes.first
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
scope.inner_scopes.first.references?(variable).should be_false
end
end
describe "#add_variable" do
it "adds a new variable to the scope" do
scope = Scope.new as_node("")
scope.add_variable(Crystal::Var.new "foo")
scope.variables.empty?.should be_false
end
end
describe "#find_variable" do
it "returns the variable in the scope by name" do
scope = Scope.new as_node("foo = 1")
scope.add_variable(Crystal::Var.new "foo")
scope.find_variable("foo").should_not be_nil
end
it "returns nil if variable not exist in this scope" do
scope = Scope.new as_node("foo = 1")
scope.find_variable("bar").should be_nil
end
end
describe "#assign_variable" do
it "creates a new assignment" do
scope = Scope.new as_node("foo = 1")
scope.add_variable(Crystal::Var.new "foo")
scope.assign_variable("foo", Crystal::Var.new "foo")
var = scope.find_variable("foo").should_not be_nil
var.assignments.size.should eq 1
end
it "does not create the assignment if variable is wrong" do
scope = Scope.new as_node("foo = 1")
scope.add_variable(Crystal::Var.new "foo")
scope.assign_variable("bar", Crystal::Var.new "bar")
var = scope.find_variable("foo").should_not be_nil
var.assignments.size.should eq 0
end
end
describe "#block?" do
it "returns true if Crystal::Block" do
nodes = as_nodes("3.times {}")
scope = Scope.new nodes.block_nodes.first
scope.block?.should be_true
end
it "returns false otherwise" do
scope = Scope.new as_node("a = 1")
scope.block?.should be_false
end
end
describe "#spawn_block?" do
it "returns true if a node is a spawn block" do
nodes = as_nodes("spawn {}")
scope = Scope.new nodes.block_nodes.first
scope.spawn_block?.should be_true
end
it "returns false otherwise" do
scope = Scope.new as_node("a = 1")
scope.spawn_block?.should be_false
end
end
describe "#def?" do
context "when check_outer_scopes: true" do
it "returns true if outer scope is Crystal::Def" do
nodes = as_nodes("def foo; 3.times {}; end")
outer_scope = Scope.new nodes.def_nodes.first
scope = Scope.new nodes.block_nodes.first, outer_scope
scope.def?(check_outer_scopes: true).should be_true
scope.def?.should be_false
end
end
it "returns true if Crystal::Def" do
nodes = as_nodes("def foo; end")
scope = Scope.new nodes.def_nodes.first
scope.def?.should be_true
end
it "returns false otherwise" do
scope = Scope.new as_node("a = 1")
scope.def?.should be_false
end
end
describe "#in_macro?" do
it "returns true if Crystal::Macro" do
nodes = as_nodes <<-CRYSTAL
macro included
end
CRYSTAL
scope = Scope.new nodes.macro_nodes.first
scope.in_macro?.should be_true
end
it "returns true if node is nested to Crystal::Macro" do
nodes = as_nodes <<-CRYSTAL
macro included
{{ @type.each do |type| a = type end }}
end
CRYSTAL
outer_scope = Scope.new nodes.macro_nodes.first
scope = Scope.new nodes.block_nodes.first, outer_scope
scope.in_macro?.should be_true
end
it "returns false otherwise" do
scope = Scope.new as_node("a = 1")
scope.in_macro?.should be_false
end
end
end
================================================
FILE: spec/ameba/ast/util_spec.cr
================================================
require "../../spec_helper"
module Ameba::AST
struct Test
include Util
end
describe Util do
subject = Test.new
describe "#literal?" do
[
Crystal::ArrayLiteral.new,
Crystal::BoolLiteral.new(false),
Crystal::CharLiteral.new('a'),
Crystal::HashLiteral.new,
Crystal::NamedTupleLiteral.new,
Crystal::NilLiteral.new,
Crystal::NumberLiteral.new(42),
Crystal::RegexLiteral.new(Crystal::StringLiteral.new("")),
Crystal::StringLiteral.new(""),
Crystal::SymbolLiteral.new(""),
Crystal::TupleLiteral.new([] of Crystal::ASTNode),
Crystal::RangeLiteral.new(
Crystal::NilLiteral.new,
Crystal::NilLiteral.new,
true),
].each do |literal|
it "returns true if node is #{literal}" do
subject.literal?(literal).should be_true
end
end
it "returns false if node is not a literal" do
subject.literal?(Crystal::Nop).should be_false
end
end
describe "#static/dynamic_literal?" do
[
Crystal::ArrayLiteral.new,
Crystal::ArrayLiteral.new([
Crystal::StringLiteral.new("foo"),
] of Crystal::ASTNode),
Crystal::BoolLiteral.new(false),
Crystal::CharLiteral.new('a'),
Crystal::HashLiteral.new,
Crystal::NamedTupleLiteral.new,
Crystal::NilLiteral.new,
Crystal::NumberLiteral.new(42),
Crystal::RegexLiteral.new(Crystal::StringLiteral.new("foo")),
Crystal::RegexLiteral.new(Crystal::StringInterpolation.new([
Crystal::StringLiteral.new("foo"),
] of Crystal::ASTNode)),
Crystal::StringLiteral.new("foo"),
Crystal::StringInterpolation.new([
Crystal::StringLiteral.new("foo"),
] of Crystal::ASTNode),
Crystal::SymbolLiteral.new("foo"),
Crystal::TupleLiteral.new([] of Crystal::ASTNode),
Crystal::TupleLiteral.new([
Crystal::StringLiteral.new("foo"),
] of Crystal::ASTNode),
Crystal::RangeLiteral.new(
Crystal::NumberLiteral.new(0),
Crystal::NumberLiteral.new(10),
true),
].each do |literal|
it "properly identifies static node #{literal}" do
subject.static_literal?(literal).should be_true
subject.dynamic_literal?(literal).should be_false
end
end
[
Crystal::StringInterpolation.new([Crystal::Path.new(%w[Foo])] of Crystal::ASTNode),
Crystal::ArrayLiteral.new([Crystal::Path.new(%w[Foo])] of Crystal::ASTNode),
Crystal::TupleLiteral.new([Crystal::Path.new(%w[Foo])] of Crystal::ASTNode),
Crystal::RegexLiteral.new(Crystal::StringInterpolation.new([
Crystal::StringLiteral.new("foo"),
Crystal::Path.new(%w[Foo]),
] of Crystal::ASTNode)),
Crystal::RangeLiteral.new(
Crystal::Path.new(%w[Foo]),
Crystal::NumberLiteral.new(10),
true),
Crystal::RangeLiteral.new(
Crystal::NumberLiteral.new(10),
Crystal::Path.new(%w[Foo]),
true),
].each do |literal|
it "properly identifies dynamic node #{literal}" do
subject.dynamic_literal?(literal).should be_true
subject.static_literal?(literal).should be_false
end
end
end
describe "#node_source" do
it "returns original source of the node" do
s = <<-CRYSTAL
a = 1
CRYSTAL
node = Crystal::Parser.new(s).parse
source = subject.node_source node, s.split("\n")
source.should eq "a = 1"
end
it "returns original source of multiline node" do
s = <<-CRYSTAL
if ()
:ok
end
CRYSTAL
node = Crystal::Parser.new(s).parse
source = subject.node_source node, s.split("\n")
source.should eq <<-CRYSTAL
if ()
:ok
end
CRYSTAL
end
it "does not report source of node which has incorrect location" do
s = <<-CRYSTAL
module MyModule
macro conditional_error_for_inline_callbacks
{%
raise ""
%}
end
macro before_save(x = nil)
end
end
CRYSTAL
node = as_nodes(s).nil_literal_nodes.first
source = subject.node_source node, s.split("\n")
source.should eq "nil"
end
end
describe "#flow_command?" do
it "returns true if this is return" do
node = as_node("return 22")
subject.flow_command?(node, false).should be_true
end
it "returns true if this is a break in a loop" do
node = as_node("break")
subject.flow_command?(node, true).should be_true
end
it "returns false if this is a break out of loop" do
node = as_node("break")
subject.flow_command?(node, false).should be_false
end
it "returns true if this is a next in a loop" do
node = as_node("next")
subject.flow_command?(node, true).should be_true
end
it "returns false if this is a next out of loop" do
node = as_node("next")
subject.flow_command?(node, false).should be_false
end
it "returns true if this is raise" do
node = as_node("raise e")
subject.flow_command?(node, false).should be_true
end
it "returns true if this is exit" do
node = as_node("exit")
subject.flow_command?(node, false).should be_true
end
it "returns true if this is abort" do
node = as_node("abort")
subject.flow_command?(node, false).should be_true
end
it "returns false otherwise" do
node = as_node("foobar")
subject.flow_command?(node, false).should be_false
end
end
describe "#flow_expression?" do
it "returns true if this is a flow command" do
node = as_node("return")
subject.flow_expression?(node, true).should be_true
end
it "returns true if this is if-else consumed by flow expressions" do
node = as_node <<-CRYSTAL
if foo
return :foo
else
return :bar
end
CRYSTAL
subject.flow_expression?(node, false).should be_true
end
it "returns true if this is unless-else consumed by flow expressions" do
node = as_node <<-CRYSTAL
unless foo
return :foo
else
return :bar
end
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns true if this is case consumed by flow expressions" do
node = as_node <<-CRYSTAL
case
when 1
return 1
when 2
return 2
else
return 3
end
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns true if this is select consumed by flow expressions" do
node = as_node <<-CRYSTAL
select
when a = foo
return 1
when a = bar
return 2
else
return 3
end
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns true if this is exception handler consumed by flow expressions" do
node = as_node <<-CRYSTAL
begin
raise "exp"
rescue ex
return ex
end
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns true if this while consumed by flow expressions" do
node = as_node <<-CRYSTAL
while true
return
end
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns false if this while with break" do
node = as_node <<-CRYSTAL
while true
break
end
CRYSTAL
subject.flow_expression?(node).should be_false
end
it "returns true if this until consumed by flow expressions" do
node = as_node <<-CRYSTAL
until false
return
end
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns false if this until with break" do
node = as_node <<-CRYSTAL
until false
break
end
CRYSTAL
subject.flow_expression?(node).should be_false
end
it "returns true if this expressions consumed by flow expressions" do
node = as_node <<-CRYSTAL
exp1
exp2
return
CRYSTAL
subject.flow_expression?(node).should be_true
end
it "returns false otherwise" do
node = as_node <<-CRYSTAL
exp1
exp2
CRYSTAL
subject.flow_expression?(node).should be_false
end
end
describe "#suffix?" do
it "returns true if the node is a suffix `if`" do
node = as_node("foo if bar")
subject.suffix?(node).should be_true
end
it "returns true if the node is a suffix `if` (2)" do
node = as_node("foo if bar.end")
subject.suffix?(node).should be_true
end
it "returns true if the node is a suffix `unless`" do
node = as_node("foo unless bar")
subject.suffix?(node).should be_true
end
it "returns true if the node is a suffix `rescue`" do
node = as_node("foo rescue bar")
subject.suffix?(node).should be_true
end
it "returns true if the node is a suffix `ensure`" do
node = as_node("foo ensure bar")
subject.suffix?(node).should be_true
end
it "returns false if the node is not a suffix `if` or `unless`" do
node = as_node("foo")
subject.suffix?(node).should be_false
end
it "returns false if the node is a ternary `if`" do
node = as_node("foo ? bar : baz")
subject.suffix?(node).should be_false
end
it "returns false if the node is a non-suffix `if`" do
node = as_node("if foo; bar; end")
subject.suffix?(node).should be_false
end
it "returns false if the node is a non-suffix `if` (2)" do
node = as_node("if foo;bar;end")
subject.suffix?(node).should be_false
end
it "returns false if the node is a non-suffix `if` (3)" do
node = as_node <<-CRYSTAL
if foo
bar
end
CRYSTAL
subject.suffix?(node).should be_false
end
it "returns false if the node is a non-suffix `unless`" do
node = as_node("unless foo; bar; end")
subject.suffix?(node).should be_false
end
it "returns false if the node is a non-suffix `rescue`" do
node = as_node("begin; foo; rescue; end")
subject.suffix?(node).should be_false
end
it "returns false if the node is a non-suffix `ensure`" do
node = as_node("begin; foo; ensure; end")
subject.suffix?(node).should be_false
end
end
describe "#has_short_block?" do
it "returns true if the node has a short block variant" do
source = Source.new "foo :bar, &.baz"
node = as_node(source.code)
subject.has_short_block?(node, source.lines).should be_true
end
it "returns false if the node has a one line block" do
source = Source.new "foo :bar { |x| x.baz? }"
node = as_node(source.code)
subject.has_short_block?(node, source.lines).should be_false
end
it "returns false if the node does not have a short block variant" do
source = Source.new "foo :bar { |x| x.baz? }"
node = as_node(source.code)
subject.has_short_block?(node, source.lines).should be_false
end
it "returns false if the node does not have a block" do
source = Source.new "foo :bar"
node = as_node(source.code)
subject.has_short_block?(node, source.lines).should be_false
end
end
describe "#has_block?" do
it "returns true if the node has a block" do
node = as_node("%w[foo bar].first { :baz }")
subject.has_block?(node).should be_true
end
it "returns true if the node has a block (shorthand)" do
node = as_node("%w[foo bar].find(&.empty?)")
subject.has_block?(node).should be_true
end
it "returns true if the node has a block (block argument)" do
node = as_node("%w[foo bar].find(&block)")
subject.has_block?(node).should be_true
end
it "returns true if the node has a block (forwarded block argument)" do
node = as_node("%w[foo bar].find(&->foo)")
subject.has_block?(node).should be_true
end
it "returns false if the node does not have a block" do
node = as_node("%w[foo bar].first")
subject.has_block?(node).should be_false
end
end
describe "#has_arguments?" do
it "returns false if the node has no arguments" do
node = as_node("foo.bar")
subject.has_arguments?(node).should be_false
end
it "returns true if the node has positional arguments" do
node = as_node("foo.bar(1)")
subject.has_arguments?(node).should be_true
end
it "returns true if the node has named arguments" do
node = as_node("foo.bar(baz: 1)")
subject.has_arguments?(node).should be_true
end
it "returns true if the node has splat arguments" do
node = as_node("foo.bar(*baz)")
subject.has_arguments?(node).should be_true
end
it "returns true if the node has double splat arguments" do
node = as_node("foo.bar(**baz)")
subject.has_arguments?(node).should be_true
end
end
describe "#takes_arguments?" do
it "returns false if the node takes no arguments" do
node = as_node("def foo; end")
subject.takes_arguments?(node).should be_false
end
it "returns true if the node takes positional arguments" do
node = as_node("def foo(bar); end")
subject.takes_arguments?(node).should be_true
end
it "returns true if the node takes named arguments" do
node = as_node("def foo(*, bar); end")
subject.takes_arguments?(node).should be_true
end
it "returns true if the node has splat index" do
node = as_node("def foo(*args); end")
subject.takes_arguments?(node).should be_true
end
it "returns true if the node has double splat" do
node = as_node("def foo(**kwargs); end")
subject.takes_arguments?(node).should be_true
end
end
describe "#raise?" do
it "returns true if this is a raise method call" do
node = as_node "raise e"
subject.raise?(node).should be_true
end
it "returns false if it has a receiver" do
node = as_node "obj.raise e"
subject.raise?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "raise"
subject.raise?(node).should be_false
end
end
describe "#exit?" do
it "returns true if this is a exit method call" do
node = as_node "exit"
subject.exit?(node).should be_true
end
it "returns true if this is a exit method call with one argument" do
node = as_node "exit 1"
subject.exit?(node).should be_true
end
it "returns false if it has a receiver" do
node = as_node "obj.exit"
subject.exit?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "exit 1, 1"
subject.exit?(node).should be_false
end
end
describe "#abort?" do
it "returns true if this is an abort method call" do
node = as_node "abort"
subject.abort?(node).should be_true
end
it "returns true if this is an abort method call with one argument" do
node = as_node "abort \"message\""
subject.abort?(node).should be_true
end
it "returns true if this is an abort method call with two arguments" do
node = as_node "abort \"message\", 1"
subject.abort?(node).should be_true
end
it "returns false if it has a receiver" do
node = as_node "obj.abort"
subject.abort?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "abort 1, 1, 1"
subject.abort?(node).should be_false
end
end
describe "#loop?" do
it "returns true if this is a loop method call" do
node = as_node "loop"
subject.loop?(node).should be_true
end
it "returns false if it has a receiver" do
node = as_node "obj.loop"
subject.loop?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "loop 1"
subject.loop?(node).should be_false
end
end
describe "#operator_method_name?" do
it "returns true for operator method names" do
%w[+ - * / == != ~= !~ <=>].each do |op|
subject.operator_method_name?(op).should be_true
end
end
it "returns false for non-operator method names" do
%w[foo? foo! ->].each do |op|
subject.operator_method_name?(op).should be_false
end
end
end
describe "#operator_method?" do
it "returns true for operator method definitions" do
node = as_node "def +(other); end"
subject.operator_method?(node).should be_true
end
it "returns false for other method definitions" do
node = as_node "def method; end"
subject.operator_method?(node).should be_false
end
it "returns true for operator method calls" do
node = as_node "obj + 1"
subject.operator_method?(node).should be_true
end
it "returns false for other method calls" do
node = as_node "obj.method"
subject.operator_method?(node).should be_false
end
it "returns false for procs" do
node = as_node "-> { nil }"
subject.operator_method?(node).should be_false
end
end
describe "#setter_method_name?" do
it "returns true for setter method names" do
%w[foo= []=].each do |op|
subject.setter_method_name?(op).should be_true
end
end
it "returns false for operator method names" do
%w[== !=].each do |op|
subject.setter_method_name?(op).should be_false
end
end
it "returns false for non-operator method names" do
%w[foo foo? foo!].each do |op|
subject.setter_method_name?(op).should be_false
end
end
end
describe "#setter_method?" do
it "returns true for setter method definitions" do
node = as_node "def foo=(@foo); end"
subject.setter_method?(node).should be_true
end
it "returns false for other method definitions" do
node = as_node "def foo; end"
subject.setter_method?(node).should be_false
end
it "returns true for setter method calls" do
node = as_node "foo.bar = 123"
subject.setter_method?(node).should be_true
end
it "returns false for regular method calls" do
node = as_node "obj.method"
subject.setter_method?(node).should be_false
end
it "returns false for procs" do
node = as_node "-> { nil }"
subject.setter_method?(node).should be_false
end
end
describe "#nodoc?" do
it "returns true if a node has a single `:nodoc:` annotation" do
node = as_node <<-CRYSTAL, wants_doc: true
# :nodoc:
def foo; end
CRYSTAL
subject.nodoc?(node).should be_true
end
it "returns true if a node has a `:nodoc:` annotation in the first line" do
node = as_node <<-CRYSTAL, wants_doc: true
# :nodoc:
#
# foo
def foo; end
CRYSTAL
subject.nodoc?(node).should be_true
end
it "returns false if a node has a `:nodoc:` annotation in the middle" do
node = as_node <<-CRYSTAL, wants_doc: true
# foo
# :nodoc:
# bar
def foo; end
CRYSTAL
subject.nodoc?(node).should be_false
end
end
describe "#heredoc?" do
it "returns true if a node is a heredoc string" do
source = Source.new <<-CRYSTAL
<<-FOO
This is a heredoc
FOO
CRYSTAL
subject.heredoc?(source.ast, source).should be_true
end
it "returns true if a node is a heredoc string interpolation" do
source = Source.new <<-'CRYSTAL'
<<-FOO
This is a heredoc #{1 + 2}
FOO
CRYSTAL
subject.heredoc?(source.ast, source).should be_true
end
it "returns false if a node is a regular string interpolation" do
source = Source.new <<-'CRYSTAL'
"This is not a heredoc #{1 + 2}"
CRYSTAL
subject.heredoc?(source.ast, source).should be_false
end
end
describe "#control_exp_code" do
it "returns the exp code of a control expression" do
s = "return 1"
node = as_node(s).as Crystal::ControlExpression
exp_code = subject.control_exp_code node, [s]
exp_code.should eq "1"
end
it "wraps implicit tuple literal with curly brackets" do
s = "return 1, 2"
node = as_node(s).as Crystal::ControlExpression
exp_code = subject.control_exp_code node, [s]
exp_code.should eq "{1, 2}"
end
it "accepts explicit tuple literal" do
s = "return {1, 2}"
node = as_node(s).as Crystal::ControlExpression
exp_code = subject.control_exp_code node, [s]
exp_code.should eq "{1, 2}"
end
end
describe "#name_location_or" do
it "adjusts location column number by a given value" do
node = as_node("def foo; end").as Crystal::Def
subject.name_location_or(node, adjust_location_column_number: 10).to_s
.should eq "{:1:15, :1:17}"
end
it "works on method call" do
node = as_node("def foo; end").as Crystal::Def
subject.name_location_or(node).to_s.should eq "{:1:5, :1:7}"
end
it "works on class definition" do
node = as_node("class Foo; end").as Crystal::ClassDef
subject.name_location_or(node).to_s.should eq "{:1:7, :1:9}"
end
it "works on module definition" do
node = as_node("module Foo; end").as Crystal::ModuleDef
subject.name_location_or(node).to_s.should eq "{:1:8, :1:10}"
end
end
describe "#name_end_location" do
it "works on method call" do
node = as_node("name(foo)").as Crystal::Call
subject.name_end_location(node).to_s.should eq ":1:4"
end
it "works on method definition" do
node = as_node("def name; end").as Crystal::Def
subject.name_end_location(node).to_s.should eq ":1:8"
end
it "works on macro definition" do
node = as_node("macro name; end").as Crystal::Macro
subject.name_end_location(node).to_s.should eq ":1:10"
end
it "works on class definition" do
node = as_node("class Name; end").as Crystal::ClassDef
subject.name_end_location(node).to_s.should eq ":1:10"
end
it "works on module definition" do
node = as_node("module Name; end").as Crystal::ModuleDef
subject.name_end_location(node).to_s.should eq ":1:11"
end
it "works on annotation definition" do
node = as_node("annotation Name; end").as Crystal::AnnotationDef
subject.name_end_location(node).to_s.should eq ":1:15"
end
it "works on enum definition" do
node = as_node("enum Name; end").as Crystal::EnumDef
subject.name_end_location(node).to_s.should eq ":1:9"
end
it "works on alias definition" do
node = as_node("alias Name = Foo").as Crystal::Alias
subject.name_end_location(node).to_s.should eq ":1:10"
end
it "works on generic" do
node = as_node("Name(Foo)").as Crystal::Generic
subject.name_end_location(node).to_s.should eq ":1:4"
end
it "works on include" do
node = as_node("include Name").as Crystal::Include
subject.name_end_location(node).to_s.should eq ":1:12"
end
it "works on extend" do
node = as_node("extend Name").as Crystal::Extend
subject.name_end_location(node).to_s.should eq ":1:11"
end
it "works on variable type declaration" do
node = as_node("name : Foo").as Crystal::TypeDeclaration
subject.name_end_location(node).to_s.should eq ":1:4"
end
it "works on uninitialized variable" do
node = as_node("name = uninitialized Foo").as Crystal::UninitializedVar
subject.name_end_location(node).to_s.should eq ":1:4"
end
it "works on lib definition" do
node = as_node("lib Name; end").as Crystal::LibDef
subject.name_end_location(node).to_s.should eq ":1:8"
end
it "works on lib type definition" do
node = as_node("lib Foo; type Name = Bar; end").as(Crystal::LibDef).body
node.class.should eq Crystal::TypeDef
subject.name_end_location(node).to_s.should eq ":1:18"
end
it "works on metaclass" do
node = as_node("foo : Name.class").as(Crystal::TypeDeclaration).declared_type
node.class.should eq Crystal::Metaclass
subject.name_end_location(node).to_s.should eq ":1:10"
end
end
end
end
================================================
FILE: spec/ameba/ast/variabling/argument_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe Argument do
arg = Crystal::Arg.new "a"
scope = Scope.new as_node "foo = 1"
variable = Variable.new(Crystal::Var.new("foo"), scope)
describe "#initialize" do
it "creates a new argument" do
argument = Argument.new(arg, variable)
argument.node.should_not be_nil
end
end
describe "delegation" do
it "delegates locations to node" do
argument = Argument.new(arg, variable)
argument.location.should eq arg.location
argument.end_location.should eq arg.end_location
end
it "delegates to_s to node" do
argument = Argument.new(arg, variable)
argument.to_s.should eq arg.to_s
end
end
describe "#ignored?" do
it "is true if arg starts with _" do
argument = Argument.new(Crystal::Arg.new("_a"), variable)
argument.ignored?.should be_true
end
it "is false otherwise" do
argument = Argument.new(arg, variable)
argument.ignored?.should be_false
end
end
end
end
================================================
FILE: spec/ameba/ast/variabling/assignment_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe Assignment do
node = Crystal::NilLiteral.new
scope = Scope.new as_node "foo = 1"
variable = Variable.new(Crystal::Var.new("foo"), scope)
describe "#initialize" do
it "creates a new assignment with node and var" do
assignment = Assignment.new(node, variable, scope)
assignment.node.should_not be_nil
end
end
describe "delegation" do
it "delegates locations" do
assignment = Assignment.new(node, variable, scope)
assignment.location.should eq node.location
assignment.end_location.should eq node.end_location
end
it "delegates to_s" do
assignment = Assignment.new(node, variable, scope)
assignment.to_s.should eq node.to_s
end
it "delegates scope" do
assignment = Assignment.new(node, variable, scope)
assignment.scope.should eq variable.scope
end
end
end
end
================================================
FILE: spec/ameba/ast/variabling/reference_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe Reference do
it "is derived from a Variable" do
node = Crystal::Var.new "foo"
ref = Reference.new(node, Scope.new as_node "foo = 1")
ref.should be_a Variable
end
end
end
================================================
FILE: spec/ameba/ast/variabling/type_dec_variable_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe TypeDecVariable do
var = Crystal::Var.new("foo")
declared_type = Crystal::Path.new("String")
type_dec = Crystal::TypeDeclaration.new(var, declared_type)
describe "#initialize" do
it "creates a new type dec variable" do
variable = TypeDecVariable.new(type_dec)
variable.node.should_not be_nil
end
end
describe "#name" do
it "returns var name" do
variable = TypeDecVariable.new(type_dec)
variable.name.should eq var.name
end
it "raises if type declaration is incorrect" do
type_dec = Crystal::TypeDeclaration.new(declared_type, declared_type)
expect_raises(Exception, "Unsupported var node type: Crystal::Path") do
TypeDecVariable.new(type_dec).name
end
end
end
end
end
================================================
FILE: spec/ameba/ast/variabling/variable_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe Variable do
var_node = Crystal::Var.new("foo")
scope = Scope.new as_node "foo = 1"
describe "#initialize" do
it "creates a new variable" do
variable = Variable.new(var_node, scope)
variable.node.should_not be_nil
end
end
describe "delegation" do
it "delegates locations" do
variable = Variable.new(var_node, scope)
variable.location.should eq var_node.location
variable.end_location.should eq var_node.end_location
end
it "delegates name" do
variable = Variable.new(var_node, scope)
variable.name.should eq var_node.name
end
it "delegates to_s" do
variable = Variable.new(var_node, scope)
variable.to_s.should eq var_node.to_s
end
end
describe "#special?" do
it "returns truthy if it is a special `$?` var" do
variable = Variable.new Crystal::Var.new("$?"), scope
variable.special?.should be_truthy
end
it "returns falsey otherwise" do
variable = Variable.new Crystal::Var.new("a"), scope
variable.special?.should be_falsey
end
end
describe "#assign" do
assign_node = as_node("foo=1")
it "assigns the variable (creates a new assignment)" do
variable = Variable.new(var_node, scope)
variable.assign(assign_node, scope)
variable.assignments.empty?.should be_false
end
it "can create multiple assignments" do
variable = Variable.new(var_node, scope)
variable.assign(assign_node, scope)
variable.assign(assign_node, scope)
variable.assignments.size.should eq 2
end
end
describe "#reference" do
it "references the existed assignment" do
variable = Variable.new(var_node, scope)
variable.assign(as_node("foo=1"), scope)
variable.reference(var_node, scope)
variable.references.empty?.should be_false
end
it "adds a reference to the scope" do
scope = Scope.new as_node "foo = 1"
variable = Variable.new(var_node, scope)
variable.assign(as_node("foo=1"), scope)
variable.reference(var_node, scope)
scope.references.size.should eq 1
scope.references.first.node.to_s.should eq "foo"
end
end
describe "#captured_by_block?" do
it "returns truthy if the variable is captured by block" do
nodes = as_nodes <<-CRYSTAL
def method
a = 2
3.times { |i| a = a + i }
end
CRYSTAL
var_node = nodes.var_nodes.first
scope = Scope.new(nodes.def_nodes.first)
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
variable.reference(nodes.var_nodes.last, scope.inner_scopes.last)
variable.captured_by_block?.should be_truthy
end
it "returns falsy if the variable is not captured by the block" do
scope = Scope.new as_node <<-CRYSTAL
def method
a = 1
end
CRYSTAL
scope.add_variable(Crystal::Var.new "a")
variable = scope.variables.first
variable.captured_by_block?.should be_falsey
end
end
describe "#target_of?" do
it "returns true if the variable is a target of Crystal::Assign node" do
assign_node = as_nodes("foo=1").assign_nodes.last
variable = Variable.new assign_node.target.as(Crystal::Var), scope
variable.target_of?(assign_node).should be_true
end
it "returns true if the variable is a target of Crystal::OpAssign node" do
assign_node = as_nodes("foo=1;foo+=1").op_assign_nodes.last
variable = Variable.new assign_node.target.as(Crystal::Var), scope
variable.target_of?(assign_node).should be_true
end
it "returns true if the variable is a target of Crystal::MultiAssign node" do
assign_node = as_nodes("a,b,c={1,2,3}").multi_assign_nodes.last
assign_node.targets.size.should_not eq 0
assign_node.targets.each do |target|
variable = Variable.new target.as(Crystal::Var), scope
variable.target_of?(assign_node).should be_true
end
end
it "returns false if the node is not assign" do
variable = Variable.new(Crystal::Var.new("v"), scope)
variable.target_of?(as_node "nil").should be_false
end
it "returns false if the variable is not a target of the assign" do
variable = Variable.new(Crystal::Var.new("foo"), scope)
variable.target_of?(as_node("bar = 1")).should be_false
end
end
describe "#ignored?" do
it "is true if variable is ignored" do
variable = Variable.new(Crystal::Var.new("_var"), scope)
variable.ignored?.should be_true
end
it "is false if variable is not ignored" do
variable = Variable.new(Crystal::Var.new("v_ar"), scope)
variable.ignored?.should be_false
end
it "is true if variable is a black hole" do
variable = Variable.new(Crystal::Var.new("_"), scope)
variable.ignored?.should be_true
end
end
describe "#eql?" do
var = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 1, 2))
variable = Variable.new var, scope
it "is false if node is not a Crystal::Var" do
variable.eql?(as_node("nil")).should be_false
end
it "is false if node name is different" do
variable.eql?(Crystal::Var.new "bar").should be_false
end
it "is false if node has a different location" do
variable.eql?(Crystal::Var.new "foo").should be_false
end
it "is true otherwise" do
variable.eql?(variable.node).should be_true
end
end
describe "#declared_before?" do
it "is falsey if variable doesn't have location" do
var1 = Crystal::Var.new("foo")
var2 = Crystal::Var.new("bar").at(Crystal::Location.new(nil, 1, 2))
Variable.new(var1, scope).declared_before?(var2).should be_falsey
end
it "is falsey if node doesn't have location" do
var1 = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 1, 2))
var2 = Crystal::Var.new("bar")
Variable.new(var1, scope).declared_before?(var2).should be_falsey
end
it "is true if var's line_number below the node" do
var1 = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 1, 2))
var2 = Crystal::Var.new("bar").at(Crystal::Location.new(nil, 2, 2))
Variable.new(var1, scope).declared_before?(var2).should be_true
end
it "is true if var's column_number is after the node" do
var1 = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 1, 2))
var2 = Crystal::Var.new("bar").at(Crystal::Location.new(nil, 1, 3))
Variable.new(var1, scope).declared_before?(var2).should be_true
end
it "is false if var's location is before the node" do
var1 = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 2, 2))
var2 = Crystal::Var.new("bar").at(Crystal::Location.new(nil, 1, 3))
Variable.new(var1, scope).declared_before?(var2).should be_false
end
it "is false is the node is the same var" do
var = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 2, 2))
Variable.new(var, scope).declared_before?(var).should be_false
end
end
end
end
================================================
FILE: spec/ameba/ast/visitors/counting_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe CountingVisitor do
describe "#visit" do
it "allow to visit ASTNode" do
node = Crystal::Parser.new("").parse
visitor = CountingVisitor.new node
node.accept visitor
end
end
describe "#count" do
it "is 1 for an empty method" do
node = Crystal::Parser.new("def hello; end").parse
visitor = CountingVisitor.new node
visitor.count.should eq 1
end
it "is 1 if there is Macro::For" do
code = <<-CRYSTAL
def initialize
{% for c in ALL_NODES %}
true || false
{% end %}
end
CRYSTAL
node = Crystal::Parser.new(code).parse
visitor = CountingVisitor.new node
visitor.count.should eq 1
end
it "is 1 if there is Macro::If" do
code = <<-CRYSTAL
def initialize
{% if foo.bar? %}
true || false
{% end %}
end
CRYSTAL
node = Crystal::Parser.new(code).parse
visitor = CountingVisitor.new node
visitor.count.should eq 1
end
it "increases count for every exhaustive case" do
code = <<-CRYSTAL
def hello(a : Int32 | Int64 | Float32 | Float64)
case a
in Int32 then "int32"
in Int64 then "int64"
in Float32 then "float32"
in Float64 then "float64"
end
end
CRYSTAL
node = Crystal::Parser.new(code).parse
visitor = CountingVisitor.new node
visitor.count.should eq 2
end
{% for pair in [
{code: "if true; end", description: "conditional"},
{code: "while true; end", description: "while loop"},
{code: "until 1 < 2; end", description: "until loop"},
{code: "begin; rescue; end", description: "rescue"},
{code: "true || false", description: "or"},
{code: "true && false", description: "and"},
{
code: "a : String | Int32 = 1; case a when true; end",
description: "inexhaustive when",
},
{
code: "select; when a = foo; end",
description: "select",
},
] %}
it "increases count for every {{ pair[:description].id }}" do
node = Crystal::Parser.new("def hello; {{ pair[:code].id }} end").parse
visitor = CountingVisitor.new node
visitor.count.should eq 2
end
{% end %}
end
end
end
================================================
FILE: spec/ameba/ast/visitors/elseif_aware_node_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe ElseIfAwareNodeVisitor do
rule = ElseIfRule.new
subject = ElseIfAwareNodeVisitor.new rule, Source.new <<-CRYSTAL
# rule.ifs[0]
foo ? bar : baz
def foo
# rule.ifs[2]
if :one
1
elsif :two
2
elsif :three
3
else
%w[].each do
# rule.ifs[1]
if true
'a'
elsif false
'b'
else
'c'
end
end
end
end
CRYSTAL
it "inherits a logic from `NodeVisitor`" do
subject.should be_a(NodeVisitor)
end
it "fires a callback for every `if` node, excluding `elsif` branches" do
rule.ifs.size.should eq 3
end
it "fires a callback with an array containing an `if` node without an `elsif` branches" do
if_node, ifs = rule.ifs[0]
if_node.to_s.should eq "foo ? bar : baz"
ifs.should be_nil
end
it "fires a callback with an array containing an `if` node with multiple `elsif` branches" do
if_node, ifs = rule.ifs[2]
if_node.cond.to_s.should eq ":one"
ifs = ifs.should_not be_nil
ifs.size.should eq 3
ifs.first.should be if_node
ifs.map(&.then.to_s).should eq %w[1 2 3]
end
it "fires a callback with an array containing an `if` node with the `else` branch as the last item" do
if_node, ifs = rule.ifs[1]
if_node.cond.to_s.should eq "true"
ifs = ifs.should_not be_nil
ifs.size.should eq 2
ifs.first.should be if_node
ifs.map(&.then.to_s).should eq %w['a' 'b']
ifs.last.else.to_s.should eq %('c')
end
end
end
================================================
FILE: spec/ameba/ast/visitors/flow_expression_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe FlowExpressionVisitor do
it "creates an expression for return" do
rule = FlowExpressionRule.new
FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL
def foo
return :bar
end
CRYSTAL
rule.expressions.size.should eq 1
end
it "can create multiple expressions" do
rule = FlowExpressionRule.new
FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL
def foo
if bar
return :baz
else
return :foobar
end
end
CRYSTAL
rule.expressions.size.should eq 3
end
it "properly creates nested flow expressions" do
rule = FlowExpressionRule.new
FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL
def foo
return (
loop do
break if a > 1
return a
end
)
end
CRYSTAL
rule.expressions.size.should eq 4
end
it "creates an expression for break" do
rule = FlowExpressionRule.new
FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL
while true
break
end
CRYSTAL
rule.expressions.size.should eq 1
end
it "creates an expression for next" do
rule = FlowExpressionRule.new
FlowExpressionVisitor.new rule, Source.new <<-CRYSTAL
while true
next if something
end
CRYSTAL
rule.expressions.size.should eq 1
end
end
end
================================================
FILE: spec/ameba/ast/visitors/implicit_return_visitor_spec.cr
================================================
require "../../../spec_helper"
private def implicit_return_visit(code)
Ameba::ImplicitReturnRule.new.tap do |rule|
Ameba::AST::ImplicitReturnVisitor.new rule, Ameba::Source.new(code)
end
end
private def has_unused_expression?(rule, str)
rule.unused_expressions.any?(&.to_s.== str)
end
private def has_unused_expression?(rule, node_type : Crystal::ASTNode.class)
rule.unused_expressions.any?(node_type)
end
private def has_unused_call?(rule, name)
rule.unused_expressions.any? do |node|
node.is_a?(Crystal::Call) && node.name == name
end
end
module Ameba::AST
describe ImplicitReturnVisitor do
context "Crystal::Expressions" do
it "reports all non-last expressions as unused" do
rule = implicit_return_visit(<<-CRYSTAL)
def method
foo
bar
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_false
end
it "reports non-last expression even when parent captures result" do
rule = implicit_return_visit(<<-CRYSTAL)
x = begin
foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "x").should be_false
end
it "stops processing after control expressions" do
rule = implicit_return_visit(<<-CRYSTAL)
foo
return bar
baz
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "baz").should be_false
end
it "handles nested expressions correctly" do
rule = implicit_return_visit(<<-CRYSTAL)
begin
foo
begin
bar
baz
end
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
end
context "assignments" do
it "marks assigned value as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
foo
x = bar
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "x").should be_false
end
it "reports assignments when not captured" do
rule = implicit_return_visit(<<-CRYSTAL)
def method
x = 1
y = 2
end
CRYSTAL
assigns = rule.unused_expressions.select(Crystal::Assign)
assigns.size.should eq 1
assigns.first.to_s.should start_with "x ="
end
it "marks op-assigned values as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
x = 0
x += foo
bar
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks multi-assigned values as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
a, b = foo, bar
baz
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
end
context "Crystal::Call" do
it "marks all call arguments as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
foo(bar, baz)
qux
CRYSTAL
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "baz").should be_false
end
it "reports the call itself when not captured" do
rule = implicit_return_visit(<<-CRYSTAL)
foo(1)
bar
CRYSTAL
has_unused_call?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
it "handles method calls with blocks" do
rule = implicit_return_visit(<<-CRYSTAL)
foo.map { |x| x + 1 }
bar
CRYSTAL
has_unused_call?(rule, "map").should be_true
has_unused_expression?(rule, "bar").should be_true
end
end
context "Crystal::If and Crystal::Unless" do
it "marks condition as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
if foo
bar
else
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_true
end
it "reports if statement when not captured" do
rule = implicit_return_visit(<<-CRYSTAL)
if true
1
end
2
CRYSTAL
has_unused_expression?(rule, Crystal::If).should be_true
has_unused_expression?(rule, "1").should be_true
has_unused_expression?(rule, "2").should be_true
end
it "captures last line of branches when if is captured" do
rule = implicit_return_visit(<<-CRYSTAL)
x = if foo
bar
baz
else
qux
end
CRYSTAL
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_false
has_unused_expression?(rule, "qux").should be_false
end
end
context "Crystal::While and Crystal::Until" do
it "marks condition as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
while foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_true
end
it "does not capture loop body by default" do
rule = implicit_return_visit(<<-CRYSTAL)
while true
foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
end
context "Crystal::Case" do
it "marks case condition as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
case foo
when 1
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks when conditions as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
case x
when foo, bar
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "inherits parent capture state for when bodies" do
rule = implicit_return_visit(<<-CRYSTAL)
result = case x
when 1
foo
bar
when 2
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
end
end
context "Crystal::Def" do
it "marks method arguments as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
def method(x = foo)
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "captures method body last line by default" do
rule = implicit_return_visit(<<-CRYSTAL)
def method
foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
end
it "does not capture body when return type is Nil" do
rule = implicit_return_visit(<<-CRYSTAL)
def method : Nil
foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
it "does not capture body in initialize methods" do
rule = implicit_return_visit(<<-CRYSTAL)
class Foo
def initialize
foo
bar
end
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
it "handles method with complex body" do
rule = implicit_return_visit(<<-CRYSTAL)
def outer
foo
if condition
bar
end
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_false
end
end
context "literals" do
it "marks array elements as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
[foo, bar]
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "marks hash values as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
{a: foo, b: bar}
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "marks range bounds as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
foo..bar
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "marks string interpolation expressions as captured" do
rule = implicit_return_visit(<<-'CRYSTAL')
"hello #{foo} world"
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks regex contents as captured" do
rule = implicit_return_visit(<<-'CRYSTAL')
/#{foo}/
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
end
context "Crystal::BinaryOp" do
it "marks left side as captured when right is a Call" do
rule = implicit_return_visit(<<-CRYSTAL)
foo + bar()
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks left side as captured when right is ControlExpression" do
rule = implicit_return_visit(<<-CRYSTAL)
foo || return bar
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "does not mark left side as captured for simple ops" do
rule = implicit_return_visit(<<-CRYSTAL)
foo + bar
CRYSTAL
has_unused_expression?(rule, "foo + bar").should be_true
end
end
context "type operations" do
it "marks casted object as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
foo.as(Bar)
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks tested object as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
foo.is_a?(Bar)
baz.responds_to?(:method)
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "baz").should be_false
end
it "marks typeof argument as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
typeof(foo)
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks declared value as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
x : Int32 = foo
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
end
context "Crystal::ExceptionHandler" do
it "does not capture body last line when else is present" do
rule = implicit_return_visit(<<-CRYSTAL)
begin
foo
bar
rescue
baz
else
qux
end
CRYSTAL
has_unused_expression?(rule, "bar").should be_true
end
it "captures body last line when no else is present" do
rule = implicit_return_visit(<<-CRYSTAL)
x = begin
foo
bar
rescue
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "baz").should be_false
end
it "does not capture ensure block" do
rule = implicit_return_visit(<<-CRYSTAL)
begin
foo
ensure
bar
baz
end
CRYSTAL
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_true
end
it "inherits capture state for rescue bodies" do
rule = implicit_return_visit(<<-CRYSTAL)
x = begin
foo
rescue
bar
baz
end
CRYSTAL
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_false
end
end
context "Crystal::Block" do
it "inherits parent capture state for block body" do
rule = implicit_return_visit(<<-CRYSTAL)
[1, 2].map do |x|
foo
x + 1
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
end
it "processes block when block itself is unused" do
rule = implicit_return_visit(<<-CRYSTAL)
3.times do
foo
bar
end
baz
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "baz").should be_true
end
end
context "Crystal::ControlExpression" do
it "marks control expression arguments as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
return foo
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "reports the control expression itself" do
rule = implicit_return_visit(<<-CRYSTAL)
def method : Nil
return 1
end
CRYSTAL
has_unused_expression?(rule, Crystal::Return).should be_true
end
it "handles break with value" do
rule = implicit_return_visit(<<-CRYSTAL)
loop do
break foo if condition
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "handles next with value" do
rule = implicit_return_visit(<<-CRYSTAL)
[1, 2].each do |x|
next foo if x == 1
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
end
context "macros" do
it "marks macro arguments as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
macro method(arg = foo)
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "captures macro body" do
rule = implicit_return_visit(<<-CRYSTAL)
macro method
foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "sets in_macro flag for macro body" do
rule = implicit_return_visit(<<-CRYSTAL)
macro method
{% foo %}
end
CRYSTAL
# ameba:disable Performance/AnyInsteadOfPresent
rule.macro_flags.any?.should be_true
has_unused_expression?(rule, "foo").should be_true
end
it "captures output macro expressions" do
rule = implicit_return_visit(<<-CRYSTAL)
{{ foo }}
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "does not capture non-output macro expressions" do
rule = implicit_return_visit(<<-CRYSTAL)
{% foo %}
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
end
it "sets in_macro flag for macro expression" do
rule = implicit_return_visit(<<-CRYSTAL)
{% foo %}
CRYSTAL
# ameba:disable Performance/AnyInsteadOfPresent
rule.macro_flags.any?.should be_true
end
it "marks macro if condition as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
{% if foo %}
{% end %}
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "does not capture macro if branches" do
rule = implicit_return_visit(<<-CRYSTAL)
{% if true %}
foo
{% else %}
bar
{% end %}
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "does not capture macro for body" do
rule = implicit_return_visit(<<-CRYSTAL)
{% for x in [1, 2] %}
foo
{% end %}
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
end
context "other node types" do
it "visits enum members" do
rule = implicit_return_visit(<<-CRYSTAL)
enum Color
Red
Green
Blue
end
CRYSTAL
has_unused_expression?(rule, "Red").should be_false
has_unused_expression?(rule, "Green").should be_false
has_unused_expression?(rule, "Blue").should be_false
end
it "visits class and module bodies" do
rule = implicit_return_visit(<<-CRYSTAL)
class Foo
foo
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
it "marks function body as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
fun foo : Int32
return 1
end
CRYSTAL
has_unused_expression?(rule, Crystal::FunDef).should be_true
end
it "marks unary operand as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
!foo
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "!foo").should be_true
end
it "marks yielded values as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
yield foo, bar
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "marks default argument value as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
def method(x = foo)
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
end
it "marks annotation arguments as captured" do
rule = implicit_return_visit(<<-CRYSTAL)
@[Foo(bar)]
def method
end
CRYSTAL
has_unused_expression?(rule, "bar").should be_false
end
it "visits select whens" do
rule = implicit_return_visit(<<-CRYSTAL)
select
when x = foo
bar
end
CRYSTAL
has_unused_expression?(rule, Crystal::Select).should be_true
has_unused_expression?(rule, "bar").should be_true
end
end
context "edge cases" do
it "handles deeply nested scopes" do
rule = implicit_return_visit(<<-CRYSTAL)
class Outer
class Inner
def method
if true
foo
bar
end
end
end
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_false
end
it "handles control expressions in method body" do
rule = implicit_return_visit(<<-CRYSTAL)
def method
return foo if condition
bar
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
end
it "handles multiple assignment targets" do
rule = implicit_return_visit(<<-CRYSTAL)
a, b, c = foo, bar, baz
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "baz").should be_false
end
it "handles nested exception handlers" do
rule = implicit_return_visit(<<-CRYSTAL)
begin
begin
foo
rescue
bar
end
rescue
baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
has_unused_expression?(rule, "baz").should be_true
end
it "handles case with multiple when conditions" do
rule = implicit_return_visit(<<-CRYSTAL)
case x
when foo, bar, baz
qux
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_false
has_unused_expression?(rule, "bar").should be_false
has_unused_expression?(rule, "baz").should be_false
end
it "handles trailing control expressions" do
rule = implicit_return_visit(<<-CRYSTAL)
begin
foo
bar
return baz
end
CRYSTAL
has_unused_expression?(rule, "foo").should be_true
has_unused_expression?(rule, "bar").should be_true
end
end
context "integration scenarios" do
it "handles complex method with multiple features" do
rule = implicit_return_visit(<<-CRYSTAL)
def process(items)
return nil if items.empty?
result = items.map do |item|
if item.valid?
item.process
else
item.skip
end
end
log(result)
result
end
CRYSTAL
has_unused_call?(rule, "log").should be_true
end
it "handles initialize with side effects" do
rule = implicit_return_visit(<<-CRYSTAL)
class Foo
def initialize(@x : Int32)
validate!
setup_hooks
end
end
CRYSTAL
has_unused_call?(rule, "validate!").should be_true
has_unused_call?(rule, "setup_hooks").should be_true
end
end
end
end
================================================
FILE: spec/ameba/ast/visitors/node_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe NodeVisitor do
describe "visit" do
it "allow to visit ASTNode" do
rule = DummyRule.new
visitor = NodeVisitor.new rule, Source.new
nodes = Crystal::Parser.new("").parse
nodes.accept visitor
end
end
end
end
================================================
FILE: spec/ameba/ast/visitors/redundant_control_expression_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe RedundantControlExpressionVisitor do
source = Source.new
rule = RedundantControlExpressionRule.new
node = as_node <<-CRYSTAL
a = 1
b = 2
return a + b
CRYSTAL
subject = RedundantControlExpressionVisitor.new(rule, source, node)
it "assigns valid attributes" do
subject.rule.should eq rule
subject.source.should eq source
subject.node.should eq node
end
it "fires a callback with a valid node" do
rule.nodes.size.should eq 1
rule.nodes.first.to_s.should eq "return a + b"
end
end
end
================================================
FILE: spec/ameba/ast/visitors/scope_calls_with_self_receiver_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe ScopeCallsWithSelfReceiverVisitor do
{% for type in %w[class module].map(&.id) %}
it "creates a scope for the {{ type }} def" do
rule = SelfCallsRule.new
visitor = ScopeCallsWithSelfReceiverVisitor.new rule, Source.new <<-CRYSTAL
{{ type }} Foo
self.foo
end
CRYSTAL
call_queue = visitor.scope_call_queue
call_queue.size.should eq 1
call_queue.should eq rule.call_queue
scope, calls = call_queue.first
node = scope.node.should be_a(Crystal::{{ type.capitalize }}Def)
node.name.should be_a(Crystal::Path)
node.name.to_s.should eq "Foo"
calls.size.should eq 1
calls.first.name.should eq "foo"
end
{% end %}
it "creates a scope for the def" do
rule = SelfCallsRule.new
visitor = ScopeCallsWithSelfReceiverVisitor.new rule, Source.new <<-CRYSTAL
class Foo
def method
self.foo :a, :b, :c
self.bar
baz :x, :y, :z
it_is_a.bat "country"
self
end
end
CRYSTAL
call_queue = visitor.scope_call_queue
call_queue.size.should eq 1
call_queue.should eq rule.call_queue
scope, calls = call_queue.first
node = scope.node.should be_a(Crystal::Def)
node.name.should eq "method"
calls.size.should eq 2
calls.first.name.should eq "foo"
calls.last.name.should eq "bar"
end
it "creates a scope for the proc" do
rule = SelfCallsRule.new
visitor = ScopeCallsWithSelfReceiverVisitor.new rule, Source.new <<-CRYSTAL
-> { self.foo }
CRYSTAL
call_queue = visitor.scope_call_queue
call_queue.size.should eq 1
call_queue.should eq rule.call_queue
end
it "creates a scope for the block" do
rule = SelfCallsRule.new
visitor = ScopeCallsWithSelfReceiverVisitor.new rule, Source.new <<-CRYSTAL
3.times { self.foo }
CRYSTAL
call_queue = visitor.scope_call_queue
call_queue.size.should eq 1
call_queue.should eq rule.call_queue
end
context "inner scopes" do
it "creates scope for block inside def" do
rule = SelfCallsRule.new
visitor = ScopeCallsWithSelfReceiverVisitor.new rule, Source.new <<-CRYSTAL
def method
self.foo
3.times { self.bar }
end
CRYSTAL
call_queue = visitor.scope_call_queue
call_queue.size.should eq 2
call_queue.should eq rule.call_queue
call_queue.last_key.outer_scope.should eq call_queue.first_key
end
it "creates scope for block inside block" do
rule = SelfCallsRule.new
visitor = ScopeCallsWithSelfReceiverVisitor.new rule, Source.new <<-CRYSTAL
def method
3.times do
self.bar
2.times { self.baz }
end
end
CRYSTAL
call_queue = visitor.scope_call_queue
call_queue.size.should eq 2
call_queue.should eq rule.call_queue
call_queue.last_key.outer_scope.should eq call_queue.first_key
end
end
end
end
================================================
FILE: spec/ameba/ast/visitors/scope_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe ScopeVisitor do
{% for type in %w[class module enum].map(&.id) %}
it "creates a scope for the {{ type }} def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
{{ type }} Foo
end
CRYSTAL
rule.scopes.size.should eq 1
end
{% end %}
it "creates a scope for the def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
def method
end
CRYSTAL
rule.scopes.size.should eq 1
end
it "creates a scope for the proc" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
-> {}
CRYSTAL
rule.scopes.size.should eq 1
end
it "creates a scope for the block" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
3.times {}
CRYSTAL
rule.scopes.size.should eq 2
end
it "correctly resets location-less node scope" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
class Foo # Scope #1
def foo? # Scope #2
{% begin %} # Scope #3
# `Crystal::MacroFor` doesn't have location information
{% for method in @type.ancestors %} # Scope #4
{% end %}
{% end %}
end
end
CRYSTAL
rule.scopes.size.should eq 4
end
context "inner scopes" do
it "creates scope for block inside def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
def method
3.times {}
end
CRYSTAL
rule.scopes.size.should eq 2
rule.scopes.last.outer_scope.should_not be_nil
rule.scopes.first.outer_scope.should eq rule.scopes.last
end
it "creates scope for block inside block" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
3.times do
2.times {}
end
CRYSTAL
rule.scopes.size.should eq 3
inner_block = rule.scopes.first
outer_block = rule.scopes.last
inner_block.outer_scope.should_not eq outer_block
outer_block.outer_scope.should be_nil
end
end
context "#visibility" do
it "is being properly set" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
private class Foo
end
CRYSTAL
rule.scopes.size.should eq 1
rule.scopes.first.visibility.should eq Crystal::Visibility::Private
end
it "is being inherited from the outer scope(s)" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
private class Foo
class Bar
def baz
end
end
end
CRYSTAL
rule.scopes.size.should eq 3
rule.scopes.each &.visibility.should eq Crystal::Visibility::Private
rule.scopes.last.node.visibility.should eq Crystal::Visibility::Private
rule.scopes[0...-1].each &.node.visibility.should eq Crystal::Visibility::Public
end
end
end
end
================================================
FILE: spec/ameba/ast/visitors/top_level_nodes_visitor_spec.cr
================================================
require "../../../spec_helper"
module Ameba::AST
describe TopLevelNodesVisitor do
describe "#require_nodes" do
it "returns require node" do
source = Source.new <<-CRYSTAL
require "foo"
def bar
end
CRYSTAL
visitor = TopLevelNodesVisitor.new(source.ast)
visitor.require_nodes.size.should eq 1
visitor.require_nodes.first.to_s.should eq %q(require "foo")
end
end
end
end
================================================
FILE: spec/ameba/base_spec.cr
================================================
require "../spec_helper"
module Ameba::Rule
root = Path[__DIR__, "..", ".."].expand
describe Base do
context ".rules" do
it "returns a list of all rules" do
rules = Rule.rules
rules.should_not be_nil
rules.should contain DummyRule
end
it "contains rules across all the available groups" do
Rule.rules.map(&.group_name).uniq!.reject!(&.empty?).sort.should eq %w[
Ameba
Documentation
Layout
Lint
Metrics
Naming
Performance
Style
Typing
]
end
end
context "properties" do
subject = DummyRule.new
it "is enabled by default" do
subject.enabled?.should be_true
end
it "has a description property" do
subject.description.should_not be_nil
end
it "has a dummy? property" do
subject.dummy?.should be_true
end
it "has excluded property" do
subject.excluded.should be_nil
end
end
describe "#excluded?" do
it "returns false if a rule does no have a list of excluded source" do
DummyRule.new.excluded?(Source.new path: "source.cr").should_not be_true
end
it "returns false if source is not excluded from this rule" do
rule = DummyRule.new
rule.excluded = Set{"some_source.cr"}
rule.excluded?(Source.new path: "another_source.cr").should_not be_true
end
it "returns true if source is excluded from this rule" do
rule = DummyRule.new
rule.excluded = Set{"source.cr"}
rule.excluded?(Source.new path: "source.cr").should be_true
end
it "returns true if source matches the wildcard" do
rule = DummyRule.new
rule.excluded = Set{"**/*.cr"}
rule.excluded?(Source.new(path: __FILE__), root).should be_true
end
it "returns false if source does not match the wildcard" do
rule = DummyRule.new
rule.excluded = Set{"*_spec.cr"}
rule.excluded?(Source.new path: "source.cr").should be_false
end
end
describe ".documentation_url" do
it "returns the parsed rule documentation URL" do
Lint::Syntax.documentation_url.should eq \
"https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/Syntax.html"
end
end
describe ".parsed_doc" do
it "returns the parsed rule doc" do
DummyRule.parsed_doc.should eq "Dummy Rule which does nothing."
end
end
describe "#==" do
it "returns true if rule has the same name" do
DummyRule.new.should eq(DummyRule.new)
end
it "returns false if rule has a different name" do
DummyRule.new.should_not eq(ScopeRule.new)
end
end
end
end
================================================
FILE: spec/ameba/cli/cmd_spec.cr
================================================
require "../../spec_helper"
require "../../../src/ameba/cli/cmd"
module Ameba::CLI
root = Path[__DIR__, "..", "..", "fixtures"].expand
describe "Cmd" do
describe ".run" do
it "runs ameba" do
r = CLI.run %w[-f silent file.cr]
r.should be_true
end
end
describe ".parse_args" do
%w[-s --silent].each do |flag|
it "accepts #{flag} flag" do
opts = CLI.parse_args [flag]
opts.formatter.should eq :silent
end
end
%w[-c --config].each do |flag|
it "accepts #{flag} flag" do
opts = CLI.parse_args [flag, "config.yml"]
opts.config.should eq Path["config.yml"]
end
end
%w[-f --format].each do |flag|
it "accepts #{flag} flag" do
opts = CLI.parse_args [flag, "my-formatter"]
opts.formatter.should eq "my-formatter"
end
end
%w[-u --up-to-version].each do |flag|
it "accepts #{flag} flag" do
opts = CLI.parse_args [flag, "1.5.0"]
opts.version.should eq "1.5.0"
end
end
it "accepts --stdin-filename flag" do
opts = CLI.parse_args %w[--stdin-filename foo.cr]
opts.stdin_filename.should eq "foo.cr"
end
it "accepts --only flag" do
opts = CLI.parse_args ["--only", "RULE1,RULE2"]
opts.only.should eq Set{"RULE1", "RULE2"}
end
it "accepts --except flag" do
opts = CLI.parse_args ["--except", "RULE1,RULE2"]
opts.except.should eq Set{"RULE1", "RULE2"}
end
it "defaults rules? flag to false" do
opts = CLI.parse_args %w[file.cr]
opts.rules?.should be_false
end
it "defaults skip_reading_config? flag to false" do
opts = CLI.parse_args %w[file.cr]
opts.skip_reading_config?.should be_false
end
it "accepts --rules flag" do
opts = CLI.parse_args %w[--rules]
opts.rules?.should be_true
end
it "defaults all? flag to false" do
opts = CLI.parse_args %w[file.cr]
opts.all?.should be_false
end
it "accepts --all flag" do
opts = CLI.parse_args %w[--all]
opts.all?.should be_true
end
it "accepts --gen-config flag" do
opts = CLI.parse_args %w[--gen-config]
opts.formatter.should eq :todo
end
it "accepts --no-color flag" do
opts = CLI.parse_args %w[--no-color]
opts.colors?.should be_false
end
it "accepts --without-affected-code flag" do
opts = CLI.parse_args %w[--without-affected-code]
opts.without_affected_code?.should be_true
end
it "doesn't disable colors by default" do
opts = CLI.parse_args %w[--all]
opts.colors?.should be_true
end
it "ignores --config if --gen-config flag passed" do
opts = CLI.parse_args %w[--gen-config --config my_config.yml]
opts.formatter.should eq :todo
opts.skip_reading_config?.should be_true
end
describe "-e/--explain" do
it "configures file/line/column" do
opts = CLI.parse_args %w[--explain src/file.cr:3:5]
location_to_explain = opts.location_to_explain.should_not be_nil
location_to_explain.filename.should eq "src/file.cr"
location_to_explain.line_number.should eq 3
location_to_explain.column_number.should eq 5
end
it "raises an error if location is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
CLI.parse_args %w[--explain src/file.cr:3]
end
end
it "raises an error if line number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
CLI.parse_args %w[--explain src/file.cr:a:3]
end
end
it "raises an error if column number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
CLI.parse_args %w[--explain src/file.cr:3:&]
end
end
it "raises an error if line/column are missing" do
expect_raises(Exception, "location should have PATH:line:column") do
CLI.parse_args %w[--explain src/file.cr]
end
end
end
context "--min-severity" do
it "configures fail level Convention" do
opts = CLI.parse_args %w[--min-severity convention]
opts.severity.should eq Severity::Convention
end
it "configures fail level Warning" do
opts = CLI.parse_args %w[--min-severity Warning]
opts.severity.should eq Severity::Warning
end
it "configures fail level Error" do
opts = CLI.parse_args %w[--min-severity error]
opts.severity.should eq Severity::Error
end
it "raises if fail level is incorrect" do
expect_raises(Exception, "Incorrect severity name JohnDoe") do
CLI.parse_args %w[--min-severity JohnDoe]
end
end
end
it "sets #root to the first directory passed as an argument if it's a project directory" do
opts = CLI.parse_args [root.to_s]
opts.root.should eq root
end
it "sets #root to the current directory if the given directory is not a project directory" do
opts = CLI.parse_args [Dir.tempdir]
opts.root.should eq Path[Dir.current]
end
it "sets #root to the current directory if no project directory is passed" do
opts = CLI.parse_args %w[]
opts.root.should eq Path[Dir.current]
end
it "accepts unknown args as globs" do
opts = CLI.parse_args %w[source1.cr source2.cr]
opts.globs.should eq Set{
Path["source1.cr"].expand.to_posix.to_s,
Path["source2.cr"].expand.to_posix.to_s,
}
end
it "leaves the absolute paths intact" do
opts = CLI.parse_args [
Path[Dir.tempdir, "foo.cr"].to_s,
Path[Dir.tempdir, "bar.cr"].to_s,
Path[Dir.tempdir, "baz*.cr"].to_s,
]
opts.root.should eq Path[Dir.current]
opts.globs.should eq Set{
Path[Dir.tempdir, "foo.cr"].to_posix.to_s,
Path[Dir.tempdir, "bar.cr"].to_posix.to_s,
Path[Dir.tempdir, "baz*.cr"].to_posix.to_s,
}
end
it "expands relative globs using current directory as base" do
opts = CLI.parse_args [
Path[Dir.tempdir, "foo.cr"].to_s,
"**/bar.cr",
]
opts.root.should eq Path[Dir.current]
opts.globs.should eq Set{
Path[Dir.tempdir, "foo.cr"].to_posix.to_s,
Path[Dir.current, "**", "bar.cr"].to_posix.to_s,
}
end
it "expands relative globs using project root directory as base" do
opts = CLI.parse_args [
root.to_s,
Path[Dir.tempdir, "foo.cr"].to_s,
"**/bar.cr",
]
opts.globs.should eq Set{
root.to_posix.to_s,
Path[Dir.tempdir, "foo.cr"].to_posix.to_s,
Path[Dir.current, "**", "bar.cr"].to_posix.to_s,
}
end
it "accepts single '-' argument as STDIN" do
opts = CLI.parse_args %w[-]
opts.stdin_filename.should eq "-"
end
it "accepts one unknown arg as explain location if it has correct format" do
opts = CLI.parse_args %w[source.cr:3:22]
location_to_explain = opts.location_to_explain.should_not be_nil
location_to_explain.filename.should eq "source.cr"
location_to_explain.line_number.should eq 3
location_to_explain.column_number.should eq 22
end
it "allows args to be blank" do
opts = CLI.parse_args [] of String
opts.root.should eq Path[Dir.current]
opts.formatter.should be_nil
opts.globs.should be_nil
opts.only.should be_nil
opts.except.should be_nil
opts.config.should be_nil
end
end
end
end
================================================
FILE: spec/ameba/config_spec.cr
================================================
require "../spec_helper"
module Ameba
root = Path[__DIR__, "..", ".."].expand
describe Config do
config_sample = Path[__DIR__, "..", "fixtures", ".ameba.yml"].to_s
it "should have a list of available formatters" do
Config::AVAILABLE_FORMATTERS.should_not be_nil
end
describe ".new" do
it "raises when the config is not a Hash" do
yaml = YAML.parse "[]"
expect_raises(Exception, "Invalid config file format") do
Config.from_yaml(yaml)
end
end
it "loads default globs when config is empty" do
yaml = YAML.parse "{}"
config = Config.from_yaml(yaml)
config.globs.should eq Config::DEFAULT_GLOBS
end
it "loads default globs when config has no value" do
yaml = YAML.parse <<-YAML
# Empty config with comment
YAML
config = Config.from_yaml(yaml)
config.globs.should eq Config::DEFAULT_GLOBS
end
it "initializes globs as string" do
yaml = YAML.parse <<-YAML
---
Globs: src/*.cr
YAML
config = Config.from_yaml(yaml)
config.globs.should eq Set{"src/*.cr"}
end
it "initializes globs as array" do
yaml = YAML.parse <<-YAML
---
Globs:
- "src/*.cr"
- "!spec"
YAML
config = Config.from_yaml(yaml)
config.globs.should eq Set{"src/*.cr", "!spec"}
end
it "raises if Globs has a wrong type" do
yaml = YAML.parse <<-YAML
---
Globs: 100
YAML
expect_raises(Exception, "Incorrect `Globs` section in a config file") do
Config.from_yaml(yaml)
end
end
it "initializes excluded as string" do
yaml = YAML.parse <<-YAML
---
Excluded: spec
YAML
config = Config.from_yaml(yaml)
config.excluded.should eq Set{"spec"}
end
it "initializes excluded as array" do
yaml = YAML.parse <<-YAML
---
Excluded:
- spec
- lib/*.cr
YAML
config = Config.from_yaml(yaml)
config.excluded.should eq Set{"spec", "lib/*.cr"}
end
it "raises if Excluded has a wrong type" do
yaml = YAML.parse <<-YAML
---
Excluded: true
YAML
expect_raises(Exception, "Incorrect `Excluded` section in a config file") do
Config.from_yaml(yaml)
end
end
end
describe ".load" do
it "loads custom config" do
config = Config.load config_sample
config.should_not be_nil
config.version.should_not be_nil
config.globs.should_not be_nil
config.formatter.should be_a Formatter::FlycheckFormatter
end
it "raises when custom config file doesn't exist" do
expect_raises(Exception, %(Unable to load config file: Config file "foo.yml" does not exist)) do
Config.load "foo.yml"
end
end
it "loads default config" do
config = Config.load
config.should_not be_nil
config.version.should be_nil
config.globs.should_not be_nil
config.formatter.should_not be_nil
end
end
describe "#globs, #globs=" do
config = Config.load config_sample
it "holds source globs" do
config.globs.should eq Config::DEFAULT_GLOBS
end
it "allows to set globs" do
config.globs = Set{"src/**/*.cr"}
config.globs.should eq Set{"src/**/*.cr"}
end
end
describe "#excluded, #excluded=" do
config = Config.load config_sample
it "defaults to `lib`" do
config.excluded.should eq Set{"lib"}
end
it "allows to set excluded" do
config.excluded = Set{"spec"}
config.excluded.should eq Set{"spec"}
end
end
describe "#sources" do
config = Config.load config_sample, root: root
it "returns list of sources" do
config.sources.size.should be > 0
config.sources.first.should be_a Source
config.sources.any?(&.fullpath.==(__FILE__)).should be_true
end
it "returns a list of sources matching globs" do
config.globs = Set{"**/config_spec.cr"}
config.sources.size.should eq(1)
end
it "returns a list of sources excluding 'Excluded'" do
config.excluded = Set{"**/config_spec.cr"}
config.sources.any?(&.fullpath.==(__FILE__)).should be_false
end
end
describe "#formatter, formatter=" do
config = Config.load config_sample
formatter = DummyFormatter.new
it "contains default formatter" do
config.formatter.should_not be_nil
end
it "allows to set formatter" do
config.formatter = formatter
config.formatter.should eq formatter
end
it "allows to set formatter using a name" do
config.formatter = :progress
config.formatter.should_not be_nil
end
it "raises an error if not available formatter is set" do
expect_raises(Exception) do
config.formatter = :no_such_formatter
end
end
end
describe "#version, version=" do
config = Config.load config_sample
version = SemanticVersion.parse("1.5.0")
it "contains default version" do
config.version.should_not be_nil
end
it "allows to set version" do
config.version = version
config.version.should eq version
end
it "allows to set version using a string" do
config.version = version.to_s
config.version.should eq version
end
it "raises an error if version is not valid" do
expect_raises(Exception) do
config.version = "foo"
end
end
end
describe "#update_rule" do
config = Config.load config_sample
it "updates enabled property" do
name = DummyRule.rule_name
config.update_rule name, enabled: false
rule = config.rules.find!(&.name.== name)
rule.enabled?.should be_false
end
it "updates excluded property" do
name = DummyRule.rule_name
excluded = Set{"spec/source.cr"}
config.update_rule name, excluded: excluded
rule = config.rules.find!(&.name.== name)
rule.excluded.should eq excluded
end
end
describe "#update_rules" do
config = Config.load config_sample
it "updates multiple rules by enabled property" do
name = DummyRule.rule_name
config.update_rules [name], enabled: false
rule = config.rules.find!(&.name.== name)
rule.enabled?.should be_false
end
it "updates multiple rules by excluded property" do
name = DummyRule.rule_name
excluded = Set{"spec/source.cr"}
config.update_rules [name], excluded: excluded
rule = config.rules.find!(&.name.== name)
rule.excluded.should eq excluded
end
it "updates a group of rules by enabled property" do
group = DummyRule.group_name
config.update_rules [group], enabled: false
rule = config.rules.find!(&.name.== DummyRule.rule_name)
rule.enabled?.should be_false
end
it "updates a group by excluded property" do
name = DummyRule.group_name
excluded = Set{"spec/source.cr"}
config.update_rules [name], excluded: excluded
rule = config.rules.find!(&.name.== DummyRule.rule_name)
rule.excluded.should eq excluded
end
end
end
end
================================================
FILE: spec/ameba/ext/location_spec.cr
================================================
require "../../spec_helper"
describe Crystal::Location do
subject = Crystal::Location.new(nil, 2, 3)
describe "#with" do
it "changes line number" do
subject.with(line_number: 1).to_s.should eq ":1:3"
end
it "changes column number" do
subject.with(column_number: 1).to_s.should eq ":2:1"
end
it "changes line and column numbers" do
subject.with(line_number: 1, column_number: 2).to_s.should eq ":1:2"
end
end
describe "#adjust" do
it "adjusts line number" do
subject.adjust(line_number: 1).to_s.should eq ":3:3"
end
it "adjusts column number" do
subject.adjust(column_number: 1).to_s.should eq ":2:4"
end
it "adjusts line and column numbers" do
subject.adjust(line_number: 1, column_number: 2).to_s.should eq ":3:5"
end
end
describe "#seek" do
it "adjusts column number if line offset is 1" do
subject.seek(Crystal::Location.new(nil, 1, 2)).to_s.should eq ":2:4"
end
it "adjusts line number and changes column number if line offset is greater than 1" do
subject.seek(Crystal::Location.new(nil, 2, 1)).to_s.should eq ":3:1"
end
it "adjusts line number and changes column number if line offset is less than 1" do
subject.seek(Crystal::Location.new(nil, 0, 1)).to_s.should eq ":1:1"
end
it "raises exception if filenames don't match" do
expect_raises(ArgumentError, "Mismatching filenames:\n source.cr\n source2.cr") do
location = Crystal::Location.new("source.cr", 1, 1)
location.seek(Crystal::Location.new("source2.cr", 1, 1))
end
end
end
end
================================================
FILE: spec/ameba/formatter/disabled_formatter_spec.cr
================================================
require "../../spec_helper"
module Ameba::Formatter
describe DisabledFormatter do
output = IO::Memory.new
subject = DisabledFormatter.new output
before_each do
output.clear
end
describe "#finished" do
it "writes a final message" do
subject.finished [Source.new]
output.to_s.should contain "Disabled rules using inline directives:"
end
it "writes disabled rules if any" do
path = "source.cr"
source = Source.new(path: path)
source.add_issue(ErrorRule.new, {1, 2}, message: "ErrorRule", status: :disabled)
source.add_issue(NamedRule.new, location: {2, 2}, message: "NamedRule", status: :disabled)
subject.finished [source]
log = output.to_s
log = Util.deansify(log).should_not be_nil
log.should contain "#{path}:1 #{ErrorRule.rule_name}"
log.should contain "#{path}:2 #{NamedRule.rule_name}"
end
it "does not write not-disabled rules" do
source = Source.new(path: "source.cr")
source.add_issue(ErrorRule.new, {1, 2}, "ErrorRule")
source.add_issue(NamedRule.new, location: {2, 2},
message: "NamedRule", status: :disabled)
subject.finished [source]
output.to_s.should_not contain ErrorRule.rule_name
end
end
end
end
================================================
FILE: spec/ameba/formatter/dot_formatter_spec.cr
================================================
require "../../spec_helper"
module Ameba::Formatter
describe DotFormatter do
output = IO::Memory.new
subject = DotFormatter.new output
before_each do
output.clear
end
describe "#started" do
it "writes started message" do
subject.started [Source.new]
output.to_s.should eq "Inspecting 1 file\n\n"
end
end
describe "#source_finished" do
it "writes valid source" do
subject.source_finished Source.new
output.to_s.should contain "."
end
it "writes invalid source" do
source = Source.new
source.add_issue DummyRule.new, Crystal::Nop.new, "message"
subject.source_finished source
output.to_s.should contain "F"
end
end
describe "#finished" do
it "writes a final message" do
subject.finished [Source.new]
output.to_s.should contain "1 inspected, 0 failures"
end
it "writes the elapsed time" do
subject.finished [Source.new]
output.to_s.should contain "Finished in"
end
context "when issues found" do
it "writes each issue" do
source = Source.new
source.add_issue(DummyRule.new, {1, 1}, "DummyRuleError")
source.add_issue(NamedRule.new, {1, 2}, "NamedRuleError")
subject.finished [source]
log = output.to_s
log.should contain "1 inspected, 2 failures"
log.should contain "DummyRuleError"
log.should contain "NamedRuleError"
end
it "writes affected code by default" do
source = Source.new <<-CRYSTAL
a = 22
puts a
CRYSTAL
source.add_issue(DummyRule.new, {1, 5}, "DummyRuleError")
subject.finished [source]
log = output.to_s
log = subject.deansify(log).should_not be_nil
log.should contain "> a = 22"
log.should contain " ^"
end
it "writes severity" do
source = Source.new <<-CRYSTAL
a = 22
puts a
CRYSTAL
source.add_issue(DummyRule.new, {1, 5}, "DummyRuleError")
subject.finished [source]
log = output.to_s
log.should contain "[C]"
end
it "doesn't write affected code if it is disabled" do
source = Source.new <<-CRYSTAL
a = 22
puts a
CRYSTAL
source.add_issue(DummyRule.new, {1, 5}, "DummyRuleError")
formatter = DotFormatter.new output
formatter.config[:without_affected_code] = true
formatter.finished [source]
log = output.to_s
log = formatter.deansify(log).should_not be_nil
log.should_not contain "> a = 22"
log.should_not contain " ^"
end
it "does not write disabled issues" do
source = Source.new
source.add_issue(DummyRule.new, {1, 1}, "DummyRuleError", status: :disabled)
source.add_issue(NamedRule.new, {1, 2}, "NamedRuleError")
subject.finished [source]
log = output.to_s
log.should_not contain "DummyRuleError"
log.should contain "1 inspected, 1 failure"
end
end
end
end
end
================================================
FILE: spec/ameba/formatter/explain_formatter_spec.cr
================================================
require "../../spec_helper"
private LOCATION = Crystal::Location.new("source.cr", 1, 1)
private def explanation(source)
Ameba::ErrorRule.new.catch(source)
output = IO::Memory.new
Ameba::Formatter::ExplainFormatter.new(output, LOCATION).finished([source])
output.to_s
end
module Ameba::Formatter
describe ExplainFormatter do
describe "#location" do
it "returns crystal location" do
location = ExplainFormatter
.new(STDOUT, LOCATION)
.location
location.should eq LOCATION
end
end
describe "#output" do
it "returns io" do
output = ExplainFormatter
.new(STDOUT, LOCATION)
.output
output.should eq STDOUT
end
end
describe "#finished" do
it "writes issue info" do
source = Source.new "a = 42", "source.cr"
output = explanation(source)
output.should contain "ISSUE INFO"
output.should contain "This rule always adds an error"
output.should contain "source.cr:1:1"
end
it "writes affected code" do
source = Source.new "a = 42", "source.cr"
output = explanation(source)
output.should contain "AFFECTED CODE"
output.should contain "a = 42"
end
it "writes rule info" do
source = Source.new "a = 42", "source.cr"
output = explanation(source)
output.should contain "RULE INFO"
output.should contain "Convention"
output.should contain "Ameba/ErrorRule"
output.should contain "Always adds an error"
end
it "writes detailed description" do
source = Source.new "a = 42", "source.cr"
output = explanation(source)
output.should contain "DETAILED DESCRIPTION"
output.should contain "Rule extended description"
end
it "writes nothing if location not found" do
source = Source.new "a = 42", "another_source.cr"
explanation(source).should be_empty
end
end
end
end
================================================
FILE: spec/ameba/formatter/flycheck_formatter_spec.cr
================================================
require "../../spec_helper"
module Ameba::Formatter
describe FlycheckFormatter do
output = IO::Memory.new
subject = FlycheckFormatter.new output
before_each do
output.clear
end
context "problems not found" do
it "reports nothing" do
subject.source_finished Source.new
subject.output.to_s.empty?.should be_true
end
end
context "when problems found" do
it "reports an issue" do
source = Source.new "a = 1", "source.cr"
source.add_issue DummyRule.new, {1, 2}, "message"
subject.source_finished(source)
subject.output.to_s.should eq(
"source.cr:1:2: C: [#{DummyRule.rule_name}] message\n"
)
end
it "properly reports multi-line message" do
source = Source.new "a = 1", "source.cr"
source.add_issue DummyRule.new, {1, 2}, "multi\nline"
subject.source_finished(source)
subject.output.to_s.should eq(
"source.cr:1:2: C: [#{DummyRule.rule_name}] multi line\n"
)
end
it "reports nothing if location was not set" do
source = Source.new "a = 1", "source.cr"
source.add_issue DummyRule.new, Crystal::Nop.new, "message"
subject.source_finished(source)
subject.output.to_s.should be_empty
end
end
end
end
================================================
FILE: spec/ameba/formatter/github_actions_formatter_spec.cr
================================================
require "../../spec_helper"
module Ameba::Formatter
describe GitHubActionsFormatter do
output = IO::Memory.new
subject = GitHubActionsFormatter.new(output)
before_each do
output.clear
end
describe "#source_finished" do
it "writes valid source" do
source = Source.new path: "/path/to/file.cr"
subject.source_finished(source)
output.to_s.should be_empty
end
it "writes invalid source" do
source = Source.new path: "/path/to/file.cr"
location = Crystal::Location.new("/path/to/file.cr", 1, 2)
source.add_issue DummyRule.new, location, location, "message\n2nd line"
subject.source_finished(source)
output.to_s.should eq(
"::notice file=/path/to/file.cr,line=1,col=2,endLine=1,endColumn=2," \
"title=Ameba/DummyRule::message%0A2nd line\n"
)
end
end
describe "#finished" do
it "doesn't do anything if 'GITHUB_STEP_SUMMARY' ENV var is not set" do
subject.finished [Source.new]
output.to_s.should be_empty
end
it "writes a Markdown summary to a filename given in 'GITHUB_STEP_SUMMARY' ENV var" do
prev_summary = ENV["GITHUB_STEP_SUMMARY"]?
ENV["GITHUB_STEP_SUMMARY"] = summary_filename = File.tempname
begin
sources = [Source.new]
subject.started(sources)
subject.finished(sources)
File.exists?(summary_filename).should be_true
summary = File.read(summary_filename)
summary.should contain "## Ameba Results :green_heart:"
summary.should contain "Finished in"
summary.should contain "**1** sources inspected, **0** failures."
summary.should contain "> Ameba version: **#{Ameba.version}**"
ensure
ENV["GITHUB_STEP_SUMMARY"] = prev_summary
File.delete(summary_filename) rescue nil
end
end
context "when issues found" do
it "writes each issue" do
prev_summary = ENV["GITHUB_STEP_SUMMARY"]?
ENV["GITHUB_STEP_SUMMARY"] = summary_filename = File.tempname
repo = ENV["GITHUB_REPOSITORY"]?
sha = ENV["GITHUB_SHA"]?
begin
source = Source.new path: "src/source.cr"
source.add_issue(DummyRule.new, {1, 1}, {2, 1}, "DummyRuleError")
source.add_issue(DummyRule.new, {1, 1}, "DummyRuleError 2|3")
source.add_issue(NamedRule.new, {1, 2}, "NamedRuleError", status: :disabled)
subject.finished([source])
File.exists?(summary_filename).should be_true
summary = File.read(summary_filename)
summary.should contain "## Ameba Results :bug:"
summary.should contain "### Issues found"
summary.should contain "#### `src/source.cr` (**2** issues)"
linked_name =
"[#{DummyRule.rule_name}](#{DummyRule.documentation_url})"
if repo && sha
summary.should contain(
"| [1-2](https://github.com/#{repo}/blob/#{sha}/src/source.cr#L1-L2) " \
"| Convention | #{linked_name} | DummyRuleError |"
)
summary.should contain(
"| [1](https://github.com/#{repo}/blob/#{sha}/src/source.cr#L1) " \
"| Convention | #{linked_name} | DummyRuleError 2\\|3 |"
)
else
summary.should contain "| 1-2 | Convention | #{linked_name} | DummyRuleError |"
summary.should contain "| 1 | Convention | #{linked_name} | DummyRuleError 2\\|3 |"
end
summary.should_not contain "NamedRuleError"
summary.should contain "**1** sources inspected, **2** failures."
ensure
ENV["GITHUB_STEP_SUMMARY"] = prev_summary
File.delete(summary_filename) rescue nil
end
end
end
end
end
end
================================================
FILE: spec/ameba/formatter/json_formatter_spec.cr
================================================
require "../../spec_helper"
private def get_result(sources = [Ameba::Source.new])
output = IO::Memory.new
formatter = Ameba::Formatter::JSONFormatter.new output
formatter.started sources
sources.each { |source| formatter.source_finished source }
formatter.finished sources
JSON.parse(output.to_s)
end
module Ameba::Formatter
describe JSONFormatter do
context "metadata" do
it "shows ameba version" do
get_result["metadata"]["ameba_version"].should eq Ameba.version.to_s
end
it "shows crystal version" do
get_result["metadata"]["crystal_version"].should eq Crystal::VERSION
end
end
context "sources" do
it "doesn't add empty sources" do
result = get_result [Source.new path: "source.cr"]
result["sources"].as_a.should be_empty
end
it "shows path to the source" do
source = Source.new path: "source.cr"
source.add_issue DummyRule.new, {1, 2}, "message"
result = get_result [source]
result["sources"][0]["path"].should eq "source.cr"
end
it "shows rule name" do
source = Source.new
source.add_issue DummyRule.new, {1, 2}, "message"
result = get_result [source]
result["sources"][0]["issues"][0]["rule_name"].should eq DummyRule.rule_name
end
it "shows severity" do
source = Source.new
source.add_issue DummyRule.new, {1, 2}, "message"
result = get_result [source]
result["sources"][0]["issues"][0]["severity"].should eq "Convention"
end
it "shows a message" do
source = Source.new
source.add_issue DummyRule.new, {1, 2}, "message"
result = get_result [source]
result["sources"][0]["issues"][0]["message"].should eq "message"
end
it "shows issue location" do
source = Source.new
source.add_issue DummyRule.new, {1, 2}, "message"
result = get_result [source]
location = result["sources"][0]["issues"][0]["location"]
location["line"].should eq 1
location["column"].should eq 2
end
it "shows issue end_location" do
source = Source.new
source.add_issue DummyRule.new,
Crystal::Location.new("path", 3, 3),
Crystal::Location.new("path", 5, 4),
"message"
result = get_result [source]
end_location = result["sources"][0]["issues"][0]["end_location"]
end_location["line"].should eq 5
end_location["column"].should eq 4
end
end
context "summary" do
it "shows a target sources count" do
result = get_result [Source.new, Source.new]
result["summary"]["target_sources_count"].should eq 2
end
it "shows issues count" do
s1 = Source.new
s1.add_issue DummyRule.new, {1, 2}, "message1"
s1.add_issue DummyRule.new, {1, 2}, "message2"
s2 = Source.new
s2.add_issue DummyRule.new, {1, 2}, "message3"
result = get_result [s1, s2]
result["summary"]["issues_count"].should eq 3
end
end
end
end
================================================
FILE: spec/ameba/formatter/todo_formatter_spec.cr
================================================
require "../../spec_helper"
require "file_utils"
private CONFIG_PATH =
Path[Dir.tempdir] / Ameba::Config::Loader::FILENAME
module Ameba
private def with_formatter(&)
io = IO::Memory.new
formatter = Formatter::TODOFormatter.new(io, CONFIG_PATH)
yield formatter, io
end
private def create_todo
with_formatter do |formatter|
source = Source.new "a = 1", "source.cr"
source.add_issue DummyRule.new, {1, 2}, "message"
formatter.finished([source])
File.exists?(CONFIG_PATH) ? File.read(CONFIG_PATH) : ""
end
end
describe Formatter::TODOFormatter do
::Spec.after_each do
FileUtils.rm_rf(CONFIG_PATH)
end
context "problems not found" do
it "does not create file" do
with_formatter do |formatter|
file = formatter.finished [Source.new]
file.should be_nil
end
end
it "reports a message saying file is not created" do
with_formatter do |formatter, io|
formatter.finished [Source.new]
io.to_s.should contain "No issues found. File is not generated"
end
end
end
context "problems found" do
it "prints a message saying file is created" do
with_formatter do |formatter, io|
source = Source.new "a = 1", "source.cr"
source.add_issue DummyRule.new, {1, 2}, "message"
formatter.finished([source])
io.to_s.should contain "Created #{CONFIG_PATH}"
end
end
it "creates a valid YAML document" do
YAML.parse(create_todo).should_not be_nil
end
it "creates a todo with header" do
create_todo.should contain "# This configuration file was generated by"
end
it "creates a todo with UTC time" do
create_todo.should match /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/
end
it "creates a todo with version" do
create_todo.should contain "Ameba version #{Ameba.version}"
end
it "creates a todo with a rule name" do
create_todo.should contain "DummyRule"
end
it "excludes source from this rule" do
create_todo.should contain "Excluded:\n - source.cr"
end
context "with multiple issues" do
it "does generate todo file" do
with_formatter do |formatter|
s1 = Source.new "a = 1", "source1.cr"
s2 = Source.new "a = 1", "source2.cr"
s1.add_issue DummyRule.new, {1, 2}, "message1"
s1.add_issue NamedRule.new, {1, 2}, "message1"
s1.add_issue DummyRule.new, {2, 2}, "message1"
s2.add_issue DummyRule.new, {2, 2}, "message2"
formatter.finished([s1, s2])
content = File.read(CONFIG_PATH)
content.should contain <<-YAML
Ameba/DummyRule:
Excluded:
- source1.cr
- source2.cr
YAML
end
end
end
it "output format" do
with_formatter do |formatter|
s1 = Source.new(path: "source1.cr")
s2 = Source.new(path: "source2.cr")
s1.add_issue NamedRule.new, {1, 1}, ""
s2.add_issue DummyRule.new, {2, 2}, ""
s1.add_issue DummyRule.new, {3, 3}, ""
formatter.finished([s1, s2])
content = File.read(CONFIG_PATH)
content.should contain <<-YAML
Ameba/DummyRule:
Excluded:
- source1.cr
- source2.cr
BreakingRule:
Excluded:
- source1.cr
YAML
end
end
it "converts paths to posix variant" do
with_formatter do |formatter|
s1 = Source.new(path: Path["foo", "bar", "source1.cr"].to_s)
s2 = Source.new(path: Path["foo", "bar", "source2.cr"].to_s)
s1.add_issue NamedRule.new, {1, 1}, ""
s2.add_issue DummyRule.new, {2, 2}, ""
s1.add_issue DummyRule.new, {3, 3}, ""
formatter.finished([s1, s2])
content = File.read(CONFIG_PATH)
content.should contain <<-YAML
Ameba/DummyRule:
Excluded:
- foo/bar/source1.cr
- foo/bar/source2.cr
BreakingRule:
Excluded:
- foo/bar/source1.cr
YAML
end
end
context "when invalid syntax" do
it "does generate todo file" do
with_formatter do |formatter|
source = Source.new "def invalid_syntax"
source.add_issue Rule::Lint::Syntax.new, {1, 2}, "message"
file = formatter.finished [source]
file.should be_nil
end
end
it "prints an error message" do
with_formatter do |formatter, io|
source = Source.new "def invalid_syntax"
source.add_issue Rule::Lint::Syntax.new, {1, 2}, "message"
formatter.finished [source]
io.to_s.should contain "Unable to generate TODO file"
io.to_s.should contain "Please fix syntax issues"
end
end
end
end
end
end
================================================
FILE: spec/ameba/formatter/util_spec.cr
================================================
require "../../spec_helper"
module Ameba::Formatter
class Subject
include Util
end
describe Util do
subject = Subject.new
describe "#colorize_text_styles" do
it "underlines headings" do
1.upto(6) do |i|
string = "#{("#" * i)} foo"
subject.colorize_text_styles(string)
.should eq string.colorize.underline.to_s
end
end
it "applies italic" do
%w[* _].each do |char|
string = "%1$s|foo|%1$s" % char
subject.colorize_text_styles(string)
.should eq string.colorize.italic.to_s
end
end
it "applies bold" do
%w[* _].each do |char|
string = "%1$s|foo|%1$s" % (char * 2)
subject.colorize_text_styles(string)
.should eq string.colorize.bold.to_s
end
end
it "applies strikethrough" do
string = "~~foo~~"
subject.colorize_text_styles(string)
.should eq string.colorize.strikethrough.to_s
end
it "combines styles" do
subject.colorize_text_styles("~~*__foo__*~~")
.should eq "~~#{"*#{"__foo__".colorize.bold}*".colorize.italic}~~"
.colorize.strikethrough.to_s
end
end
describe "#colorize_code_fences" do
it "highlights multiline code blocks" do
code_string = "```\nfoo\nbar\nbaz\n```"
string = "foo\n\n%s\n\nbar"
subject.colorize_code_fences(string % code_string, :red)
.should eq (string % code_string.colorize.red).to_s
end
it "highlights inline code blocks" do
code_string = "`foo bar baz`"
string = "foo %s bar"
subject.colorize_code_fences(string % code_string, :red)
.should eq (string % code_string.colorize.red).to_s
end
end
describe "#deansify" do
it "returns given string without ANSI codes" do
str = String.build do |io|
io << "foo".colorize.green.underline
io << '-'
io << "bar".colorize.red.underline
end
subject.deansify("foo-bar").should eq "foo-bar"
subject.deansify(str).should eq "foo-bar"
end
end
describe "#trim" do
it "trims string longer than :max_length" do
subject.trim(("+" * 300), 1).should eq "+"
subject.trim(("+" * 300), 3).should eq "+++"
subject.trim(("+" * 300), 5).should eq "+ ..."
subject.trim(("+" * 300), 7).should eq "+++ ..."
end
it "leaves intact string shorter than :max_length" do
subject.trim(("+" * 3), 100).should eq "+++"
end
it "allows to use custom ellipsis" do
subject.trim(("+" * 300), 3, "…").should eq "++…"
end
end
describe "#context" do
it "returns correct pre/post context lines" do
source = Source.new <<-CRYSTAL
# pre:1
# pre:2
# pre:3
# pre:4
# pre:5
a = 1
# post:1
# post:2
# post:3
# post:4
# post:5
CRYSTAL
subject.context(source.lines, lineno: 6, context_lines: 3)
.should eq({<<-PRE.lines, <<-POST.lines
# pre:3
# pre:4
# pre:5
PRE
# post:1
# post:2
# post:3
POST
})
end
end
describe "#affected_code" do
it "returns nil if there is no such a line number" do
code = <<-CRYSTAL
a = 1
CRYSTAL
location = Crystal::Location.new("filename", 2, 1)
subject.affected_code(code, location).should be_nil
end
it "works with file-wide location (1, 1) + indented code" do
code = <<-CRYSTAL
a = 1
CRYSTAL
location = Crystal::Location.new("filename", 1, 1)
subject.deansify(subject.affected_code(code, location))
.should eq "> a = 1\n ^\n"
end
context "trimming" do
max_length = 33
code = <<-CRYSTAL
FOO = "#{"foo" * 111}"
CRYSTAL
it "trims the affected line to `max_length` if location is within the trimmed line" do
location = Crystal::Location.new("filename", 1, max_length - 10)
end_location = Crystal::Location.new("filename", 1, max_length + 10)
affected_code = subject.affected_code(code, location, end_location, max_length: max_length)
affected_code = subject.deansify(affected_code).should_not be_nil
affected_code.should start_with "> %s\n" % subject.trim(code, max_length)
affected_code.should end_with "^%s^\n" % {
"-" * (max_length - location.column_number - 1),
}
end
it "does not trim the affected line if location is not within the `max_length`" do
location = Crystal::Location.new("filename", 1, max_length + 10)
end_location = Crystal::Location.new("filename", 1, max_length + 30)
affected_code = subject.affected_code(code, location, end_location, max_length: max_length)
affected_code = subject.deansify(affected_code).should_not be_nil
affected_code.should start_with "> %s\n" % code
affected_code.should end_with "^%s^\n" % {
"-" * (end_location.column_number - location.column_number - 1),
}
end
end
it "returns correct line if it is found" do
code = <<-CRYSTAL
a = 1
CRYSTAL
location = Crystal::Location.new("filename", 1, 1)
subject.deansify(subject.affected_code(code, location))
.should eq "> a = 1\n ^\n"
end
it "returns correct line if it is found (2)" do
code = <<-CRYSTAL
# pre:1
# pre:2
# pre:3
# pre:4
# pre:5
a = 1
# post:1
# post:2
# post:3
# post:4
# post:5
CRYSTAL
location = Crystal::Location.new("filename", 6, 1)
subject.deansify(subject.affected_code(code, location, context_lines: 3))
.should eq <<-STR
> # pre:3
> # pre:4
> # pre:5
> a = 1
^
> # post:1
> # post:2
> # post:3
STR
end
end
end
end
================================================
FILE: spec/ameba/glob_utils_spec.cr
================================================
require "../spec_helper"
module Ameba
subject = GlobUtils
root = Path[__DIR__, "..", ".."].expand
current_file_path = __FILE__
current_file_basename = File.basename(current_file_path)
current_file_relative_path =
Path[current_file_path].relative_to(Dir.current).to_s
describe GlobUtils do
describe "#find_files_by_globs" do
it "returns files by directory" do
subject.find_files_by_globs([__DIR__], root: root)
.should contain current_file_relative_path
end
it "returns a file by filepath" do
subject.find_files_by_globs([__FILE__], root: root)
.should eq [current_file_relative_path]
end
it "returns a file by globs" do
subject.find_files_by_globs(["**/#{current_file_basename}"], root: root)
.should eq [current_file_relative_path]
end
it "returns files by globs" do
subject.find_files_by_globs(["**/*_spec.cr"], root: root)
.should contain current_file_relative_path
end
it "doesn't return rejected globs" do
subject
.find_files_by_globs(["**/*_spec.cr", "!**/#{current_file_basename}"], root: root)
.should_not contain current_file_relative_path
end
it "doesn't return rejected folders" do
subject
.find_files_by_globs(["**/*_spec.cr", "!spec"], root: root)
.should be_empty
end
it "doesn't return duplicated globs" do
subject
.find_files_by_globs(["**/*_spec.cr", "**/*_spec.cr"], root: root)
.count(current_file_relative_path)
.should eq 1
end
end
describe "#expand" do
it "expands globs" do
subject.expand(["**/#{current_file_basename}"], root: root)
.should eq [current_file_relative_path]
end
it "does not list duplicated files" do
subject.expand(["**/#{current_file_basename}"] * 3, root: root)
.should eq [current_file_relative_path]
end
it "list only files" do
subject.expand(["**/*"], root: root).each do |path|
fail "#{path.inspect} should be a file" unless File.file?(path)
end
end
it "expands folders" do
subject.expand(["spec"], root: root).should_not be_empty
end
end
end
end
================================================
FILE: spec/ameba/inline_comments_spec.cr
================================================
require "../spec_helper"
module Ameba
describe InlineComments do
describe InlineComments::COMMENT_DIRECTIVE_REGEX do
subject = InlineComments::COMMENT_DIRECTIVE_REGEX
it "allows to parse action and rule name" do
result = subject.match("# ameba:enable Group/RuleName")
result = result.should_not be_nil
result["action"].should eq "enable"
result["rules"].should eq "Group/RuleName"
end
it "parses multiple rules" do
result = subject.match("# ameba:enable Group/RuleName, OtherRule, Foo/Bar")
result = result.should_not be_nil
result["action"].should eq "enable"
result["rules"].should eq "Group/RuleName, OtherRule, Foo/Bar"
end
it "fails to parse directives with spaces" do
result = subject.match("# ameba : enable Group/RuleName")
result.should be_nil
end
end
it "disables a rule with a comment directive" do
source = Source.new <<-CRYSTAL
# ameba:disable #{NamedRule.name}
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {1, 12}, message: "Error!")
source.should be_valid
end
it "disables a rule with a line that ends with a comment directive" do
source = Source.new <<-CRYSTAL
Time.epoch(1483859302) # ameba:disable #{NamedRule.name}
CRYSTAL
source.add_issue(NamedRule.new, location: {1, 12}, message: "Error!")
source.should be_valid
end
it "does not disable a rule of a different name" do
source = Source.new <<-CRYSTAL
# ameba:disable WrongName
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {2, 12}, message: "Error!")
source.should_not be_valid
end
it "disables a rule if multiple rule names provided" do
source = Source.new <<-CRYSTAL
# ameba:disable SomeRule LargeNumbers #{NamedRule.name} SomeOtherRule
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {2, 12}, message: "")
source.should be_valid
end
it "disables a rule if multiple rule names are separated by comma" do
source = Source.new <<-CRYSTAL
# ameba:disable SomeRule, LargeNumbers, #{NamedRule.name}, SomeOtherRule
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {2, 12}, message: "")
source.should be_valid
end
it "does not disable if multiple rule names used without required one" do
source = Source.new <<-CRYSTAL
# ameba:disable SomeRule, SomeOtherRule LargeNumbers
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {2, 12}, message: "")
source.should_not be_valid
end
it "does not disable if comment directive has wrong place" do
source = Source.new <<-CRYSTAL
# ameba:disable #{NamedRule.name}
#
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {3, 12}, message: "")
source.should_not be_valid
end
it "does not disable if comment directive added to the wrong line" do
source = Source.new <<-CRYSTAL
if use_epoch? # ameba:disable #{NamedRule.name}
Time.epoch(1483859302)
end
CRYSTAL
source.add_issue(NamedRule.new, location: {3, 12}, message: "")
source.should_not be_valid
end
it "does not disable if that is not a comment directive" do
source = Source.new <<-CRYSTAL
"ameba:disable #{NamedRule.name}"
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {3, 12}, message: "")
source.should_not be_valid
end
it "does not disable if that is a commented out directive" do
source = Source.new <<-CRYSTAL
# # ameba:disable #{NamedRule.name}
Time.epoch(1483859302)
CRYSTAL
source.add_issue(NamedRule.new, location: {3, 12}, message: "")
source.should_not be_valid
end
it "does not disable if that is an inline commented out directive" do
source = Source.new <<-CRYSTAL
a = 1 # Disable it: # ameba:disable #{NamedRule.name}
CRYSTAL
source.add_issue(NamedRule.new, location: {2, 12}, message: "")
source.should_not be_valid
end
context "with group name" do
it "disables one rule with a group" do
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable #{DummyRule.rule_name}
CRYSTAL
source.add_issue(DummyRule.new, location: {1, 12}, message: "")
source.should be_valid
end
it "doesn't disable others rules" do
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable #{DummyRule.rule_name}
CRYSTAL
source.add_issue(NamedRule.new, location: {2, 12}, message: "")
source.should_not be_valid
end
it "disables a hole group of rules" do
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable #{DummyRule.group_name}
CRYSTAL
source.add_issue(DummyRule.new, location: {1, 12}, message: "")
source.should be_valid
end
it "does not disable rules which do not belong to the group" do
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable Lint
CRYSTAL
source.add_issue(DummyRule.new, location: {2, 12}, message: "")
source.should_not be_valid
end
end
end
end
================================================
FILE: spec/ameba/issue_spec.cr
================================================
require "../spec_helper"
module Ameba
describe Issue do
it "accepts rule and message" do
issue = Issue.new code: "",
rule: DummyRule.new,
location: nil,
end_location: nil,
message: "Blah",
status: nil
issue.rule.should_not be_nil
issue.message.should eq "Blah"
end
it "accepts location" do
location = Crystal::Location.new("path", 3, 2)
issue = Issue.new code: "",
rule: DummyRule.new,
location: location,
end_location: nil,
message: "Blah",
status: nil
issue.location.to_s.should eq location.to_s
issue.end_location.should be_nil
end
it "accepts end_location" do
location = Crystal::Location.new("path", 3, 2)
issue = Issue.new code: "",
rule: DummyRule.new,
location: nil,
end_location: location,
message: "Blah",
status: nil
issue.location.should be_nil
issue.end_location.to_s.should eq location.to_s
end
it "accepts status" do
issue = Issue.new code: "",
rule: DummyRule.new,
location: nil,
end_location: nil,
message: "",
status: :disabled
issue.status.should eq Issue::Status::Disabled
issue.disabled?.should be_true
issue.enabled?.should be_false
end
it "sets status to :enabled by default" do
issue = Issue.new code: "",
rule: DummyRule.new,
location: nil,
end_location: nil,
message: ""
issue.status.should eq Issue::Status::Enabled
issue.enabled?.should be_true
issue.disabled?.should be_false
end
end
end
================================================
FILE: spec/ameba/presenter/rule_collection_presenter_spec.cr
================================================
require "../../spec_helper"
module Ameba
private def with_rule_collection_presenter(&)
rules = Config.load.rules
with_presenter(Presenter::RuleCollectionPresenter, rules) do |presenter, output|
yield rules, output, presenter
end
end
describe Presenter::RuleCollectionPresenter do
it "outputs rule collection details" do
with_rule_collection_presenter do |rules, output|
rules.each do |rule|
output.should contain rule.name
output.should contain rule.severity.symbol
if description = rule.description
output.should contain description
end
end
output.should contain "Total rules: #{rules.size}"
output.should match /\d+ enabled/
end
end
end
end
================================================
FILE: spec/ameba/presenter/rule_presenter_spec.cr
================================================
require "../../spec_helper"
module Ameba
private def rule_presenter_each_rule(&)
rules = Config.load.rules
rules.each do |rule|
with_presenter(Presenter::RulePresenter, rule) do |presenter, output|
yield rule, output, presenter
end
end
end
describe Presenter::RulePresenter do
it "outputs rule details" do
rule_presenter_each_rule do |rule, output|
output.should contain rule.name
output.should contain rule.severity.to_s
if description = rule.description
output.should contain description
end
end
end
end
end
================================================
FILE: spec/ameba/presenter/rule_versions_presenter_spec.cr
================================================
require "../../spec_helper"
module Ameba
private def with_rule_versions_presenter(&)
rules = Config.load.rules
with_presenter(Presenter::RuleVersionsPresenter, rules) do |presenter, output|
yield rules, output, presenter
end
end
describe Presenter::RuleVersionsPresenter do
it "outputs rule versions" do
with_rule_versions_presenter do |_rules, output|
output.should contain <<-TEXT
- 0.1.0
- Layout/LineLength
- Layout/TrailingBlankLines
- Layout/TrailingWhitespace
- Lint/ComparisonToBoolean
- Lint/DebuggerStatement
- Lint/LiteralInCondition
- Lint/LiteralInInterpolation
- Style/UnlessElse
TEXT
end
end
end
end
================================================
FILE: spec/ameba/reportable_spec.cr
================================================
require "../spec_helper"
module Ameba
describe Reportable do
describe "#add_issue" do
it "adds a new issue for node" do
source = Source.new path: "source.cr"
source.add_issue(DummyRule.new, Crystal::Nop.new, "Error!")
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should be_empty
issue.message.should eq "Error!"
end
it "adds a new issue by line and column number" do
source = Source.new path: "source.cr"
source.add_issue(DummyRule.new, {23, 2}, "Error!")
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:23:2"
issue.message.should eq "Error!"
end
end
describe "#valid?" do
it "returns true if no issues added" do
source = Source.new path: "source.cr"
source.should be_valid
end
it "returns false if there are issues added" do
source = Source.new path: "source.cr"
source.add_issue DummyRule.new, {22, 2}, "ERROR!"
source.should_not be_valid
end
end
end
end
================================================
FILE: spec/ameba/rule/base_spec.cr
================================================
require "../../spec_helper"
module Ameba
describe Rule::Base do
describe "#catch" do
it "accepts and returns source" do
source = Source.new
DummyRule.new.catch(source).should eq source
end
end
describe "#name" do
it "returns name of the rule" do
DummyRule.new.name.should eq "Ameba/DummyRule"
end
end
describe "#group" do
it "returns a group rule belongs to" do
DummyRule.new.group.should eq "Ameba"
end
end
end
describe Rule do
describe ".rules" do
it "returns a list of all defined rules" do
Rule.rules.includes?(DummyRule).should be_true
end
end
end
end
================================================
FILE: spec/ameba/rule/documentation/admonition_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Documentation
describe Admonition do
subject = Admonition.new
it "passes for comments with admonition mid-word/sentence" do
subject.admonitions.each do |admonition|
expect_no_issues subject, <<-CRYSTAL
# Mentioning #{admonition} mid-sentence
# x#{admonition}x
# x#{admonition}
# #{admonition}x
CRYSTAL
end
end
it "fails for comments with admonition" do
subject.admonitions.each do |admonition|
expect_issue subject, <<-CRYSTAL, admonition: admonition
def foo
# %{admonition}: Single-line comment
# ^{admonition} error: Found a %{admonition} admonition in a comment
end
CRYSTAL
expect_issue subject, <<-CRYSTAL, admonition: admonition
def foo
# Text before ...
# %{admonition}(some context): Part of multi-line comment
# ^{admonition} error: Found a %{admonition} admonition in a comment
# Text after ...
end
CRYSTAL
expect_issue subject, <<-CRYSTAL, admonition: admonition
def foo
# %{admonition}
# ^{admonition} error: Found a %{admonition} admonition in a comment
if rand > 0.5
end
end
CRYSTAL
end
end
context "with date" do
it "passes for admonitions with future date" do
subject.admonitions.each do |admonition|
future_date = (Time.utc + 21.days).to_s(format: "%F")
expect_no_issues subject, <<-CRYSTAL
# #{admonition}(#{future_date}): sth in the future
CRYSTAL
end
end
it "fails for admonitions with past date" do
subject.admonitions.each do |admonition|
past_date = (Time.utc - 21.days).to_s(format: "%F")
expect_issue subject, <<-CRYSTAL, admonition: admonition
# %{admonition}(#{past_date}): sth in the past
# ^{admonition} error: Found a %{admonition} admonition in a comment (21 days past)
CRYSTAL
end
end
it "fails for admonitions with yesterday's date" do
subject.admonitions.each do |admonition|
yesterday_date = (Time.utc - 1.day).to_s(format: "%F")
expect_issue subject, <<-CRYSTAL, admonition: admonition
# %{admonition}(#{yesterday_date}): sth in the past
# ^{admonition} error: Found a %{admonition} admonition in a comment (1 day past)
CRYSTAL
end
end
it "fails for admonitions with today's date" do
subject.admonitions.each do |admonition|
today_date = Time.utc.to_s(format: "%F")
expect_issue subject, <<-CRYSTAL, admonition: admonition
# %{admonition}(#{today_date}): sth in the past
# ^{admonition} error: Found a %{admonition} admonition in a comment (today is the day!)
CRYSTAL
end
end
it "fails for admonitions with invalid date" do
subject.admonitions.each do |admonition|
expect_issue subject, <<-CRYSTAL, admonition: admonition
# %{admonition}(0000-00-00): sth wrong
# ^{admonition} error: %{admonition} admonition error: Invalid time: "0000-00-00"
CRYSTAL
end
end
end
context "properties" do
describe "#admonitions" do
it "lets setting custom admonitions" do
rule = Admonition.new
rule.admonitions = %w[FOO BAR]
rule.admonitions.each do |admonition|
expect_issue rule, <<-CRYSTAL, admonition: admonition
# %{admonition}
# ^{admonition} error: Found a %{admonition} admonition in a comment
CRYSTAL
end
subject.admonitions.each do |admonition|
expect_no_issues rule, <<-CRYSTAL
# #{admonition}
CRYSTAL
end
end
end
end
end
end
================================================
FILE: spec/ameba/rule/documentation/documentation_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Documentation
describe Documentation do
subject = Documentation.new
subject.ignore_classes = false
subject.ignore_modules = false
subject.ignore_enums = false
subject.ignore_defs = false
subject.ignore_macros = false
it "passes for undocumented private types" do
expect_no_issues subject, <<-CRYSTAL
private class Foo
def foo
end
end
private module Bar
def bar
end
end
private enum Baz
end
private def bat
end
private macro bag
end
CRYSTAL
end
it "passes for documented public types" do
expect_no_issues subject, <<-CRYSTAL
# Foo
class Foo
# foo
def foo
end
end
# Bar
module Bar
# bar
def bar
end
end
# Baz
enum Baz
end
# bat
def bat
end
# bag
macro bag
end
CRYSTAL
end
it "fails if there is an undocumented public type" do
expect_issue subject, <<-CRYSTAL
class Foo
# ^^^^^^^ error: Missing documentation
end
module Bar
# ^^^^^^^^ error: Missing documentation
end
enum Baz
# ^^^^^^ error: Missing documentation
end
def bat
# ^^^^^ error: Missing documentation
end
macro bag
# ^^^^^^^ error: Missing documentation
end
CRYSTAL
end
context "properties" do
describe "#ignore_classes" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_classes = true
expect_no_issues rule, <<-CRYSTAL
class Foo
end
CRYSTAL
end
end
describe "#ignore_modules" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_modules = true
expect_no_issues rule, <<-CRYSTAL
module Bar
end
CRYSTAL
end
end
describe "#ignore_enums" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_enums = true
expect_no_issues rule, <<-CRYSTAL
enum Baz
end
CRYSTAL
end
end
describe "#ignore_defs" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_defs = true
expect_no_issues rule, <<-CRYSTAL
def bat
end
CRYSTAL
end
end
describe "#ignore_macros" do
it "lets the rule to ignore macros if true" do
rule = Documentation.new
rule.ignore_macros = true
expect_no_issues rule, <<-CRYSTAL
macro bag
end
CRYSTAL
end
end
describe "#require_example" do
rule = Documentation.new
rule.require_example = true
it "fails if there is a documented public type without example" do
expect_issue rule, <<-CRYSTAL
# Foo documentation
class Foo
# ^^^^^^^ error: Missing documentation example
end
CRYSTAL
end
it "passes if there is a documented public type with example" do
expect_no_issues rule, <<-CRYSTAL
# Foo documentation
#
# ```
# foo = Foo.new
# foo.bar # => :baz
# ```
class Foo
end
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/layout/line_length_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Layout
describe LineLength do
subject = LineLength.new
long_line = "*" * (subject.max_length + 1)
it "passes if all lines are shorter than MaxLength symbols" do
expect_no_issues subject, <<-CRYSTAL
short line
CRYSTAL
end
it "passes if line consists of MaxLength symbols" do
expect_no_issues subject, <<-CRYSTAL
#{"*" * subject.max_length}
CRYSTAL
end
it "fails if there is at least one line longer than MaxLength symbols" do
source = Source.new long_line
subject.catch(source).should_not be_valid
end
it "reports rule, pos and message" do
source = Source.new long_line, "source.cr"
subject.catch(source).should_not be_valid
issue = source.issues.first
issue.rule.should eq subject
issue.location.to_s.should eq "source.cr:1:#{subject.max_length + 1}"
issue.end_location.should be_nil
issue.message.should eq "Line too long"
end
context "properties" do
it "#max_length" do
rule = LineLength.new
rule.max_length = long_line.size
expect_no_issues rule, long_line
end
end
end
end
================================================
FILE: spec/ameba/rule/layout/trailing_blank_lines_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Layout
describe TrailingBlankLines do
subject = TrailingBlankLines.new
it "passes if there is a blank line at the end of a source" do
expect_no_issues subject, "a = 1\n"
end
it "passes if source is empty" do
expect_no_issues subject, ""
end
it "fails if there is no blank lines at the end" do
source = expect_issue subject, "no-blankline # error: Trailing newline missing"
expect_correction source, "no-blankline\n"
end
it "fails if there more then one blank line at the end of a source" do
source = expect_issue subject, "a = 1\n \n # error: Excessive trailing newline detected"
expect_no_corrections source
end
it "fails if last line is not blank" do
source = expect_issue subject, "\n\n\n puts 22 # error: Trailing newline missing"
expect_correction source, "\n\n\n puts 22\n"
end
context "when unnecessary blank line has been detected" do
it "reports rule, pos and message" do
source = Source.new "a = 1\n\n", "source.cr", normalize: false
subject.catch(source).should_not be_valid
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:3:1"
issue.end_location.should be_nil
issue.message.should eq "Excessive trailing newline detected"
end
end
context "when final line has been missed" do
it "reports rule, pos and message" do
source = Source.new "a = 1", "source.cr", normalize: false
subject.catch(source).should_not be_valid
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:1"
issue.end_location.should be_nil
issue.message.should eq "Trailing newline missing"
end
end
end
end
================================================
FILE: spec/ameba/rule/layout/trailing_whitespace_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Layout
describe TrailingWhitespace do
subject = TrailingWhitespace.new
it "passes if all lines do not have trailing whitespace" do
expect_no_issues subject, "no-whitespace"
end
it "passes a line ends with trailing CRLF sequence" do
expect_no_issues subject, "no-whitespace\r\n"
end
it "fails if a line ends with trailing \\r character" do
source = expect_issue subject, <<-TEXT
carriage return at the end\r
# ^ error: Trailing whitespace detected
TEXT
expect_correction source, "carriage return at the end"
end
it "fails if there is a line with trailing tab" do
source = expect_issue subject, <<-TEXT
tab at the end\t
# ^ error: Trailing whitespace detected
TEXT
expect_correction source, "tab at the end"
end
it "fails if there is a line with trailing whitespace" do
source = expect_issue subject,
"whitespace at the end \n" \
" # ^^ error: Trailing whitespace detected"
expect_correction source, "whitespace at the end"
end
end
end
================================================
FILE: spec/ameba/rule/lint/ambiguous_assignment_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe AmbiguousAssignment do
subject = AmbiguousAssignment.new
context "when using `-`" do
it "registers an offense with `x`" do
expect_issue subject, <<-CRYSTAL
x =- y
# ^^ error: Suspicious assignment detected. Did you mean `-=`?
CRYSTAL
end
it "registers an offense with `@x`" do
expect_issue subject, <<-CRYSTAL
@x =- y
# ^^ error: Suspicious assignment detected. Did you mean `-=`?
CRYSTAL
end
it "registers an offense with `@@x`" do
expect_issue subject, <<-CRYSTAL
@@x =- y
# ^^ error: Suspicious assignment detected. Did you mean `-=`?
CRYSTAL
end
it "registers an offense with `X`" do
expect_issue subject, <<-CRYSTAL
X =- y
# ^^ error: Suspicious assignment detected. Did you mean `-=`?
CRYSTAL
end
it "does not register an offense when no mistype assignments" do
expect_no_issues subject, <<-CRYSTAL
x = 1
x -= y
x = -y
CRYSTAL
end
end
context "when using `+`" do
it "registers an offense with `x`" do
expect_issue subject, <<-CRYSTAL
x =+ y
# ^^ error: Suspicious assignment detected. Did you mean `+=`?
CRYSTAL
end
it "registers an offense with `@x`" do
expect_issue subject, <<-CRYSTAL
@x =+ y
# ^^ error: Suspicious assignment detected. Did you mean `+=`?
CRYSTAL
end
it "registers an offense with `@@x`" do
expect_issue subject, <<-CRYSTAL
@@x =+ y
# ^^ error: Suspicious assignment detected. Did you mean `+=`?
CRYSTAL
end
it "registers an offense with `X`" do
expect_issue subject, <<-CRYSTAL
X =+ y
# ^^ error: Suspicious assignment detected. Did you mean `+=`?
CRYSTAL
end
it "does not register an offense when no mistype assignments" do
expect_no_issues subject, <<-CRYSTAL
x = 1
x += y
x = +y
CRYSTAL
end
end
context "when using `!`" do
it "registers an offense with `x`" do
expect_issue subject, <<-CRYSTAL
x =! y
# ^^ error: Suspicious assignment detected. Did you mean `!=`?
CRYSTAL
end
it "registers an offense with `@x`" do
expect_issue subject, <<-CRYSTAL
@x =! y
# ^^ error: Suspicious assignment detected. Did you mean `!=`?
CRYSTAL
end
it "registers an offense with `@@x`" do
expect_issue subject, <<-CRYSTAL
@@x =! y
# ^^ error: Suspicious assignment detected. Did you mean `!=`?
CRYSTAL
end
it "registers an offense with `X`" do
expect_issue subject, <<-CRYSTAL
X =! y
# ^^ error: Suspicious assignment detected. Did you mean `!=`?
CRYSTAL
end
it "does not register an offense when no mistype assignments" do
expect_no_issues subject, <<-CRYSTAL
x = false
x != y
x = !y
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/assignment_in_call_argument_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe AssignmentInCallArgument do
subject = AssignmentInCallArgument.new
context "outside of a method call arguments" do
it "ignores const assignments" do
expect_no_issues subject, <<-CRYSTAL
FOO = 1
CRYSTAL
end
it "ignores bare assignments" do
expect_no_issues subject, <<-CRYSTAL
foo = 1
CRYSTAL
end
it "ignores assignments within blocks" do
expect_no_issues subject, <<-CRYSTAL
foo do
foo = 1
end
CRYSTAL
end
it "ignores assignments within methods" do
expect_no_issues subject, <<-CRYSTAL
def foo
foo = 1
end
CRYSTAL
end
it "ignores assignments within classes" do
expect_no_issues subject, <<-CRYSTAL
class Foo
foo = 1
end
CRYSTAL
end
end
context "in stdlib macros" do
it "ignores assignments within accessor macros" do
expect_no_issues subject, <<-CRYSTAL
class Foo
class_getter foo = 1
getter bar = 2
property baz = 3
end
CRYSTAL
end
it "ignores assignments within `record` macro" do
expect_no_issues subject, <<-CRYSTAL
record Foo, foo = 1
CRYSTAL
end
end
it "ignores assignments within assignments" do
expect_no_issues subject, <<-CRYSTAL
self.foo = foo = 1
CRYSTAL
end
it "ignores assignments within operator assignments" do
expect_no_issues subject, <<-CRYSTAL
self.foo += foo = 1
CRYSTAL
end
it "ignores assignments within operator calls" do
expect_no_issues subject, <<-CRYSTAL
self.foo + (foo = 1)
CRYSTAL
end
it "ignores assignment within a proc passed as a method call argument" do
expect_no_issues subject, <<-CRYSTAL
foo -> {
bar = 1
}
CRYSTAL
end
it "ignores assignment within a proc passed as a method call named argument" do
expect_no_issues subject, <<-CRYSTAL
foo bar: -> {
baz = 1
}
CRYSTAL
end
it "ignores assignment within a nested call" do
expect_no_issues subject, <<-CRYSTAL
foo(bar do
baz = 1
end)
CRYSTAL
end
it "reports assignment within a method call argument" do
expect_issue subject, <<-CRYSTAL
foo a = 1
# ^^^^^ error: Assignment within a call argument detected
CRYSTAL
end
it "reports multiple assignments within a method call arguments" do
expect_issue subject, <<-CRYSTAL
foo a = 1, b = 2
# ^^^^^ error: Assignment within a call argument detected
# ^^^^^ error: Assignment within a call argument detected
CRYSTAL
end
it "reports operator assignment within a method call argument" do
expect_issue subject, <<-CRYSTAL
a = 0
foo a += 1
# ^^^^^^ error: Assignment within a call argument detected
CRYSTAL
end
it "reports assignment within a method call named argument" do
expect_issue subject, <<-CRYSTAL
foo a: a = 1
# ^^^^^ error: Assignment within a call argument detected
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/bad_directive_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe BadDirective do
subject = BadDirective.new
it "does not report if action is correct" do
expect_no_issues subject, <<-CRYSTAL
# ameba:disable Lint/BadDirective
CRYSTAL
end
it "reports if there is incorrect action" do
expect_issue subject, <<-CRYSTAL
# ameba:foo Lint/BadDirective
# ^^^ error: Bad action in comment directive: `foo`. Possible values: `disable`, `enable`
CRYSTAL
end
it "does not report if there no action and rules at all" do
expect_no_issues subject, <<-CRYSTAL
# ameba:
CRYSTAL
end
it "does not report if there are no rules" do
expect_no_issues subject, <<-CRYSTAL
# ameba:enable
# ameba:disable
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/comparison_to_boolean_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe ComparisonToBoolean do
subject = ComparisonToBoolean.new
it "passes if there is no comparison to boolean" do
expect_no_issues subject, <<-CRYSTAL
a = true
if a
:ok
end
if true
:ok
end
unless s.empty?
:ok
end
:ok if a
:ok if a != 1
:ok if a == "true"
case a
when true
:ok
when false
:not_ok
end
CRYSTAL
end
context "boolean on the right" do
it "fails if there is == comparison to boolean" do
source = expect_issue subject, <<-CRYSTAL
if s.empty? == true
# ^^^^^^^^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
if s.empty? == false
# ^^^^^^^^^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
CRYSTAL
expect_correction source, <<-CRYSTAL
if s.empty?
:ok
end
if !s.empty?
:ok
end
CRYSTAL
end
it "fails if there is != comparison to boolean" do
source = expect_issue subject, <<-CRYSTAL
if a != false
# ^^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
if a != true
# ^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
CRYSTAL
expect_correction source, <<-CRYSTAL
if a
:ok
end
if !a
:ok
end
CRYSTAL
end
it "fails if there is case comparison to boolean" do
source = expect_issue subject, <<-CRYSTAL
a === true
# ^^^^^^^^ error: Comparison to a boolean is pointless
CRYSTAL
expect_correction source, <<-CRYSTAL
a
CRYSTAL
end
end
context "boolean on the left" do
it "fails if there is == comparison to boolean" do
source = expect_issue subject, <<-CRYSTAL
if true == s.empty?
# ^^^^^^^^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
if false == s.empty?
# ^^^^^^^^^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
CRYSTAL
expect_correction source, <<-CRYSTAL
if s.empty?
:ok
end
if !s.empty?
:ok
end
CRYSTAL
end
it "fails if there is != comparison to boolean" do
source = expect_issue subject, <<-CRYSTAL
if false != a
# ^^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
if true != a
# ^^^^^^^^^ error: Comparison to a boolean is pointless
:ok
end
CRYSTAL
expect_correction source, <<-CRYSTAL
if a
:ok
end
if !a
:ok
end
CRYSTAL
end
it "fails if there is case comparison to boolean" do
source = expect_issue subject, <<-CRYSTAL
true === a
# ^^^^^^^^ error: Comparison to a boolean is pointless
CRYSTAL
expect_correction source, <<-CRYSTAL
a
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/debug_calls_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DebugCalls do
subject = DebugCalls.new
it "fails if there is a debug call" do
subject.method_names.each do |name|
source = expect_issue subject, <<-CRYSTAL, name: name
a = 2
%{name} a
# ^{name} error: Possibly forgotten debug-related `%{name}` call detected
a = a + 1
CRYSTAL
expect_no_corrections source
end
end
it "passes if there is no debug call" do
subject.method_names.each do |name|
expect_no_issues subject, <<-CRYSTAL
class A
def #{name}
end
end
A.new.#{name}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/debugger_statement_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DebuggerStatement do
subject = DebuggerStatement.new
it "passes if there is no debugger statement" do
expect_no_issues subject, <<-CRYSTAL
"this is not a debugger statement"
s = "debugger"
def debugger(program)
end
debugger ""
class A
def debugger
end
end
A.new.debugger
CRYSTAL
end
it "fails if there is a debugger statement" do
source = expect_issue subject, <<-CRYSTAL
a = 2
debugger
# ^^^^^^ error: Possible forgotten `debugger` statement detected
a = a + 1
CRYSTAL
expect_correction source, <<-CRYSTAL
a = 2
a = a + 1
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/duplicate_branch_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DuplicateBranch do
subject = DuplicateBranch.new
it "does not report different `if` and `else` branch bodies" do
expect_no_issues subject, <<-CRYSTAL
if :foo
1
elsif :foo
2
elsif :foo
3
else
nil
end
CRYSTAL
end
it "reports duplicated `if` and `else` branch bodies" do
expect_issue subject, <<-CRYSTAL
if 1
:foo
elsif 2
:foo
# ^^^^ error: Duplicate branch body detected
elsif 3
:foo
# ^^^^ error: Duplicate branch body detected
else
:foo
# ^^^^ error: Duplicate branch body detected
end
CRYSTAL
end
it "reports duplicated `if` branch bodies" do
expect_issue subject, <<-CRYSTAL
if true
:foo
elsif false
:foo
# ^^^^ error: Duplicate branch body detected
end
CRYSTAL
end
it "reports duplicated `else` branch bodies" do
expect_issue subject, <<-CRYSTAL
if true
:foo
else
:foo
# ^^^^ error: Duplicate branch body detected
end
CRYSTAL
end
it "reports duplicated `else` branch body within `unless`" do
expect_issue subject, <<-CRYSTAL
unless true
:foo
else
:foo
# ^^^^ error: Duplicate branch body detected
end
CRYSTAL
end
it "reports duplicated `if` / `else` branch bodies nested within `if`" do
expect_issue subject, <<-CRYSTAL
if true
:foo
elsif false
%w[foo bar].each do
if 1
:abc
elsif 2
:abc
# ^^^^ error: Duplicate branch body detected
else
:abc
# ^^^^ error: Duplicate branch body detected
end
end
:foo
end
CRYSTAL
end
it "reports duplicated `if` / `else` branch bodies nested within `else`" do
expect_issue subject, <<-CRYSTAL
if true
:foo
else
%w[foo bar].each do
if 1
:abc
elsif 2
:abc
# ^^^^ error: Duplicate branch body detected
else
:abc
# ^^^^ error: Duplicate branch body detected
end
end
end
CRYSTAL
end
it "reports duplicated `else` branch bodies within a ternary `if`" do
expect_issue subject, <<-CRYSTAL
true ? :foo : :foo
# ^^^^ error: Duplicate branch body detected
CRYSTAL
end
it "reports duplicated `case` branch bodies" do
expect_issue subject, <<-CRYSTAL
case
when true
:foo
when false
:foo
# ^^^^ error: Duplicate branch body detected
end
CRYSTAL
end
it "reports duplicated exception handler branch bodies" do
expect_issue subject, <<-CRYSTAL
begin
:foo
rescue ArgumentError
:foo
rescue OverflowError
:foo
# ^^^^ error: Duplicate branch body detected
else
:foo
# ^^^^ error: Duplicate branch body detected
end
CRYSTAL
end
context "properties" do
context "#ignore_literal_branches" do
it "when disabled reports duplicated (static) literal branch bodies" do
rule = DuplicateBranch.new
rule.ignore_literal_branches = false
expect_issue rule, <<-CRYSTAL
true ? :foo : :foo
# ^^^^ error: Duplicate branch body detected
true ? "foo" : "foo"
# ^^^^^ error: Duplicate branch body detected
true ? 123 : 123
# ^^^ error: Duplicate branch body detected
true ? [1, 2, 3] : [1, 2, 3]
# ^^^^^^^^^ error: Duplicate branch body detected
true ? [foo, bar, baz] : [foo, bar, baz]
# ^^^^^^^^^^^^^^^ error: Duplicate branch body detected
CRYSTAL
end
it "when enabled does not report duplicated (static) literal branch bodies" do
rule = DuplicateBranch.new
rule.ignore_literal_branches = true
# static literals
expect_no_issues rule, <<-CRYSTAL
true ? :foo : :foo
true ? "foo" : "foo"
true ? 123 : 123
true ? [1, 2, 3] : [1, 2, 3]
true ? {foo: "bar"} : {foo: "bar"}
CRYSTAL
# dynamic literals
expect_issue rule, <<-CRYSTAL
true ? [foo, bar, baz] : [foo, bar, baz]
# ^^^^^^^^^^^^^^^ error: Duplicate branch body detected
CRYSTAL
end
end
context "#ignore_constant_branches" do
it "when disabled reports constant branch bodies" do
rule = DuplicateBranch.new
rule.ignore_constant_branches = false
expect_issue rule, <<-CRYSTAL
true ? FOO : FOO
# ^^^ error: Duplicate branch body detected
true ? Foo::Bar : Foo::Bar
# ^^^^^^^^ error: Duplicate branch body detected
CRYSTAL
end
it "when enabled does not report constant branch bodies" do
rule = DuplicateBranch.new
rule.ignore_constant_branches = true
expect_no_issues rule, <<-CRYSTAL
true ? FOO : FOO
true ? Foo::Bar : Foo::Bar
CRYSTAL
end
end
context "#ignore_duplicate_else_branch" do
rule = DuplicateBranch.new
rule.ignore_duplicate_else_branch = true
context "when enabled does not report duplicated `else` branch bodies" do
it "in `if`" do
expect_no_issues rule, <<-CRYSTAL
if true
:foo
else
:foo
end
CRYSTAL
end
it "in ternary `if`" do
expect_no_issues rule, <<-CRYSTAL
true ? :foo : :foo
CRYSTAL
end
it "in `unless`" do
expect_no_issues rule, <<-CRYSTAL
unless true
:foo
else
:foo
end
CRYSTAL
end
it "in `case`" do
expect_no_issues rule, <<-CRYSTAL
case
when true
:foo
else
:foo
end
CRYSTAL
end
it "in exception handler" do
expect_no_issues rule, <<-CRYSTAL
begin
:foo
rescue ArgumentError
:foo
else
:foo
end
CRYSTAL
end
end
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/duplicate_enum_value_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DuplicateEnumValue do
subject = DuplicateEnumValue.new
it "passes if there are no enum members with duplicate values" do
expect_no_issues subject, <<-CRYSTAL
enum Foo
Foo
Bar
Baz
end
CRYSTAL
end
it "passes if there are no duplicate enum member values" do
expect_no_issues subject, <<-CRYSTAL
enum Foo
Foo = 1
Bar = 2
Baz = 3
end
CRYSTAL
end
it "passes if there are aliased enum member values" do
expect_no_issues subject, <<-CRYSTAL
enum Foo
Foo = 1
Bar = 2
Baz = Bar
end
CRYSTAL
end
it "reports if there are a duplicate enum member values" do
expect_issue subject, <<-CRYSTAL
enum Foo
Foo = 111
Bar = 222
Baz = 222
# ^^^ error: Duplicate enum member value detected
Bat = 222
# ^^^ error: Duplicate enum member value detected
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/duplicate_method_signature_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DuplicateMethodSignature do
subject = DuplicateMethodSignature.new
it "passes if there are no duplicate methods" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def foo(&); end
def foo?; end
def foo!; end
def foo(
bar : Bar,
)
end
def foo(
baz : Baz,
)
end
end
CRYSTAL
end
it "passes if there are duplicate methods with `previous_def`" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
42
end
def foo
previous_def if rand >= 0.42
24
end
end
CRYSTAL
end
it "reports if there are duplicate methods without `previous_def`" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo
42
end
def foo
# ^^^^^^^ error: Duplicate method signature detected
previous_definition if rand >= 0.42
24
end
end
CRYSTAL
end
it "reports if there are multiple duplicate methods" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo; end
def bar; end
def foo; end
# ^^^^^^^^^^^^ error: Duplicate method signature detected
def foo; end
# ^^^^^^^^^^^^ error: Duplicate method signature detected
end
CRYSTAL
end
it "reports if there are duplicate methods (with block)" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo(&); end
def bar(&); end
def foo(&); end
# ^^^^^^^^^^^^^^^ error: Duplicate method signature detected
end
CRYSTAL
end
it "reports if there are duplicate methods (with visibility modifier)" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo; end
private def foo; end
# ^^^^^^^^^^^^ error: Duplicate method signature detected
protected def foo; end
# ^^^^^^^^^^^^ error: Duplicate method signature detected
end
CRYSTAL
end
it "reports if there are duplicate methods (with arguments)" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo(a, b, c = 3, &); end
def foo(a, b, c = 3); end
def foo(a, b, c = 3); end
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicate method signature detected
end
CRYSTAL
end
it "reports if there are duplicate methods with different bodies" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo(a, b, c = 3)
puts :foo
end
def foo(a, b, c = 3)
# ^^^^^^^^^^^^^^^^^^^^ error: Duplicate method signature detected
puts :bar
end
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/duplicate_when_condition_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DuplicateWhenCondition do
subject = DuplicateWhenCondition.new
it "passes if there are no duplicated `when` conditions" do
expect_no_issues subject, <<-CRYSTAL
case x
when .nil?
do_something
when Symbol
do_something_else
end
CRYSTAL
end
it "reports if there are a duplicated `when` conditions in `case` expression" do
expect_issue subject, <<-CRYSTAL
case x
when .foo?, .nil?
do_something
when .nil?
# ^^^^^ error: Duplicate `when` condition detected
do_something_else
end
CRYSTAL
expect_issue subject, <<-CRYSTAL
case
when foo?
:foo
when foo?, bar?
# ^^^^ error: Duplicate `when` condition detected
:foobar
when Time.utc.year == 1996
:yo
when Time.utc.year == 1996
# ^^^^^^^^^^^^^^^^^^^^^ error: Duplicate `when` condition detected
:yo
end
CRYSTAL
end
it "reports if there are a duplicated `when` conditions in `select` expression" do
expect_issue subject, <<-CRYSTAL
select
when foo = foo_channel.receive
puts foo
when foo = foo_channel.receive
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicate `when` condition detected
puts foo
when bar = bar_channel.receive?
puts bar
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/duplicated_require_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe DuplicatedRequire do
subject = DuplicatedRequire.new
it "passes if there are no duplicated requires" do
expect_no_issues subject, <<-CRYSTAL
require "math"
require "big"
require "big/big_decimal"
CRYSTAL
end
it "reports if there are a duplicated requires" do
source = expect_issue subject, <<-CRYSTAL
require "big"
require "math"
require "big"
# ^^^^^^^^^^^ error: Duplicated require of `big`
CRYSTAL
expect_no_corrections source
end
end
end
================================================
FILE: spec/ameba/rule/lint/else_nil_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe ElseNil do
subject = ElseNil.new
{% for keyword in %w[if unless].map(&.id) %}
it "does not report if `else` block of an `{{ keyword }}` has a non-nil body" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} foo
do_foo
else
do_bar
end
CRYSTAL
end
it "reports if there is an `else` block of an `{{ keyword }}` with a `nil` body" do
source = expect_issue subject, <<-CRYSTAL
{{ keyword }} foo
do_foo
else
nil
# ^^^ error: Avoid `else` blocks with `nil` as their body
end
CRYSTAL
expect_correction source, <<-CRYSTAL
{{ keyword }} foo
do_foo
end
CRYSTAL
end
{% end %}
it "does not report if there is an `else` block of an ternary `if` with a `nil` body" do
expect_no_issues subject, <<-CRYSTAL
foo ? do_foo : nil
CRYSTAL
end
it "does not report if `else` block of a `case` has a non-nil body" do
expect_no_issues subject, <<-CRYSTAL
case foo
when :foo
do_foo
else
do_bar
end
CRYSTAL
end
it "reports if there is an `else` block of a `case` with a `nil` body" do
source = expect_issue subject, <<-CRYSTAL
case foo
when :foo
do_foo
else
nil
# ^^^ error: Avoid `else` blocks with `nil` as their body
end
CRYSTAL
expect_no_corrections source
end
end
end
================================================
FILE: spec/ameba/rule/lint/empty_ensure_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe EmptyEnsure do
subject = EmptyEnsure.new
it "passes if there is no empty ensure blocks" do
expect_no_issues subject, <<-CRYSTAL
def some_method
do_some_stuff
ensure
do_something_else
end
begin
do_some_stuff
ensure
do_something_else
end
def method_with_rescue
rescue
ensure
nil
end
CRYSTAL
end
it "fails if there is an empty ensure in method" do
source = expect_issue subject, <<-CRYSTAL
def method
do_some_stuff
ensure
# ^^^^ error: Empty `ensure` block detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method
do_some_stuff
end
CRYSTAL
end
it "fails if there is an empty ensure in a block" do
source = expect_issue subject, <<-CRYSTAL
begin
do_some_stuff
rescue
do_some_other_stuff
ensure
# ^^^^ error: Empty `ensure` block detected
# nothing here
end
CRYSTAL
expect_correction source, <<-CRYSTAL
begin
do_some_stuff
rescue
do_some_other_stuff
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/empty_expression_spec.cr
================================================
require "../../../spec_helper"
private def it_detects_empty_expression(code, *, file = __FILE__, line = __LINE__)
it "detects empty expression #{code.inspect}", file, line do
source = Ameba::Source.new code
rule = Ameba::Rule::Lint::EmptyExpression.new
rule.catch(source).should_not be_valid, file: file, line: line
end
end
module Ameba::Rule::Lint
describe EmptyExpression do
subject = EmptyExpression.new
it "passes if there is no empty expression" do
expect_no_issues subject, <<-CRYSTAL
def method()
end
method()
method(1, 2, 3)
method(nil)
a = nil
a = ""
a = 0
nil
:any.nil?
begin "" end
[nil] << nil
CRYSTAL
end
it_detects_empty_expression %(())
it_detects_empty_expression %(((())))
it_detects_empty_expression %(a = ())
it_detects_empty_expression %((();()))
it_detects_empty_expression %(if (); end)
it_detects_empty_expression <<-CRYSTAL
if foo
1
elsif ()
2
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
case foo
when :foo then ()
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
case foo
when :foo then 1
else
()
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
case foo
when () then 1
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
def method
a = 1
()
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
def method
rescue
()
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
def method
begin
end
end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
begin; end
CRYSTAL
it_detects_empty_expression <<-CRYSTAL
begin
()
end
CRYSTAL
it "does not report empty expression in macro" do
expect_no_issues subject, <<-CRYSTAL
module MyModule
macro conditional_error_for_inline_callbacks
\\{% raise "" %}
end
macro before_save(x = nil)
end
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/empty_loop_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe EmptyLoop do
subject = EmptyLoop.new
it "does not report if there are not empty loops" do
expect_no_issues subject, <<-CRYSTAL
a = 1
while a < 10
a += 1
end
until a == 10
a += 1
end
loop do
a += 1
end
CRYSTAL
end
it "reports if there is an empty while loop" do
expect_issue subject, <<-CRYSTAL
a = 1
while true
# ^^^^^^^^ error: Empty loop detected
end
CRYSTAL
end
it "doesn't report if while loop has non-literals in cond block" do
expect_no_issues subject, <<-CRYSTAL
a = 1
while a = gets.to_s
# nothing here
end
CRYSTAL
end
it "reports if there is an empty until loop" do
expect_issue subject, <<-CRYSTAL
do_something
until false
# ^^^^^^^^^ error: Empty loop detected
end
CRYSTAL
end
it "doesn't report if until loop has non-literals in cond block" do
expect_no_issues subject, <<-CRYSTAL
until socket_open?
end
CRYSTAL
end
it "reports if there an empty loop" do
expect_issue subject, <<-CRYSTAL
a = 1
loop do
# ^^^^^ error: Empty loop detected
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/enum_member_name_conflict_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe EnumMemberNameConflict do
subject = EnumMemberNameConflict.new
it "passes if there are no enum members with duplicate names" do
expect_no_issues subject, <<-CRYSTAL
enum Foo
Foo
Bar
Baz
end
CRYSTAL
end
it "reports if enum members have their values assigned" do
expect_issue subject, <<-CRYSTAL
enum Foo
Foo = 1
FOO = 2
# ^^^ error: Enum member name conflict detected
end
CRYSTAL
end
it "reports if there are a duplicate enum member names" do
expect_issue subject, <<-CRYSTAL
enum Foo
Foo
FOo
# ^^^ error: Enum member name conflict detected
FoO
# ^^^ error: Enum member name conflict detected
FOO
# ^^^ error: Enum member name conflict detected
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/formatting_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe Formatting do
subject = Formatting.new
it "passes if source is formatted" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b)
a + b
end
CRYSTAL
end
it "reports if source is not formatted" do
source = expect_issue subject, <<-CRYSTAL
def method(a,b,c=0)
# ^{} error: Use built-in formatter to format this source
a+b+c
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a, b, c = 0)
a + b + c
end
CRYSTAL
end
context "properties" do
context "#fail_on_error" do
it "passes on formatter errors by default" do
rule = Formatting.new
expect_no_issues rule, <<-CRYSTAL
def method(a, b)
a + b
CRYSTAL
end
it "reports on formatter errors when enabled" do
rule = Formatting.new
rule.fail_on_error = true
expect_issue rule, <<-CRYSTAL
def method(a, b)
a + b
# ^ error: Error while formatting: expecting identifier 'end', not 'EOF'
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/hash_duplicated_key_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe HashDuplicatedKey do
subject = HashDuplicatedKey.new
it "passes if there is no duplicated keys in a hash literals" do
expect_no_issues subject, <<-CRYSTAL
h = {"a" => 1, :a => 2, "b" => 3}
h = {"a" => 1, "b" => 2, "c" => {"a" => 3, "b" => 4}}
h = {} of String => String
CRYSTAL
end
it "fails if there is a duplicated key in a hash literal" do
expect_issue subject, <<-CRYSTAL
h = {"a" => 1, "b" => 2, "a" => 3}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicated keys in hash literal: `"a"`
CRYSTAL
end
it "fails if there is a duplicated key in the inner hash literal" do
expect_issue subject, <<-CRYSTAL
h = {"a" => 1, "b" => {"a" => 3, "b" => 4, "a" => 5}}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicated keys in hash literal: `"a"`
CRYSTAL
end
it "reports multiple duplicated keys" do
expect_issue subject, <<-CRYSTAL
h = {"key1" => 1, "key1" => 2, "key2" => 3, "key2" => 4}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Duplicated keys in hash literal: `"key1"`, `"key2"`
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/literal_assignments_in_expressions_spec.cr
================================================
require "../../../spec_helper"
LITERAL_SAMPLES = {
nil, true, 42, 4.2, 'c', "foo", :foo, /foo/,
0..42, [1, 2, 3], {1, 2, 3},
{foo: :bar}, {:foo => :bar},
}
module Ameba::Rule::Lint
describe LiteralAssignmentsInExpressions do
subject = LiteralAssignmentsInExpressions.new
it "passes if the assignment value is not a literal" do
expect_no_issues subject, <<-CRYSTAL
if a = b
:ok
end
unless a = b.presence
:ok
end
:ok if a = b
:ok unless a = b
case {a, b}
when {0, 1} then :gt
when {1, 0} then :lt
end
CRYSTAL
end
{% for literal in LITERAL_SAMPLES %}
it %(reports if the assignment value is a {{ literal }} literal) do
expect_issue subject, <<-CRYSTAL, literal: {{ literal.stringify }}
raise "boo!" if foo = {{ literal }}
# ^{literal}^^^^^^ error: Detected assignment with a literal value in control expression
CRYSTAL
expect_issue subject, <<-CRYSTAL, literal: {{ literal.stringify }}
raise "boo!" unless foo = {{ literal }}
# ^{literal}^^^^^^ error: Detected assignment with a literal value in control expression
CRYSTAL
end
{% end %}
end
end
================================================
FILE: spec/ameba/rule/lint/literal_in_condition_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe LiteralInCondition do
subject = LiteralInCondition.new
it "passes if there is not literals in conditional" do
expect_no_issues subject, <<-CRYSTAL
if a == 2
:ok
end
:ok unless b
case string
when "a"
:ok
when "b"
:ok
end
unless a.nil?
:ok
end
CRYSTAL
end
it "fails if there is a predicate with non-literals" do
expect_issue subject, <<-CRYSTAL
:ok if [foo, bar]
# ^^^^^^^^^^ error: Literal value found in conditional
:ok unless [foo, bar]
# ^^^^^^^^^^ error: Literal value found in conditional
while [foo, bar]
# ^^^^^^^^^^ error: Literal value found in conditional
:ok
end
until [foo, bar]
# ^^^^^^^^^^ error: Literal value found in conditional
:ok
end
CRYSTAL
end
it "fails if there is a predicate in `if` conditional" do
expect_issue subject, <<-CRYSTAL
if "string"
# ^^^^^^^^ error: Literal value found in conditional
:ok
end
CRYSTAL
end
it "fails if there is a predicate in `unless` conditional" do
expect_issue subject, <<-CRYSTAL
unless true
# ^^^^ error: Literal value found in conditional
:ok
end
CRYSTAL
end
it "fails if there is a predicate in `while` conditional" do
expect_issue subject, <<-CRYSTAL
while 1
# ^ error: Literal value found in conditional
:ok
end
CRYSTAL
end
it "fails if there is a `false` predicate in `while` conditional" do
expect_issue subject, <<-CRYSTAL
while false
# ^^^^^ error: Literal value found in conditional
:ok
end
CRYSTAL
end
it "passes if there is a `true` predicate in `while` conditional" do
expect_no_issues subject, <<-CRYSTAL
while true
:ok
end
CRYSTAL
end
it "fails if there is a predicate in `until` conditional" do
expect_issue subject, <<-CRYSTAL
until true
# ^^^^ error: Literal value found in conditional
:foo
end
CRYSTAL
end
describe "range" do
it "reports range with literals" do
expect_issue subject, <<-CRYSTAL
case 1..2
# ^^^^ error: Literal value found in conditional
end
CRYSTAL
end
it "doesn't report range with non-literals" do
expect_no_issues subject, <<-CRYSTAL
case (1..a)
end
CRYSTAL
end
end
describe "array" do
it "reports array with literals" do
expect_issue subject, <<-CRYSTAL
case [1, 2, 3]
# ^^^^^^^^^ error: Literal value found in conditional
when :array
:ok
when :not_array
:also_ok
end
CRYSTAL
end
it "doesn't report array with non-literals" do
expect_no_issues subject, <<-CRYSTAL
a, b = 1, 2
case [1, 2, a]
when :array
:ok
when :not_array
:also_ok
end
CRYSTAL
end
end
describe "hash" do
it "reports hash with literals" do
expect_issue subject, <<-CRYSTAL
case { "name" => 1, 33 => 'b' }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Literal value found in conditional
when :hash
:ok
end
CRYSTAL
end
it "doesn't report hash with non-literals in keys" do
expect_no_issues subject, <<-CRYSTAL
case { a => 1, 33 => 'b' }
when :hash
:ok
end
CRYSTAL
end
it "doesn't report hash with non-literals in values" do
expect_no_issues subject, <<-CRYSTAL
case { "name" => a, 33 => 'b' }
when :hash
:ok
end
CRYSTAL
end
end
describe "tuple" do
it "reports tuple with literals" do
expect_issue subject, <<-CRYSTAL
case {1, false}
# ^^^^^^^^^^ error: Literal value found in conditional
when {1, _}
:ok
end
CRYSTAL
end
it "doesn't report tuple with non-literals" do
expect_no_issues subject, <<-CRYSTAL
a, b = 1, 2
case {1, b}
when {1, 2}
:ok
end
CRYSTAL
end
end
describe "named tuple" do
it "reports named tuple with literals" do
expect_issue subject, <<-CRYSTAL
case { name: 1, foo: :bar}
# ^^^^^^^^^^^^^^^^^^^^^ error: Literal value found in conditional
end
CRYSTAL
end
it "doesn't report named tuple with non-literals" do
expect_no_issues subject, <<-CRYSTAL
case { name: a, foo: :bar}
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/literal_in_interpolation_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe LiteralInInterpolation do
subject = LiteralInInterpolation.new
it "passes with good interpolation examples" do
expect_no_issues subject, <<-'CRYSTAL'
"Hello, #{name}"
"#{name}"
"Name size: #{name.size}"
"#{foo..}"
CRYSTAL
end
it "fails if there is useless interpolation" do
[
%q("#{:Ary}"),
%q("#{[1, 2, 3]}"),
%q("#{true}"),
%q("#{false}"),
%q("here are #{4} cats"),
].each do |str|
subject.catch(Source.new str).should_not be_valid
end
end
it "works with magic constants (#593)" do
expect_no_issues subject, <<-'CRYSTAL', "/home/foo/source.cr"
"Hello from #{__FILE__} at line #{__LINE__} in #{__DIR__}"
CRYSTAL
end
it "reports if there is a literal in interpolation" do
expect_issue subject, <<-'CRYSTAL'
"Hello, #{:world} from #{:ameba}"
# ^^^^^^ error: Literal value found in interpolation
# ^^^^^^ error: Literal value found in interpolation
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/literals_comparison_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe LiteralsComparison do
subject = LiteralsComparison.new
it "passes for valid cases" do
expect_no_issues subject, <<-'CRYSTAL'
"foo" == foo
"foo" != foo
"foo" == FOO
FOO == "foo"
foo == "foo"
foo != "foo"
{start.year, start.month} == {stop.year, stop.month}
/foo/ =~ "foo#{bar}"
/foo/ !~ "foo#{bar}"
["foo"] === [bar]
[foo] === ["bar"]
[foo] === [bar]
[foo] == [bar]
[foo] == [foo]
CRYSTAL
end
it "reports if there is a static comparison evaluating to the same" do
expect_issue subject, <<-CRYSTAL
"foo" === "bar"
# ^^^^^^^^^^^^^ error: Comparison always evaluates to the same
/foo/ =~ "bar"
# ^^^^^^^^^^^^ error: Comparison always evaluates to the same
"foo" <=> "bar"
# ^^^^^^^^^^^^^ error: Comparison always evaluates to the same
CRYSTAL
end
it "reports if there is a static comparison evaluating to true" do
expect_issue subject, <<-CRYSTAL
"foo" == "foo"
# ^^^^^^^^^^^^ error: Comparison always evaluates to `true`
"foo" != "bar"
# ^^^^^^^^^^^^ error: Comparison always evaluates to `true`
CRYSTAL
end
it "reports if there is a static comparison evaluating to false" do
expect_issue subject, <<-CRYSTAL
"foo" == "bar"
# ^^^^^^^^^^^^ error: Comparison always evaluates to `false`
"foo" != "foo"
# ^^^^^^^^^^^^ error: Comparison always evaluates to `false`
CRYSTAL
end
context "macro" do
it "reports in macro scope" do
expect_issue subject, <<-CRYSTAL
{{ "foo" == "bar" }}
# ^^^^^^^^^^^^^^ error: Comparison always evaluates to `false`
CRYSTAL
end
it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL
{{ "foo" == foo }}
{{ "foo" != foo }}
{% foo == "foo" %}
{% foo != "foo" %}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/missing_block_argument_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe MissingBlockArgument do
subject = MissingBlockArgument.new
it "passes if the block argument is defined" do
expect_no_issues subject, <<-CRYSTAL
def foo(&)
yield 42
end
def bar(&block)
yield 24
end
def baz(a, b, c, &block)
yield a, b, c
end
CRYSTAL
end
it "reports if the block argument is missing" do
expect_issue subject, <<-CRYSTAL
def foo
# ^^^ error: Missing anonymous block argument. Use `&` as an argument name to indicate yielding method.
yield 42
end
def bar
# ^^^ error: Missing anonymous block argument. Use `&` as an argument name to indicate yielding method.
yield 24
end
def baz(a, b, c)
# ^^^ error: Missing anonymous block argument. Use `&` as an argument name to indicate yielding method.
yield a, b, c
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/non_existent_rule_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe NonExistentRule do
subject = NonExistentRule.new
it "does not report if the rule name is correct" do
expect_no_issues subject, <<-CRYSTAL
# ameba:disable Lint/NonExistentRule
CRYSTAL
end
it "reports if there are incorrect rule names" do
expect_issue subject, <<-CRYSTAL
# ameba:disable BadRule1, BadRule2
# ^^^^^^^^^^^^^^^^^^ error: Such rules do not exist: `BadRule1`, `BadRule2`
CRYSTAL
end
it "does not report if there no action and rules at all" do
expect_no_issues subject, <<-CRYSTAL
# ameba:
CRYSTAL
end
it "does not report if there are no rules" do
expect_no_issues subject, <<-CRYSTAL
# ameba:enable
# ameba:disable
CRYSTAL
end
it "does not report if there are group names in the directive" do
expect_no_issues subject, <<-CRYSTAL
# ameba:disable Style Performance
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/not_nil_after_no_bang_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe NotNilAfterNoBang do
subject = NotNilAfterNoBang.new
it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL
(1..3).index(1).not_nil!(:foo)
(1..3).rindex(1).not_nil!(:foo)
(1..3).index { |i| i > 2 }.not_nil!(:foo)
(1..3).rindex { |i| i > 2 }.not_nil!(:foo)
(1..3).find { |i| i > 2 }.not_nil!(:foo)
/(.)(.)(.)/.match("abc", &.itself).not_nil!
/(.)(.)(.)/.match("abc", &foo).not_nil!
CRYSTAL
end
it "reports if there is an `index` call followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).index(1).not_nil!
# ^^^^^^^^^^^^^^^^^ error: Use `index! {...}` instead of `index {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).index!(1)
CRYSTAL
end
it "reports if there is an `rindex` call followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).rindex(1).not_nil!
# ^^^^^^^^^^^^^^^^^^ error: Use `rindex! {...}` instead of `rindex {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).rindex!(1)
CRYSTAL
end
it "reports if there is an `match` call followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
/(.)(.)(.)/.match("abc").not_nil![2]
# ^^^^^^^^^^^^^^^^^^^^^ error: Use `match! {...}` instead of `match {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
/(.)(.)(.)/.match!("abc")[2]
CRYSTAL
end
it "reports if there is an `index` call with block followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).index { |i| i > 2 }.not_nil!
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `index! {...}` instead of `index {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).index! { |i| i > 2 }
CRYSTAL
end
it "reports if there is an `rindex` call with block followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).rindex { |i| i > 2 }.not_nil!
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `rindex! {...}` instead of `rindex {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).rindex! { |i| i > 2 }
CRYSTAL
end
it "reports if there is a `find` call with block followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).find { |i| i > 2 }.not_nil!
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `find! {...}` instead of `find {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).find! { |i| i > 2 }
CRYSTAL
end
it "passes if there is a `find` call without block followed by `not_nil!`" do
expect_no_issues subject, <<-CRYSTAL
(1..3).find(1).not_nil!
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].index(1).not_nil! }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/not_nil_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe NotNil do
subject = NotNil.new
it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL
(1..3).first?.not_nil!(:foo)
not_nil!
CRYSTAL
end
it "reports if there is a `not_nil!` call" do
expect_issue subject, <<-CRYSTAL
(1..3).first?.not_nil!
# ^^^^^^^^ error: Avoid using `not_nil!`
CRYSTAL
end
it "reports if there is a `not_nil!` call in the middle of the call-chain" do
expect_issue subject, <<-CRYSTAL
(1..3).first?.not_nil!.to_s
# ^^^^^^^^ error: Avoid using `not_nil!`
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].first.not_nil! }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/percent_arrays_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe PercentArrays do
subject = PercentArrays.new
it "passes if percent arrays are written correctly" do
expect_no_issues subject, <<-CRYSTAL
%w[one two three]
%w[1 2 3]
%w[]
%i[one two three]
%i[1 2 3]
%i[]
CRYSTAL
end
it "fails if string percent array has commas" do
expect_issue subject, <<-CRYSTAL
puts %w[one, two]
# ^^ error: Symbols `,"` may be unwanted in `%w` array literals
CRYSTAL
end
it "fails if string percent array has quotes" do
expect_issue subject, <<-CRYSTAL
puts %w["one" "two"]
# ^^ error: Symbols `,"` may be unwanted in `%w` array literals
CRYSTAL
end
it "fails if symbols percent array has commas" do
expect_issue subject, <<-CRYSTAL
puts %i[one, two]
# ^^ error: Symbols `,:` may be unwanted in `%i` array literals
CRYSTAL
end
it "fails if symbols percent array has a colon" do
expect_issue subject, <<-CRYSTAL
puts %i[:one :two]
# ^^ error: Symbols `,:` may be unwanted in `%i` array literals
CRYSTAL
end
context "properties" do
it "#string_array_unwanted_symbols" do
rule = PercentArrays.new
rule.string_array_unwanted_symbols = ","
expect_no_issues rule, %(%w[one])
end
it "#symbol_array_unwanted_symbols" do
rule = PercentArrays.new
rule.symbol_array_unwanted_symbols = ","
expect_no_issues rule, %(%i[:one])
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/rand_zero_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe RandZero do
subject = RandZero.new
it "passes if it is not rand(1) or rand(0)" do
expect_no_issues subject, <<-CRYSTAL
rand(1.0)
rand(0.11)
rand(2)
CRYSTAL
end
it "fails if it is rand(0)" do
expect_issue subject, <<-CRYSTAL
rand(0)
# ^^^^^ error: `rand(0)` always returns `0`
CRYSTAL
end
it "fails if it is rand(1)" do
expect_issue subject, <<-CRYSTAL
rand(1)
# ^^^^^ error: `rand(1)` always returns `0`
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/redundant_string_cercion_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe RedundantStringCoercion do
subject = RedundantStringCoercion.new
it "does not report if there is no redundant string coercion" do
expect_no_issues subject, <<-'CRYSTAL'
"Hello, #{name}"
CRYSTAL
end
it "does not report if coercion is used in binary op" do
expect_no_issues subject, <<-'CRYSTAL'
"Hello, #{3.to_s + 's'}"
CRYSTAL
end
{% for v in %w[name :symbol 42 false 't'] %}
it "reports if there is a redundant string coercion ({{ v.id }})" do
expect_issue subject, <<-'CRYSTAL', v: {{ v }}
"Hello, #{%{v}.to_s}"
_{v} # ^^^^ error: Redundant use of `Object#to_s` in interpolation
CRYSTAL
end
{% end %}
it "reports redundant coercion in regex" do
expect_issue subject, <<-'CRYSTAL'
/\w #{name.to_s}/
# ^^^^ error: Redundant use of `Object#to_s` in interpolation
CRYSTAL
end
it "doesn't report if Object#to_s is called with arguments" do
expect_no_issues subject, <<-'CRYSTAL'
/\w #{name.to_s(io)}/
CRYSTAL
end
it "doesn't report if Object#to_s is called without receiver" do
expect_no_issues subject, <<-'CRYSTAL'
/\w #{to_s}/
CRYSTAL
end
it "doesn't report if Object#to_s is called with named args" do
expect_no_issues subject, <<-'CRYSTAL'
"0x#{250.to_s(base: 16)}"
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/redundant_with_index_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe RedundantWithIndex do
subject = RedundantWithIndex.new
context "with_index" do
it "does not report if there is index argument" do
expect_no_issues subject, <<-CRYSTAL
collection.each.with_index do |e, i|
e += i
end
CRYSTAL
end
it "reports if there is no index argument" do
expect_issue subject, <<-CRYSTAL
collection.each.with_index do |e|
# ^^^^^^^^^^ error: Remove redundant `with_index`
e += 1
end
CRYSTAL
end
it "reports if there is an underscored index argument" do
expect_issue subject, <<-CRYSTAL
collection.each.with_index do |e, _|
# ^^^^^^^^^^ error: Remove redundant `with_index`
e += 1
end
CRYSTAL
end
it "reports if there is no args" do
expect_issue subject, <<-CRYSTAL
collection.each.with_index do
# ^^^^^^^^^^ error: Remove redundant `with_index`
puts :nothing
end
CRYSTAL
end
it "does not report if there is no block" do
expect_no_issues subject, <<-CRYSTAL
collection.each.with_index
CRYSTAL
end
it "does not report if first argument is underscored" do
expect_no_issues subject, <<-CRYSTAL
collection.each.with_index do |_, i|
puts i
end
CRYSTAL
end
it "does not report if there are more than 2 args" do
expect_no_issues subject, <<-CRYSTAL
tup.each.with_index do |key, value, index|
puts i
end
CRYSTAL
end
end
context "each_with_index" do
it "does not report if there is index argument" do
expect_no_issues subject, <<-CRYSTAL
collection.each_with_index do |e, i|
e += i
end
CRYSTAL
end
it "reports if there is not index argument" do
expect_issue subject, <<-CRYSTAL
collection.each_with_index do |e|
# ^^^^^^^^^^^^^^^ error: Use `each` instead of `each_with_index`
e += 1
end
CRYSTAL
end
it "reports if there is underscored index argument" do
expect_issue subject, <<-CRYSTAL
collection.each_with_index do |e, _|
# ^^^^^^^^^^^^^^^ error: Use `each` instead of `each_with_index`
e += 1
end
CRYSTAL
end
it "reports if there is no args" do
expect_issue subject, <<-CRYSTAL
collection.each_with_index do
# ^^^^^^^^^^^^^^^ error: Use `each` instead of `each_with_index`
puts :nothing
end
CRYSTAL
end
it "does not report if there is no block" do
expect_no_issues subject, <<-CRYSTAL
collection.each_with_index(1)
CRYSTAL
end
it "does not report if first argument is underscored" do
expect_no_issues subject, <<-CRYSTAL
collection.each_with_index do |_, i|
puts i
end
CRYSTAL
end
it "does not report if there are more than 2 args" do
expect_no_issues subject, <<-CRYSTAL
tup.each_with_index do |key, value, index|
puts i
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/redundant_with_object_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe RedundantWithObject do
subject = RedundantWithObject.new
it "does not report if there is index argument" do
expect_no_issues subject, <<-CRYSTAL
collection.each_with_object(0) do |e, obj|
obj += i
end
CRYSTAL
end
it "reports if there is not index argument" do
expect_issue subject, <<-CRYSTAL
collection.each_with_object(0) do |e|
# ^^^^^^^^^^^^^^^^ error: Use `each` instead of `each_with_object`
e += 1
end
CRYSTAL
end
it "reports if there is underscored index argument" do
expect_issue subject, <<-CRYSTAL
collection.each_with_object(0) do |e, _|
# ^^^^^^^^^^^^^^^^ error: Use `each` instead of `each_with_object`
e += 1
end
CRYSTAL
end
it "reports if there is no args" do
expect_issue subject, <<-CRYSTAL
collection.each_with_object(0) do
# ^^^^^^^^^^^^^^^^ error: Use `each` instead of `each_with_object`
puts :nothing
end
CRYSTAL
end
it "does not report if there is no block" do
expect_no_issues subject, <<-CRYSTAL
collection.each_with_object(0)
CRYSTAL
end
it "does not report if first argument is underscored" do
expect_no_issues subject, <<-CRYSTAL
collection.each_with_object(0) do |_, obj|
puts i
end
CRYSTAL
end
it "does not report if there are more than 2 args" do
expect_no_issues subject, <<-CRYSTAL
tup.each_with_object(0) do |key, value, obj|
puts i
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/require_parentheses_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe RequireParentheses do
subject = RequireParentheses.new
it "passes if logical operator in call args has parentheses" do
expect_no_issues subject, <<-CRYSTAL
foo.includes?("bar") || foo.includes?("baz")
foo.includes?("bar" || foo.includes? "baz")
CRYSTAL
end
it "passes if logical operator in call doesn't involve another method call" do
expect_no_issues subject, <<-CRYSTAL
foo.includes? "bar" || "baz"
CRYSTAL
end
it "passes if logical operator in call involves another method call with no arguments" do
expect_no_issues subject, <<-CRYSTAL
foo.includes? "bar" || foo.not_nil!
CRYSTAL
end
it "passes if logical operator is used in an assignment call" do
expect_no_issues subject, <<-CRYSTAL
foo.bar = "baz" || bat.call :foo
foo.bar ||= "baz" || bat.call :foo
foo[bar] = "baz" || bat.call :foo
CRYSTAL
end
it "passes if logical operator is used in a square bracket call" do
expect_no_issues subject, <<-CRYSTAL
foo["bar" || baz.call :bat]
foo["bar" || baz.call :bat]?
CRYSTAL
end
it "fails if logical operator in call args doesn't have parentheses" do
expect_issue subject, <<-CRYSTAL
foo.includes? "bar" || foo.includes? "baz"
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use parentheses in the method call to avoid confusion about precedence
foo.in? "bar", "baz" || foo.ends_with? "bat"
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use parentheses in the method call to avoid confusion about precedence
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/self_initialize_definition_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SelfInitializeDefinition do
subject = SelfInitializeDefinition.new
{% for keyword in %w[module enum].map(&.id) %}
context "{{ keyword }}" do
it "passes for `initialize` method definition with a `self` receiver" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} Foo
def self.initialize
end
end
CRYSTAL
end
end
{% end %}
{% for keyword in %w[struct class].map(&.id) %}
context "{{ keyword }}" do
it "passes for `initialize` method definition without a receiver" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} Foo
def initialize
end
end
CRYSTAL
end
it "passes for `initialize` method definition with an explicit receiver" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} Foo
end
def Foo.initialize
end
CRYSTAL
end
it "fails for `initialize` method definition with a `self` receiver" do
source = expect_issue subject, <<-CRYSTAL
{{ keyword }} Foo
def self.initialize
# ^^^^^^^^^^^^^^^^^^^ error: `initialize` method definition should not have a receiver
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
{{ keyword }} Foo
def initialize
end
end
CRYSTAL
end
end
{% end %}
end
end
================================================
FILE: spec/ameba/rule/lint/shadowed_argument_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe ShadowedArgument do
subject = ShadowedArgument.new
it "doesn't report if there is not a shadowed argument" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
baz = 1
end
3.times do |i|
a = 1
end
proc = -> (a : Int32) {
b = 2
}
CRYSTAL
end
it "reports if there is a shadowed method argument" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
bar = 1
# ^^^^^^^ error: Argument `bar` is assigned before it is used
bar
end
CRYSTAL
end
it "reports if there is a shadowed block argument" do
expect_issue subject, <<-CRYSTAL
3.times do |i|
i = 2
# ^^^^^ error: Argument `i` is assigned before it is used
end
CRYSTAL
end
it "reports if there is a shadowed proc argument" do
expect_issue subject, <<-CRYSTAL
-> (x : Int32) {
x = 20
# ^^^^^^ error: Argument `x` is assigned before it is used
x
}
CRYSTAL
end
it "doesn't report if the argument is referenced before the assignment" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar
bar = 1
end
CRYSTAL
end
it "doesn't report if the argument is conditionally reassigned" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar = nil)
bar ||= true
bar
end
CRYSTAL
end
it "doesn't report if the op assign is followed by another assignment" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar ||= 3
bar = 43
bar
end
CRYSTAL
end
it "reports if the shadowing assignment is followed by op assign" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
bar = 42
# ^^^^^^^^ error: Argument `bar` is assigned before it is used
bar ||= 43
bar
end
CRYSTAL
end
it "doesn't report if the argument is unused" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
end
CRYSTAL
end
it "doesn't report if the argument is reassigned from super result" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar = super
bar
end
CRYSTAL
end
it "doesn't report if the argument is reassigned from previous_def result" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar = previous_def
bar
end
CRYSTAL
end
it "reports if the argument is shadowed before super" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
bar = 1
# ^^^^^^^ error: Argument `bar` is assigned before it is used
super
end
CRYSTAL
end
context "branch" do
it "doesn't report if the argument is not shadowed in a condition" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar, baz)
bar = 1 if baz
bar
end
CRYSTAL
end
it "reports if the argument is shadowed after the condition" do
expect_issue subject, <<-CRYSTAL
def foo(foo)
if something
foo = 42
end
foo = 43
# ^^^^^^^^ error: Argument `foo` is assigned before it is used
foo
end
CRYSTAL
end
it "doesn't report if the argument is conditionally assigned in a branch" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
if something
bar ||= 22
end
bar
end
CRYSTAL
end
end
context "inner scopes" do
it "doesn't report if the argument is used in an inner block" do
expect_no_issues subject, <<-CRYSTAL
def foo(catch_all = false)
items.each do |item|
if catch_all
do_something
end
catch_all = true
end
end
CRYSTAL
end
it "doesn't report if the argument is captured by a block" do
expect_no_issues subject, <<-CRYSTAL
def foo(token)
loop do
token = next_token(token.state)
process(token)
break if token.eof?
end
end
CRYSTAL
end
it "doesn't report if the argument is referenced in an inner scope" do
expect_no_issues subject, <<-CRYSTAL
def foo(x)
x = 1
3.times { puts x }
end
CRYSTAL
end
it "doesn't report if the argument is used in a macro" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar = 1
{% if flag?(:release) %}
use(bar)
{% end %}
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/shadowed_exception_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe ShadowedException do
subject = ShadowedException.new
it "passes if there isn't shadowed exception" do
expect_no_issues subject, <<-CRYSTAL
def method
do_something
rescue ArgumentError
handle_argument_error_exception
rescue Exception
handle_exception
end
def method
rescue Exception
handle_exception
end
def method
rescue ex : ArgumentError
handle_argument_error_exception
rescue ex : Exception
handle_exception
end
CRYSTAL
end
it "fails if there is a shadowed exception" do
expect_issue subject, <<-CRYSTAL
begin
do_something
rescue Exception
handle_exception
rescue ArgumentError
# ^^^^^^^^^^^^^ error: Shadowed exception found: `ArgumentError`
handle_argument_error_exception
end
CRYSTAL
end
it "fails if there is a custom shadowed exceptions" do
expect_issue subject, <<-CRYSTAL
begin
1
rescue Exception
2
rescue MySuperException
# ^^^^^^^^^^^^^^^^ error: Shadowed exception found: `MySuperException`
3
end
CRYSTAL
end
it "fails if there is a shadowed exception in a type list" do
expect_issue subject, <<-CRYSTAL
begin
rescue Exception | IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
end
CRYSTAL
end
it "fails if there is a first shadowed exception in a type list" do
expect_issue subject, <<-CRYSTAL
begin
rescue IndexError | Exception
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
rescue Exception
# ^^^^^^^^^ error: Shadowed exception found: `Exception`
rescue
end
CRYSTAL
end
it "fails if there is a shadowed duplicated exception" do
expect_issue subject, <<-CRYSTAL
begin
rescue IndexError
rescue ArgumentError
rescue IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
end
CRYSTAL
end
it "fails if there is a shadowed duplicated exception in a type list" do
expect_issue subject, <<-CRYSTAL
begin
rescue IndexError
rescue ArgumentError | IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
end
CRYSTAL
end
it "fails if there is only shadowed duplicated exceptions" do
expect_issue subject, <<-CRYSTAL
begin
rescue IndexError
rescue IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
rescue Exception
end
CRYSTAL
end
it "fails if there is only shadowed duplicated exceptions in a type list" do
expect_issue subject, <<-CRYSTAL
begin
rescue IndexError | IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
end
CRYSTAL
end
it "fails if all rescues are shadowed and there is a catch-all rescue" do
expect_issue subject, <<-CRYSTAL
begin
rescue Exception
rescue ArgumentError
# ^^^^^^^^^^^^^ error: Shadowed exception found: `ArgumentError`
rescue IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
rescue KeyError | IO::Error
# ^^^^^^^^^ error: Shadowed exception found: `IO::Error`
# ^^^^^^^^ error: Shadowed exception found: `KeyError`
rescue
end
CRYSTAL
end
it "fails if there are shadowed exception with args" do
expect_issue subject, <<-CRYSTAL
begin
rescue Exception
rescue ex : IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
rescue
end
CRYSTAL
end
it "fails if there are multiple shadowed exceptions" do
expect_issue subject, <<-CRYSTAL
begin
rescue Exception
rescue ArgumentError
# ^^^^^^^^^^^^^ error: Shadowed exception found: `ArgumentError`
rescue IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
end
CRYSTAL
end
it "fails if there are multiple shadowed exceptions in a type list" do
expect_issue subject, <<-CRYSTAL
begin
rescue Exception
rescue ArgumentError | IndexError
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
# ^^^^^^^^^^^^^ error: Shadowed exception found: `ArgumentError`
rescue IO::Error
# ^^^^^^^^^ error: Shadowed exception found: `IO::Error`
end
CRYSTAL
end
it "fails if there are multiple shadowed exceptions in a single rescue" do
expect_issue subject, <<-CRYSTAL
begin
do_something
rescue Exception | IndexError | ArgumentError
# ^^^^^^^^^^^^^ error: Shadowed exception found: `ArgumentError`
# ^^^^^^^^^^ error: Shadowed exception found: `IndexError`
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/shadowing_outer_local_var_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe ShadowingOuterLocalVar do
subject = ShadowingOuterLocalVar.new
it "doesn't report if there is no shadowing" do
expect_no_issues subject, <<-CRYSTAL
def some_method
foo = 1
3.times do |bar|
bar
end
-> (baz : Int32) { }
-> (bar : String) { }
end
CRYSTAL
end
it "reports if there is a shadowing in a block" do
expect_issue subject, <<-CRYSTAL
def some_method
foo = 1
3.times do |foo|
# ^^^ error: Shadowing outer local variable `foo`
end
end
CRYSTAL
end
pending "reports if there is a shadowing in an unpacked variable in a block" do
expect_issue subject, <<-CRYSTAL
def some_method
foo = 1
[{3}].each do |(foo)|
# ^^^ error: Shadowing outer local variable `foo`
end
end
CRYSTAL
end
pending "reports if there is a shadowing in an unpacked variable in a block (2)" do
expect_issue subject, <<-CRYSTAL
def some_method
foo = 1
[{[3]}].each do |((foo))|
# ^^^ error: Shadowing outer local variable `foo`
end
end
CRYSTAL
end
it "does not report outer vars declared below shadowed block" do
expect_no_issues subject, <<-CRYSTAL
methods = klass.methods.select { |m| m.annotation(MyAnn) }
m = methods.last
CRYSTAL
end
it "reports if there is a shadowing in a proc" do
expect_issue subject, <<-CRYSTAL
def some_method
foo = 1
-> (foo : Int32) { }
# ^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end
it "reports if there is a shadowing in an inner scope" do
expect_issue subject, <<-CRYSTAL
def foo
foo = 1
3.times do |i|
3.times { |foo| foo }
# ^^^ error: Shadowing outer local variable `foo`
end
end
CRYSTAL
end
it "reports if variable is shadowed twice" do
expect_issue subject, <<-CRYSTAL
foo = 1
3.times do |foo|
# ^^^ error: Shadowing outer local variable `foo`
-> (foo : Int32) { foo + 1 }
# ^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end
it "reports if a splat block argument shadows local var" do
expect_issue subject, <<-CRYSTAL
foo = 1
3.times do |*foo|
# ^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end
it "reports if a &block argument is shadowed" do
expect_issue subject, <<-CRYSTAL
def method_with_block(a, &block)
3.times do |block|
# ^^^^^ error: Shadowing outer local variable `block`
end
end
CRYSTAL
end
it "reports if there are multiple args and one shadows local var" do
expect_issue subject, <<-CRYSTAL
foo = 1
[1, 2, 3].each_with_index do |i, foo|
# ^^^ error: Shadowing outer local variable `foo`
i + foo
end
CRYSTAL
end
it "doesn't report if an outer var is reassigned in a block" do
expect_no_issues subject, <<-CRYSTAL
def foo
foo = 1
3.times do |i|
foo = 2
end
end
CRYSTAL
end
it "doesn't report if an argument is a black hole '_'" do
expect_no_issues subject, <<-CRYSTAL
_ = 1
3.times do |_|
end
CRYSTAL
end
it "doesn't report if it shadows record type declaration" do
expect_no_issues subject, <<-CRYSTAL
class FooBar
record Foo, index : String
def bar
3.times do |index|
end
end
end
CRYSTAL
end
it "doesn't report if it shadows type declaration" do
expect_no_issues subject, <<-CRYSTAL
class FooBar
getter index : String
def bar
3.times do |index|
end
end
end
CRYSTAL
end
it "doesn't report if it shadows throwaway arguments" do
expect_no_issues subject, <<-CRYSTAL
data = [{1, "a"}, {2, "b"}, {3, "c"}]
data.each do |_, string|
data.each do |number, _|
puts string, number
end
end
CRYSTAL
end
it "does not report if argument shadows an ivar assignment" do
expect_no_issues subject, <<-CRYSTAL
def bar(@foo)
@foo.try do |foo|
end
end
CRYSTAL
end
context "macro" do
it "does not report shadowed vars in outer scope" do
expect_no_issues subject, <<-CRYSTAL
macro included
def foo
{% for ivar in instance_vars %}
{% ann = ivar.annotation(Name) %}
{% end %}
end
def bar
{% instance_vars.reject { |ivar| ivar } %}
end
end
CRYSTAL
end
it "does not report shadowed vars in macro within the same scope" do
expect_no_issues subject, <<-CRYSTAL
{% methods = klass.methods.select { |m| m.annotation(MyAnn) } %}
{% for m, m_idx in methods %}
{% if d = m.annotation(MyAnn) %}
{% d %}
{% end %}
{% end %}
CRYSTAL
end
it "does not report shadowed vars within nested macro" do
expect_no_issues subject, <<-CRYSTAL
module Foo
macro included
def foo
{% for ann in instance_vars %}
{% pos_args = ann.args.empty? ? "Tuple.new".id : ann.args %}
{% end %}
end
def bar
{{
@type.instance_vars.map do |ivar|
ivar.annotations(Name).each do |ann|
puts ann.args
end
end
}}
end
end
end
CRYSTAL
end
it "does not report scoped vars to MacroFor" do
expect_no_issues subject, <<-CRYSTAL
struct Test
def test
{% for ivar in @type.instance_vars %}
{% var_type = ivar %}
{% end %}
{% ["a", "b"].map { |ivar| puts ivar } %}
end
end
CRYSTAL
end
# https://github.com/crystal-ameba/ameba/issues/224#issuecomment-822245167
it "does not report scoped vars to MacroFor (2)" do
expect_no_issues subject, <<-CRYSTAL
struct Test
def test
{% begin %}
{% for ivar in @type.instance_vars %}
{% var_type = ivar %}
{% end %}
{% ["a", "b"].map { |ivar| puts ivar } %}
{% end %}
end
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/shared_var_in_fiber_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SharedVarInFiber do
subject = SharedVarInFiber.new
it "doesn't report if there is only local shared var in fiber" do
expect_no_issues subject, <<-CRYSTAL
spawn do
i = 1
puts i
end
Fiber.yield
CRYSTAL
end
it "doesn't report if there is only block shared var in fiber" do
expect_no_issues subject, <<-CRYSTAL
10.times do |i|
spawn do
puts i
end
end
Fiber.yield
CRYSTAL
end
it "doesn't report if there a spawn macro is used" do
expect_no_issues subject, <<-CRYSTAL
i = 0
while i < 10
spawn puts(i)
i += 1
end
Fiber.yield
CRYSTAL
end
it "reports if there is a shared var in spawn (while)" do
source = expect_issue subject, <<-CRYSTAL
idx = 0
while idx < 10
spawn do
puts(idx)
# ^^^ error: Shared variable `idx` is used in fiber
end
idx += 1
end
Fiber.yield
CRYSTAL
expect_no_corrections source
end
it "reports if there is a shared var in spawn (loop)" do
source = expect_issue subject, <<-CRYSTAL
i = 0
loop do
break if i >= 10
spawn do
puts(i)
# ^ error: Shared variable `i` is used in fiber
end
i += 1
end
Fiber.yield
CRYSTAL
expect_no_corrections source
end
it "reports reassigned reference to shared var in spawn" do
source = expect_issue subject, <<-CRYSTAL
channel = Channel(String).new
num = 0
while num < 10
num = num + 1
spawn do
m = num
# ^^^ error: Shared variable `num` is used in fiber
channel.send m
end
end
CRYSTAL
expect_no_corrections source
end
it "doesn't report reassigned reference to shared var in block" do
expect_no_issues subject, <<-CRYSTAL
channel = Channel(String).new
n = 0
while n < 3
n = n + 1
m = n
spawn do
channel.send m
end
end
CRYSTAL
end
it "does not report block is called in a spawn" do
expect_no_issues subject, <<-CRYSTAL
def method(block)
spawn do
block.call(10)
end
end
CRYSTAL
end
it "reports multiple shared variables in spawn" do
source = expect_issue subject, <<-CRYSTAL
foo, bar, baz = 0, 0, 0
while foo < 10
baz += 1
spawn do
puts foo
# ^^^ error: Shared variable `foo` is used in fiber
puts foo + bar + baz
# ^^^ error: Shared variable `foo` is used in fiber
# ^^^ error: Shared variable `baz` is used in fiber
end
foo += 1
end
CRYSTAL
expect_no_corrections source
end
it "doesn't report if variable is passed to the proc" do
expect_no_issues subject, <<-CRYSTAL
i = 0
while i < 10
proc = -> (x : Int32) do
spawn do
puts(x)
end
end
proc.call(i)
i += 1
end
CRYSTAL
end
it "doesn't report if a channel is declared in outer scope" do
expect_no_issues subject, <<-CRYSTAL
channel = Channel(Nil).new
spawn { channel.send(nil) }
channel.receive
CRYSTAL
end
it "doesn't report if there is a loop in spawn" do
expect_no_issues subject, <<-CRYSTAL
channel = Channel(String).new
spawn do
server = TCPServer.new("0.0.0.0", 8080)
socket = server.accept
while line = socket.gets
channel.send(line)
end
end
CRYSTAL
end
it "doesn't report if a var is mutated in spawn and referenced outside" do
expect_no_issues subject, <<-CRYSTAL
def method
foo = 1
spawn { foo = 2 }
foo
end
CRYSTAL
end
it "doesn't report if variable is changed without iterations" do
expect_no_issues subject, <<-CRYSTAL
def foo
i = 0
i += 1
spawn { i }
end
CRYSTAL
end
it "doesn't report if variable is in a loop inside spawn" do
expect_no_issues subject, <<-CRYSTAL
i = 0
spawn do
while i < 10
i += 1
end
end
CRYSTAL
end
it "doesn't report if variable declared inside loop" do
expect_no_issues subject, <<-CRYSTAL
while true
i = 0
spawn { i += 1 }
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/signal_trap_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SignalTrap do
subject = SignalTrap.new
it "reports when `Signal::INT/HUP/TERM.trap` is used" do
source = expect_issue subject, <<-CRYSTAL
::Signal::INT.trap { shutdown }
# ^^^^^^^^^^^^^^^^ error: Use `Process.on_terminate` instead of `::Signal::INT.trap`
Signal::HUP.trap { shutdown }
# ^^^^^^^^^^^^^^ error: Use `Process.on_terminate` instead of `Signal::HUP.trap`
Signal::TERM.trap &shutdown
# ^^^^^^^^^^^^^^^ error: Use `Process.on_terminate` instead of `Signal::TERM.trap`
CRYSTAL
expect_correction source, <<-CRYSTAL
Process.on_terminate { shutdown }
Process.on_terminate { shutdown }
Process.on_terminate &shutdown
CRYSTAL
end
it "respects the comment between the path and the call name" do
source = expect_issue subject, <<-CRYSTAL
Signal::INT
# ^^^^^^^^^ error: Use `Process.on_terminate` instead of `Signal::INT.trap`
# foo
.trap { shutdown }
CRYSTAL
expect_correction source, <<-CRYSTAL
Process
# foo
.on_terminate { shutdown }
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/spec_eq_with_bool_or_nil_literal_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SpecEqWithBoolOrNilLiteral do
subject = SpecEqWithBoolOrNilLiteral.new
it "does not report `eq` method calls that are not arguments to `should`" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
foo.bar? eq true
foo.bar? eq false
foo.bar? eq nil
CRYSTAL
end
it "does not report if `be_true` / `be_false` expectation is used" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
foo.is_a?(String).should be_true
foo.is_a?(Int32).should be_false
foo.is_a?(String).should_not be_true
foo.is_a?(Int32).should_not be_false
CRYSTAL
end
it "does not report if `be_nil` expectation is used" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
foo.as?(Symbol).should be_nil
foo.as?(Symbol).should_not be_nil
CRYSTAL
end
it "reports if `eq` expectation with bool literal is used" do
source = expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
foo.is_a?(String).should eq true
# ^^^^^^^ error: Use `be_true` instead of `eq(true)` expectation
foo.is_a?(Int32).should eq false
# ^^^^^^^^ error: Use `be_false` instead of `eq(false)` expectation
foo.is_a?(String).should_not eq true
# ^^^^^^^ error: Use `be_true` instead of `eq(true)` expectation
foo.is_a?(Int32).should_not eq false
# ^^^^^^^^ error: Use `be_false` instead of `eq(false)` expectation
CRYSTAL
expect_correction source, <<-CRYSTAL
foo.is_a?(String).should be_true
foo.is_a?(Int32).should be_false
foo.is_a?(String).should_not be_true
foo.is_a?(Int32).should_not be_false
CRYSTAL
end
it "reports if `eq` expectation with nil literal is used" do
source = expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
foo.as?(Symbol).should eq nil
# ^^^^^^ error: Use `be_nil` instead of `eq(nil)` expectation
foo.as?(Symbol).should_not eq nil
# ^^^^^^ error: Use `be_nil` instead of `eq(nil)` expectation
CRYSTAL
expect_correction source, <<-CRYSTAL
foo.as?(Symbol).should be_nil
foo.as?(Symbol).should_not be_nil
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/spec_filename_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SpecFilename do
subject = SpecFilename.new
it "passes if relative file path does not start with `spec/`" do
expect_no_issues subject, code: "", path: "src/spec/foo.cr"
expect_no_issues subject, code: "", path: "src/spec/foo/bar.cr"
end
it "passes if file extension is not `.cr`" do
expect_no_issues subject, code: "", path: "spec/foo.json"
expect_no_issues subject, code: "", path: "spec/foo/bar.json"
end
it "passes if filename is correct" do
expect_no_issues subject, code: "", path: "spec/foo_spec.cr"
expect_no_issues subject, code: "", path: "spec/foo/bar_spec.cr"
end
it "fails if filename is wrong" do
expect_issue subject, <<-CRYSTAL, path: "spec/foo.cr"
# ^{} error: Spec filename should have `_spec` suffix: `foo_spec.cr`, not `foo.cr`
CRYSTAL
end
context "properties" do
context "#ignored_dirs" do
it "provide sane defaults" do
expect_no_issues subject, code: "", path: "spec/support/foo.cr"
expect_no_issues subject, code: "", path: "spec/fixtures/foo.cr"
expect_no_issues subject, code: "", path: "spec/data/foo.cr"
end
end
context "#ignored_filenames" do
it "ignores spec_helper by default" do
expect_no_issues subject, code: "", path: "spec/spec_helper.cr"
expect_no_issues subject, code: "", path: "spec/foo/spec_helper.cr"
end
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/spec_focus_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SpecFocus do
subject = SpecFocus.new
it "does not report if spec is not focused" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
context "context" {}
describe "describe" {}
it "it" {}
pending "pending" {}
CRYSTAL
end
it "reports if there is a focused context" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
context "context", focus: true do
# ^^^^^^^^^^^ error: Focused spec item detected
end
CRYSTAL
end
it "reports if there is a focused describe block" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
describe "describe", focus: true do
# ^^^^^^^^^^^ error: Focused spec item detected
end
CRYSTAL
end
it "reports if there is a focused describe block (with block argument)" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
describe "describe", focus: true, &block
# ^^^^^^^^^^^ error: Focused spec item detected
CRYSTAL
end
it "reports if there is a focused it block" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
it "it", focus: true do
# ^^^^^^^^^^^ error: Focused spec item detected
end
CRYSTAL
end
it "reports if there is a focused pending block" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
pending "pending", focus: true do
# ^^^^^^^^^^^ error: Focused spec item detected
end
CRYSTAL
end
it "reports if there is a spec item with `focus: false`" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
it "it", focus: false do
# ^^^^^^^^^^^^ error: Focused spec item detected
end
CRYSTAL
end
it "reports if there is a spec item with `focus: !true`" do
expect_issue subject, <<-CRYSTAL, path: "source_spec.cr"
it "it", focus: !true do
# ^^^^^^^^^^^^ error: Focused spec item detected
end
CRYSTAL
end
it "does not report if there is non spec block with :focus" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
some_method "foo", focus: true do
end
CRYSTAL
end
it "does not report if there is a parameterized focused spec item" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
def assert_foo(focus = false)
it "foo", focus: focus { yield }
end
CRYSTAL
end
it "does not report if there is a tagged item with :focus" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
it "foo", tags: "focus" do
end
CRYSTAL
end
it "does not report if there are focused spec items without blocks" do
expect_no_issues subject, <<-CRYSTAL, path: "source_spec.cr"
describe "foo", focus: true
context "foo", focus: true
it "foo", focus: true
pending "foo", focus: true
CRYSTAL
end
it "does not report if there are focused items out of spec file" do
expect_no_issues subject, <<-CRYSTAL
describe "foo", focus: true {}
context "foo", focus: true {}
it "foo", focus: true {}
pending "foo", focus: true {}
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/syntax_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe Syntax do
subject = Syntax.new
it "passes if there is no invalid syntax" do
expect_no_issues subject, <<-CRYSTAL
def hello
puts "totally valid"
rescue ex : Exception
end
CRYSTAL
end
it "fails if there is an invalid syntax" do
expect_issue subject, <<-CRYSTAL
def hello
puts "invalid"
rescue Exception => e
# ^ error: expecting any of these tokens: ;, NEWLINE (not '=>')
end
CRYSTAL
end
it "has highest severity" do
subject.severity.should eq Severity::Error
end
end
end
================================================
FILE: spec/ameba/rule/lint/top_level_operator_definition_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe TopLevelOperatorDefinition do
subject = TopLevelOperatorDefinition.new
it "passes for procs" do
expect_no_issues subject, <<-CRYSTAL
-> { nil }
CRYSTAL
end
it "passes if an operator method is defined within a class" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def +(other)
end
end
CRYSTAL
end
it "passes if an operator method is defined within an enum" do
expect_no_issues subject, <<-CRYSTAL
enum Foo
def +(other)
end
end
CRYSTAL
end
it "passes if an operator method is defined within a module" do
expect_no_issues subject, <<-CRYSTAL
module Foo
def +(other)
end
end
CRYSTAL
end
it "passes if a top-level operator method has a receiver" do
expect_no_issues subject, <<-CRYSTAL
def Foo.+(other)
end
CRYSTAL
end
it "passes if a top-level operator method is defined in a record body" do
expect_no_issues subject, <<-CRYSTAL
record Foo do
def +(other)
end
end
CRYSTAL
end
it "fails if a + operator method is defined top-level" do
expect_issue subject, <<-CRYSTAL
def +(other)
# ^^^^^^^^^^ error: Top level operator method definitions cannot be called
end
CRYSTAL
end
it "fails if an index operator method is defined top-level" do
expect_issue subject, <<-CRYSTAL
def [](other)
# ^^^^^^^^^^^ error: Top level operator method definitions cannot be called
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/trailing_rescue_exception_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe TrailingRescueException do
subject = TrailingRescueException.new
it "passes for trailing rescue with literal values" do
expect_no_issues subject, <<-CRYSTAL
puts "foo" rescue "bar"
puts :foo rescue 42
CRYSTAL
end
it "passes for trailing rescue with class initialization" do
expect_no_issues subject, <<-CRYSTAL
puts "foo" rescue MyClass.new
CRYSTAL
end
it "fails if trailing rescue has exception name" do
expect_issue subject, <<-CRYSTAL
puts "hello" rescue MyException
# ^^^^^^^^^^^ error: Use a block variant of `rescue` to filter by the exception type
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/typos_spec.cr
================================================
require "../../../spec_helper"
private def check_typos_bin!
unless Ameba::Rule::Lint::Typos::BIN_PATH
pending! "`typos` executable is not available"
end
end
module Ameba::Rule::Lint
describe Typos do
subject = Typos.new
subject.fail_on_error = true
it "reports typos" do
check_typos_bin!
source = expect_issue subject, <<-CRYSTAL
# method with no arugments
# ^^^^^^^^^ error: Typo found: `arugments` -> `arguments`
def tpos
# ^^^^ error: Typo found: `tpos` -> `typos`
:otput
# ^^^^^ error: Typo found: `otput` -> `output`
end
CRYSTAL
expect_correction source, <<-CRYSTAL
# method with no arguments
def typos
:output
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/unneeded_disable_directive_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UnneededDisableDirective do
subject = UnneededDisableDirective.new
it "passes if there are no comments" do
expect_no_issues subject, <<-CRYSTAL
a = 1
CRYSTAL
end
it "passes if there is disable directive" do
expect_no_issues subject, <<-CRYSTAL
a = 1 # my super var
CRYSTAL
end
it "doesn't report if there is disable directive and it is needed" do
source = Source.new <<-CRYSTAL
# ameba:disable #{NamedRule.name}
a = 1
CRYSTAL
source.add_issue NamedRule.new, location: {2, 1},
message: "Useless assignment", status: :disabled
subject.catch(source).should be_valid
end
it "passes if there is inline disable directive and it is needed" do
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable #{NamedRule.name}
CRYSTAL
source.add_issue NamedRule.new, location: {1, 1},
message: "Alarm!", status: :disabled
subject.catch(source).should be_valid
end
it "ignores commented out disable directive" do
source = Source.new <<-CRYSTAL
# # ameba:disable #{NamedRule.name}
a = 1
CRYSTAL
source.add_issue NamedRule.new, location: {2, 1},
message: "Alarm!", status: :disabled
subject.catch(source).should be_valid
end
it "fails if there is unneeded directive" do
expect_issue subject, <<-CRYSTAL, rule_name: NamedRule.name
# ameba:disable %{rule_name}
# ^{rule_name}^^^^^^^^^^^^^^ error: Unnecessary disabling of `%{rule_name}`
a = 1
CRYSTAL
end
it "fails if there is inline unneeded directive" do
expect_issue subject, <<-CRYSTAL, rule_name: NamedRule.name
a = 1 # ameba:disable %{rule_name}
# ^{rule_name}^^^^^^^^^^^^^^^^ error: Unnecessary disabling of `%{rule_name}`
CRYSTAL
end
it "ignores non-existent rules" do
expect_no_issues subject, <<-CRYSTAL
# ameba:disable Rule1, Rule2
a = 1 # ameba:disable Rule3
CRYSTAL
end
it "passes if the rule is excluded for this source" do
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable #{NamedRule.name}
CRYSTAL
subject.test(source, Set{NamedRule.name})
source.should be_valid
end
it "fails if there is disabled UnneededDisableDirective" do
source = Source.new <<-CRYSTAL
# ameba:disable #{UnneededDisableDirective.rule_name}
a = 1
CRYSTAL
source.add_issue UnneededDisableDirective.new, location: {2, 1},
message: "Alarm!", status: :disabled
subject.catch(source).should_not be_valid
end
end
end
================================================
FILE: spec/ameba/rule/lint/unreachable_code_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UnreachableCode do
subject = UnreachableCode.new
context "return" do
it "reports if there is unreachable code after return" do
expect_issue subject, <<-CRYSTAL
def foo
a = 1
return false
b = 2
# ^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "doesn't report if there is return in if" do
expect_no_issues subject, <<-CRYSTAL
def foo
a = 1
return false if bar
b = 2
end
CRYSTAL
end
it "doesn't report if there are returns in if-then-else" do
expect_no_issues subject, <<-CRYSTAL
if a > 0
return :positive
else
return :negative
end
CRYSTAL
end
it "doesn't report if there is no else in if" do
expect_no_issues subject, <<-CRYSTAL
if a > 0
return :positive
end
:reachable
CRYSTAL
end
it "doesn't report return in on-line if" do
expect_no_issues subject, <<-CRYSTAL
return :positive if a > 0
CRYSTAL
end
it "doesn't report if return is used in a block" do
expect_no_issues subject, <<-CRYSTAL
def foo
bar = obj.try do
if something
a = 1
end
return nil
end
bar
end
CRYSTAL
end
it "reports if there is unreachable code after if-then-else" do
expect_issue subject, <<-CRYSTAL
def foo
if a > 0
return :positive
else
return :negative
end
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "reports if there is unreachable code after if-then-else-if" do
expect_issue subject, <<-CRYSTAL
def foo
if a > 0
return :positive
elsif a != 0
return :negative
else
return :zero
end
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "doesn't report if there is no unreachable code after if-then-else" do
expect_no_issues subject, <<-CRYSTAL
def foo
if a > 0
return :positive
else
return :negative
end
end
CRYSTAL
end
it "doesn't report if there is no unreachable in inner branch" do
expect_no_issues subject, <<-CRYSTAL
def foo
if a > 0
return :positive if a != 1
else
return :negative
end
:not_unreachable
end
CRYSTAL
end
it "doesn't report if there is no unreachable in exception handler" do
expect_no_issues subject, <<-CRYSTAL
def foo
puts :bar
rescue Exception
raise "Error!"
end
CRYSTAL
end
it "doesn't report if there is multiple conditions with return" do
expect_no_issues subject, <<-CRYSTAL
if :foo
if :bar
return :foobar
else
return :foobaz
end
elsif :fox
return :foofox
end
return :reachable
CRYSTAL
end
it "reports if there is unreachable code after unless" do
expect_issue subject, <<-CRYSTAL
unless :foo
return :bar
else
return :foo
end
:unreachable
# ^^^^^^^^^^ error: Unreachable code detected
CRYSTAL
end
it "doesn't report if there is no unreachable code after unless" do
expect_no_issues subject, <<-CRYSTAL
unless :foo
return :bar
end
:reachable
CRYSTAL
end
end
context "binary op" do
it "reports unreachable code in a binary operator" do
expect_issue subject, <<-CRYSTAL
(return 22) && puts "a"
# ^^^^^^^^ error: Unreachable code detected
CRYSTAL
end
it "reports unreachable code in inner binary operator" do
expect_issue subject, <<-CRYSTAL
do_something || (return 22) && puts "a"
# ^^^^^^^^ error: Unreachable code detected
CRYSTAL
end
it "reports unreachable code after the binary op" do
expect_issue subject, <<-CRYSTAL
(return 22) && break
# ^^^^^ error: Unreachable code detected
:unreachable
# ^^^^^^^^^^ error: Unreachable code detected
CRYSTAL
end
it "doesn't report if return is not the right" do
expect_no_issues subject, <<-CRYSTAL
puts "a" && return
CRYSTAL
end
it "doesn't report unreachable code in multiple binary expressions" do
expect_no_issues subject, <<-CRYSTAL
foo || bar || baz
CRYSTAL
end
end
context "case" do
it "reports if there is unreachable code after case" do
expect_issue subject, <<-CRYSTAL
def foo
case cond
when 1
something
return
when 2
something2
return
else
something3
return
end
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "doesn't report if case does not have else" do
expect_no_issues subject, <<-CRYSTAL
def foo
case cond
when 1
something
return
when 2
something2
return
end
:reachable
end
CRYSTAL
end
it "doesn't report if one when does not return" do
expect_no_issues subject, <<-CRYSTAL
def foo
case cond
when 1
something
return
when 2
something2
else
something3
return
end
:reachable
end
CRYSTAL
end
end
context "exception handler" do
it "reports unreachable code if it returns in body and rescues" do
expect_issue subject, <<-CRYSTAL
def foo
begin
return false
rescue Error
return false
rescue Exception
return false
end
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "reports unreachable code if it returns in rescues and else" do
expect_issue subject, <<-CRYSTAL
def foo
begin
do_something
rescue Error
return :error
else
return true
end
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "doesn't report if there is no else and ensure doesn't return" do
expect_no_issues subject, <<-CRYSTAL
def foo
begin
return false
rescue Error
puts "error"
rescue Exception
return false
end
:reachable
end
CRYSTAL
end
it "doesn't report if there is no else and body doesn't return" do
expect_no_issues subject, <<-CRYSTAL
def foo
begin
do_something
rescue Error
return true
rescue Exception
return false
end
:reachable
end
CRYSTAL
end
it "doesn't report if there is else and ensure doesn't return" do
expect_no_issues subject, <<-CRYSTAL
def foo
begin
do_something
rescue Error
puts "yo"
else
return true
end
:reachable
end
CRYSTAL
end
it "doesn't report if there is else and it doesn't return" do
expect_no_issues subject, <<-CRYSTAL
def foo
begin
do_something
rescue Error
return false
else
puts "yo"
end
:reachable
end
CRYSTAL
end
it "reports if there is unreachable code in rescue" do
expect_issue subject, <<-CRYSTAL
def method
rescue
return 22
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
end
context "while/until" do
it "does not report if there is no unreachable code after while" do
expect_no_issues subject, <<-CRYSTAL
def method
while something
if :foo
return :foo
else
return :foobar
end
end
:unreachable
end
CRYSTAL
end
it "does not report if there is no unreachable code after until" do
expect_no_issues subject, <<-CRYSTAL
def method
until something
if :foo
return :foo
else
return :foobar
end
end
:unreachable
end
CRYSTAL
end
it "doesn't report if there is reachable code after while with break" do
expect_no_issues subject, <<-CRYSTAL
while something
break
end
:reachable
CRYSTAL
end
end
context "rescue" do
it "reports unreachable code in rescue" do
expect_issue subject, <<-CRYSTAL
begin
rescue ex
raise ex
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "doesn't report if there is no unreachable code in rescue" do
expect_no_issues subject, <<-CRYSTAL
begin
rescue ex
raise ex
end
CRYSTAL
end
end
context "when" do
it "reports unreachable code in when" do
expect_issue subject, <<-CRYSTAL
case
when valid?
return 22
:unreachable
# ^^^^^^^^^^^^ error: Unreachable code detected
else
end
CRYSTAL
end
it "doesn't report if there is no unreachable code in when" do
expect_no_issues subject, <<-CRYSTAL
case
when valid?
return 22
else
end
CRYSTAL
end
end
context "break" do
it "reports if there is unreachable code after break" do
expect_issue subject, <<-CRYSTAL
def foo
loop do
break
a = 1
# ^^^^^ error: Unreachable code detected
end
end
CRYSTAL
end
it "doesn't report if break is in a condition" do
expect_no_issues subject, <<-CRYSTAL
a = -100
while true
break if a > 0
a += 1
end
CRYSTAL
end
end
context "next" do
it "reports if there is unreachable code after next" do
expect_issue subject, <<-CRYSTAL
a = 1
while a < 5
next
puts a
# ^^^^^^ error: Unreachable code detected
end
CRYSTAL
end
it "doesn't report if next is in a condition" do
expect_no_issues subject, <<-CRYSTAL
a = 1
while a < 5
if a == 3
next
end
puts a
end
CRYSTAL
end
end
context "raise" do
it "reports if there is unreachable code after raise" do
expect_issue subject, <<-CRYSTAL
a = 1
raise "exception"
b = 2
# ^^^ error: Unreachable code detected
CRYSTAL
end
it "doesn't report if raise is in a condition" do
expect_no_issues subject, <<-CRYSTAL
a = 1
raise "exception" if a > 0
b = 2
CRYSTAL
end
end
context "exit" do
it "reports if there is unreachable code after exit without args" do
expect_issue subject, <<-CRYSTAL
a = 1
exit
b = 2
# ^^^ error: Unreachable code detected
CRYSTAL
end
it "reports if there is unreachable code after exit with exit code" do
expect_issue subject, <<-CRYSTAL
a = 1
exit 1
b = 2
# ^^^ error: Unreachable code detected
CRYSTAL
end
it "doesn't report if exit is in a condition" do
expect_no_issues subject, <<-CRYSTAL
a = 1
exit if a > 0
b = 2
CRYSTAL
end
end
context "abort" do
it "reports if there is unreachable code after abort with one argument" do
expect_issue subject, <<-CRYSTAL
a = 1
abort "abort"
b = 2
# ^^^ error: Unreachable code detected
CRYSTAL
end
it "reports if there is unreachable code after abort with two args" do
expect_issue subject, <<-CRYSTAL
a = 1
abort "abort", 1
b = 2
# ^^^ error: Unreachable code detected
CRYSTAL
end
it "doesn't report if abort is in a condition" do
expect_no_issues subject, <<-CRYSTAL
a = 1
abort "abort" if a > 0
b = 2
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/unused_argument_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UnusedArgument do
subject = UnusedArgument.new
subject.ignore_defs = false
it "doesn't report if arguments are used" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b, c)
a + b + c
end
3.times do |i|
i + 1
end
-> (i : Int32) { i + 1 }
CRYSTAL
end
it "reports if method argument is unused" do
source = expect_issue subject, <<-CRYSTAL
def method(foo, bar, baz : Symbol)
# ^^^^^^^^^^^^ error: Unused argument `baz`. [...]
foo + bar
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(foo, bar, _baz : Symbol)
foo + bar
end
CRYSTAL
end
it "reports if block argument is unused" do
source = expect_issue subject, <<-CRYSTAL
[1, 2].each_with_index do |foo, bar|
# ^^^ error: Unused argument `bar`. [...]
foo
end
CRYSTAL
expect_correction source, <<-CRYSTAL
[1, 2].each_with_index do |foo, _|
foo
end
CRYSTAL
end
it "reports if proc argument is unused" do
source = expect_issue subject, <<-CRYSTAL
-> (foo : Int32, bar : String) do
# ^^^^^^^^^^^^ error: Unused argument `bar`. [...]
foo += 1
end
CRYSTAL
expect_correction source, <<-CRYSTAL
-> (foo : Int32, _bar : String) do
foo += 1
end
CRYSTAL
end
it "reports multiple unused args" do
source = expect_issue subject, <<-CRYSTAL
def method(foo, bar, baz)
# ^^^ error: Unused argument `foo`. If it's necessary, use `_foo` as an argument name to indicate that it won't be used.
# ^^^ error: Unused argument `bar`. If it's necessary, use `_bar` as an argument name to [...]
# ^^^ error: Unused argument `baz`. If it's necessary, use `_baz` as an argument name to [...]
nil
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(_foo, _bar, _baz)
nil
end
CRYSTAL
end
it "doesn't report if it is an instance var argument" do
expect_no_issues subject, <<-CRYSTAL
class A
def method(@name)
end
end
CRYSTAL
end
it "doesn't report if a typed argument is used" do
expect_no_issues subject, <<-CRYSTAL
def method(x : Int32)
3.times do
puts x
end
end
CRYSTAL
end
it "doesn't report if an argument with default value is used" do
expect_no_issues subject, <<-CRYSTAL
def method(x = 1)
puts x
end
CRYSTAL
end
it "doesn't report if argument starts with a _" do
expect_no_issues subject, <<-CRYSTAL
def method(_x)
end
CRYSTAL
end
it "doesn't report if it is a block and used" do
expect_no_issues subject, <<-CRYSTAL
def method(&block)
block.call
end
CRYSTAL
end
it "doesn't report if block arg is not used" do
expect_no_issues subject, <<-CRYSTAL
def method(&block)
end
CRYSTAL
end
it "doesn't report if unused and there is yield" do
expect_no_issues subject, <<-CRYSTAL
def method(&block)
yield 1
end
CRYSTAL
end
it "doesn't report if it's an anonymous block" do
expect_no_issues subject, <<-CRYSTAL
def method(&)
yield 1
end
CRYSTAL
end
it "doesn't report if variable is referenced implicitly" do
expect_no_issues subject, <<-CRYSTAL
class Bar < Foo
def method(a, b)
super
end
end
CRYSTAL
end
it "doesn't report if arg if referenced in case" do
expect_no_issues subject, <<-CRYSTAL
def foo(a)
case a
when /foo/
end
end
CRYSTAL
end
it "doesn't report if enum in a record" do
expect_no_issues subject, <<-CRYSTAL
class Class
record Record do
enum Enum
CONSTANT
end
end
end
CRYSTAL
end
context "super" do
it "reports if variable is not referenced implicitly by super" do
source = expect_issue subject, <<-CRYSTAL
class Bar < Foo
def method(foo, bar)
# ^^^ error: Unused argument `bar`. [...]
super foo
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
class Bar < Foo
def method(foo, _bar)
super foo
end
end
CRYSTAL
end
end
context "macro" do
it "doesn't report if it is a used macro argument" do
expect_no_issues subject, <<-CRYSTAL
macro my_macro(arg)
{% arg %}
end
CRYSTAL
end
it "doesn't report if it is a used macro block argument" do
expect_no_issues subject, <<-CRYSTAL
macro my_macro(&block)
{% block %}
end
CRYSTAL
end
it "doesn't report used macro args with equal names in record" do
expect_no_issues subject, <<-CRYSTAL
record X do
macro foo(a, b)
{{ a }} + {{ b }}
end
macro bar(a, b, c)
{{ a }} + {{ b }} + {{ c }}
end
end
CRYSTAL
end
it "doesn't report used args in macro literals" do
expect_no_issues subject, <<-CRYSTAL
def print(f : Array(U)) forall U
f.size.times do |i|
{% if U == Float64 %}
puts f[i].round(3)
{% else %}
puts f[i]
{% end %}
end
end
CRYSTAL
end
end
context "properties" do
describe "#ignore_defs" do
it "lets the rule to ignore def scopes if true" do
rule = UnusedArgument.new
rule.ignore_defs = true
expect_no_issues rule, <<-CRYSTAL
def method(foo)
end
CRYSTAL
end
it "lets the rule not to ignore def scopes if false" do
rule = UnusedArgument.new
rule.ignore_defs = false
expect_issue rule, <<-CRYSTAL
def method(foo)
# ^^^ error: Unused argument `foo`. [...]
end
CRYSTAL
end
end
context "#ignore_blocks" do
it "lets the rule to ignore block scopes if true" do
rule = UnusedArgument.new
rule.ignore_blocks = true
expect_no_issues rule, <<-CRYSTAL
3.times { |idx| puts "yo!" }
CRYSTAL
end
it "lets the rule not to ignore block scopes if false" do
rule = UnusedArgument.new
rule.ignore_blocks = false
expect_issue rule, <<-CRYSTAL
3.times { |idx| puts "yo!" }
# ^^^ error: Unused argument `idx`. [...]
CRYSTAL
end
end
context "#ignore_procs" do
it "lets the rule to ignore proc scopes if true" do
rule = UnusedArgument.new
rule.ignore_procs = true
expect_no_issues rule, <<-CRYSTAL
-> (foo : Int32) { }
CRYSTAL
end
it "lets the rule not to ignore proc scopes if false" do
rule = UnusedArgument.new
rule.ignore_procs = false
expect_issue rule, <<-CRYSTAL
-> (foo : Int32) { }
# ^^^^^^^^^^^ error: Unused argument `foo`. [...]
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/unused_block_argument_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UnusedBlockArgument do
subject = UnusedBlockArgument.new
it "doesn't report if it is an instance var argument" do
expect_no_issues subject, <<-CRYSTAL
class A
def initialize(&@callback)
end
end
CRYSTAL
end
it "doesn't report if anonymous" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &)
end
CRYSTAL
end
it "doesn't report if argument name starts with a `_`" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &_block)
end
CRYSTAL
end
it "doesn't report if it is a block and used" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &block)
block.call
end
CRYSTAL
end
it "reports if block arg is not used" do
source = expect_issue subject, <<-CRYSTAL
def method(a, b, c, &block)
# ^^^^^ error: Unused block argument `block`. [...]
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a, b, c, &_block)
end
CRYSTAL
end
it "reports if unused and there is yield" do
source = expect_issue subject, <<-CRYSTAL
def method(a, b, c, &block)
# ^^^^^ error: Use `&` as an argument name to indicate that it won't be referenced
3.times do |i|
i.try do
yield i
end
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a, b, c, &)
3.times do |i|
i.try do
yield i
end
end
end
CRYSTAL
end
it "doesn't report if anonymous and there is yield" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &)
yield 1
end
CRYSTAL
end
it "doesn't report if variable is referenced implicitly" do
expect_no_issues subject, <<-CRYSTAL
class Bar < Foo
def method(a, b, c, &block)
super
end
end
CRYSTAL
end
it "doesn't report if used in abstract def" do
expect_no_issues subject, <<-CRYSTAL
abstract def debug(id : String, &on_message: Callback)
abstract def info(&on_message: Callback)
CRYSTAL
end
context "super" do
it "reports if variable is not referenced implicitly by super" do
source = expect_issue subject, <<-CRYSTAL
class Bar < Foo
def method(a, b, c, &block)
# ^^^^^ error: Unused block argument `block`. [...]
super a, b, c
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
class Bar < Foo
def method(a, b, c, &_block)
super a, b, c
end
end
CRYSTAL
end
end
context "macro" do
it "doesn't report if it is a used macro block argument" do
expect_no_issues subject, <<-CRYSTAL
macro my_macro(&block)
{% block %}
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/unused_expression_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UnusedExpression do
subject = UnusedExpression.new
context "class variable access" do
it "passes if class variables are used for assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
foo = @@ivar
end
CRYSTAL
end
it "passes if an class variable is used as a target in multi-assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
@@foo, @@bar = 1, 2
end
CRYSTAL
end
it "fails if class variables are unused in void context of class" do
expect_issue subject, <<-CRYSTAL
class Actor
@@name : String = "George"
@@name
# ^^^^^^ error: Class variable access is unused
end
CRYSTAL
end
it "fails if class variables are unused in void context of method" do
expect_issue subject, <<-'CRYSTAL'
def hello : String
@@name
# ^^^^^^ error: Class variable access is unused
"Hello, #{@@name}!"
end
CRYSTAL
end
end
context "comparison" do
it "passes if comparison used in assign" do
expect_no_issues subject, <<-CRYSTAL
foo = 1 == "1"
bar = begin
2 == "2"
end
CRYSTAL
end
it "passes if comparison used in if condition" do
expect_no_issues subject, <<-CRYSTAL
if foo == bar
puts "baz"
end
CRYSTAL
end
it "passes if comparison implicitly returns from method body" do
expect_no_issues subject, <<-CRYSTAL
def foo
1 == 2
end
CRYSTAL
end
it "passes for implicit object comparisons" do
expect_no_issues subject, <<-CRYSTAL
case obj
when .> 1 then foo
when .< 0 then bar
end
CRYSTAL
end
it "passes for comparisons inside '||' and '&&' where the other arg is a call" do
expect_no_issues subject, <<-CRYSTAL
foo(bar) == baz || raise "bat"
foo(bar) == baz && raise "bat"
CRYSTAL
end
it "passes for unused comparisons with `===`, `=~`, and `!~`" do
expect_no_issues subject, <<-CRYSTAL
/foo(bar)?/ =~ baz
/foo(bar)?/ !~ baz
"foo" === bar
CRYSTAL
end
it "fails if a comparison operation with `==` is unused" do
expect_issue subject, <<-CRYSTAL
foo == 2
# ^^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails if a comparison operation with `!=` is unused" do
expect_issue subject, <<-CRYSTAL
foo != 2
# ^^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails if a comparison operation with `<` is unused" do
expect_issue subject, <<-CRYSTAL
foo < 2
# ^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails if a comparison operation with `<=` is unused" do
expect_issue subject, <<-CRYSTAL
foo <= 2
# ^^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails if a comparison operation with `>` is unused" do
expect_issue subject, <<-CRYSTAL
foo > 2
# ^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails if a comparison operation with `>=` is unused" do
expect_issue subject, <<-CRYSTAL
foo >= 2
# ^^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails if a comparison operation with `<=>` is unused" do
expect_issue subject, <<-CRYSTAL
foo <=> 2
# ^^^^^^^ error: Comparison operation is unused
CRYSTAL
end
it "fails for an unused comparison in a begin block" do
expect_issue subject, <<-CRYSTAL
begin
x = 1
x == 2
# ^^^^^^ error: Comparison operation is unused
puts x
end
CRYSTAL
end
it "fails for unused comparisons in if/elsif/else bodies" do
expect_issue subject, <<-CRYSTAL
a = if x = 1
x == 1
# ^^^^^^ error: Comparison operation is unused
x == 2
elsif true
x == 1
# ^^^^^^ error: Comparison operation is unused
x == 2
else
x == 2
# ^^^^^^ error: Comparison operation is unused
x == 3
end
CRYSTAL
end
it "fails for unused comparisons in a proc body" do
expect_issue subject, <<-CRYSTAL
a = -> do
x == 1
# ^^^^^^ error: Comparison operation is unused
"meow"
end
CRYSTAL
end
it "fails for unused comparison in top-level if statement body" do
expect_issue subject, <<-CRYSTAL
if true
x == 1
# ^^^^^^ error: Comparison operation is unused
else
x == 2
# ^^^^^^ error: Comparison operation is unused
end
CRYSTAL
end
it "fails for unused comparison in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
if x == 3
x < 1
# ^^^^^ error: Comparison operation is unused
else
x > 1
# ^^^^^ error: Comparison operation is unused
end
return
end
CRYSTAL
end
end
context "generic or union" do
it "passes if a generic is used in a top-level type declaration" do
expect_no_issues subject, <<-CRYSTAL
foo : Bar?
CRYSTAL
end
it "passes if a union is used in a top-level type declaration" do
expect_no_issues subject, <<-CRYSTAL
foo : Bar | Baz
CRYSTAL
end
it "passes if a generic is used in an assign" do
expect_no_issues subject, <<-CRYSTAL
foo = Bar?
CRYSTAL
end
it "passes if a union is used in an assign" do
expect_no_issues subject, <<-CRYSTAL
foo = Bar | Baz
CRYSTAL
end
it "passes if a generic or union is used in a cast" do
expect_no_issues subject, <<-CRYSTAL
bar = foo.as(Bar?)
baz = bar.as?(Baz | Qux)
CRYSTAL
end
it "passes if a generic or union is used as a method argument" do
expect_no_issues subject, <<-CRYSTAL
puts StaticArray(Int32, 10)
CRYSTAL
end
it "passes if a generic is used as a method call object" do
expect_no_issues subject, <<-CRYSTAL
MyClass(String).new
CRYSTAL
end
it "passes if something that looks like a union but isn't is top-level" do
expect_no_issues subject, <<-CRYSTAL
# Not a union
Foo | "Bar"
CRYSTAL
end
it "passes for an unused path" do
expect_no_issues subject, "Foo"
end
it "passes if a generic is used for a parameter type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar : Baz?)
end
CRYSTAL
end
it "passes if a generic is used for a method return type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo : Baz?
end
CRYSTAL
end
it "passes if a union is used for a parameter type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar : Baz | Qux)
end
CRYSTAL
end
it "passes if a union is used for a method return type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo : Baz | Qux
end
CRYSTAL
end
it "fails for an unused top-level generic" do
expect_issue subject, <<-CRYSTAL
String?
# ^^^^^ error: Generic type is unused
StaticArray(Int32, 10)
# ^^^^^^^^^^^^^^^^^^^^ error: Generic type is unused
CRYSTAL
end
it "fails for an unused top-level union" do
expect_issue subject, <<-CRYSTAL
Int32 | Float64 | Nil
# ^^^^^^^^^^^^^^^^^^^ error: Union type is unused
CRYSTAL
end
it "fails for an unused top-level union of self, typeof, and underscore" do
expect_issue subject, <<-CRYSTAL
self | typeof(1) | _
# ^^^^^^^^^^^^^^^^^^ error: Union type is unused
CRYSTAL
end
it "fails if a generic is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
Float64?
# ^^^^^^^^ error: Generic type is unused
nil
end
CRYSTAL
end
it "fails if a union is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
Bar | Baz
# ^^^^^^^^^ error: Union type is unused
nil
end
CRYSTAL
end
it "fails if a generic is in void of class body" do
expect_issue subject, <<-CRYSTAL
class MyClass
String?
# ^^^^^^^ error: Generic type is unused
end
CRYSTAL
end
end
context "instance variable access" do
it "passes if instance variables are used for assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
foo = @ivar
end
CRYSTAL
end
it "passes if an instance variable is used as a target in multi-assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
@foo, @bar = 1, 2
end
CRYSTAL
end
it "fails if instance variables are unused in void context of class" do
expect_issue subject, <<-CRYSTAL
class Actor
@name : String = "George"
@name
# ^^^^^ error: Instance variable access is unused
end
CRYSTAL
end
it "fails if instance variables are unused in void context of method" do
expect_issue subject, <<-'CRYSTAL'
def hello : String
@name
# ^^^^^ error: Instance variable access is unused
"Hello, #{@name}!"
end
CRYSTAL
end
it "passes if @type is unused within a macro expression" do
expect_no_issues subject, <<-CRYSTAL
def foo
{% @type %}
:bar
end
CRYSTAL
end
it "fails if instance variable is unused within a macro expression" do
expect_issue subject, <<-CRYSTAL
def foo
{% @bar %}
# ^^^^ error: Instance variable access is unused
:baz
end
CRYSTAL
end
end
context "literal" do
it "passes if a number literal is used to assign" do
expect_no_issues subject, <<-CRYSTAL
a = 1
CRYSTAL
end
it "passes if a char literal is used to assign" do
expect_no_issues subject, <<-'CRYSTAL'
c = '\t'
CRYSTAL
end
it "passes if a string literal is used to assign" do
expect_no_issues subject, <<-'CRYSTAL'
b = "foo"
g = "bar #{baz}"
CRYSTAL
end
it "passes if a heredoc is used to assign" do
expect_no_issues subject, <<-CRYSTAL
h = <<-HEREDOC
foo
HEREDOC
CRYSTAL
end
it "passes if a symbol literal is used to assign" do
expect_no_issues subject, <<-CRYSTAL
c = :foo
CRYSTAL
end
it "passes if a named tuple literal is used to assign" do
expect_no_issues subject, <<-CRYSTAL
d = {foo: 1, bar: 2}
CRYSTAL
end
it "passes if an array literal is used to assign" do
expect_no_issues subject, <<-CRYSTAL
e = [10_f32, 20_f32, 30_f32]
CRYSTAL
end
it "passes if a proc literal is used to assign" do
expect_no_issues subject, <<-CRYSTAL
f = -> { }
CRYSTAL
end
it "passes if literals inside an if statement are implicitly returned from a method" do
expect_no_issues subject, <<-CRYSTAL
def foo
if true
:bar
else
"baz"
end
end
CRYSTAL
end
it "passes if an unused literal is beyond a return statement in a method body" do
expect_no_issues subject, <<-CRYSTAL
def foo : Nil
return
:bar
nil
end
CRYSTAL
end
it "passes if a literal is the object of a call" do
expect_no_issues subject, <<-CRYSTAL
{ foo: "bar" }.to_json(io)
CRYSTAL
end
it "passes for a literal in a generic type" do
expect_no_issues subject, <<-CRYSTAL
foo = StaticArray(Int32, 3)
bar = Int32[3]
CRYSTAL
end
it "passes if a literal is passed to with or yield" do
expect_no_issues subject, <<-CRYSTAL
yield 1
with "2" yield :three
CRYSTAL
end
it "passes if a literal value is the object of a cast" do
expect_no_issues subject, <<-CRYSTAL
foo = 1.as(Int64)
bar = "2".as?(String)
CRYSTAL
end
it "passes if a literal value is the object of a cast" do
expect_no_issues subject, <<-CRYSTAL
foo = 1.as(Int64)
bar = "2".as?(String)
CRYSTAL
end
it "fails if a number literal is top-level" do
expect_issue subject, <<-CRYSTAL
1234
# ^^ error: Literal value is unused
1234_f32
# ^^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if a string literal is top-level" do
expect_issue subject, <<-'CRYSTAL'
"hello world"
# ^^^^^^^^^^^ error: Literal value is unused
"foo #{bar}"
# ^^^^^^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if an array literal is top-level" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3, 4, 5]
# ^^^^^^^^^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if a hash literal is top-level" do
expect_issue subject, <<-CRYSTAL
{"foo" => "bar"}
# ^^^^^^^^^^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if a char literal is top-level" do
expect_issue subject, <<-'CRYSTAL'
'\t'
# ^^ error: Literal value is unused
CRYSTAL
end
it "fails if a range literal is top-level" do
expect_issue subject, <<-CRYSTAL
1..2
# ^^ error: Literal value is unused
CRYSTAL
end
it "fails if a tuple literal is top-level" do
expect_issue subject, <<-CRYSTAL
{1, 2, 3}
# ^^^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if a named tuple literal is top-level" do
expect_issue subject, <<-CRYSTAL
{foo: bar}
# ^^^^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if a heredoc is top-level" do
expect_issue subject, <<-CRYSTAL
<<-HEREDOC
# ^^^^^^^^ error: Literal value is unused
this is a heredoc
HEREDOC
CRYSTAL
end
it "fails if a number literal is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
1234
# ^^^^ error: Literal value is unused
1234_f32
# ^^^^^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a string literal is in void of method body" do
expect_issue subject, <<-'CRYSTAL'
def foo
"hello world"
# ^^^^^^^^^^^^^ error: Literal value is unused
"foo #{bar}"
# ^^^^^^^^^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if an array literal is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
[1, 2, 3, 4, 5]
# ^^^^^^^^^^^^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a hash literal is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
{"foo" => "bar"}
# ^^^^^^^^^^^^^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a char literal is in void of method body" do
expect_issue subject, <<-'CRYSTAL'
def foo
'\t'
# ^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a range literal is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
1..2
# ^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a tuple literal is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
{1, 2, 3}
# ^^^^^^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a named tuple literal is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
{foo: bar}
# ^^^^^^^^^^ error: Literal value is unused
return
end
CRYSTAL
end
it "fails if a heredoc is in void of method body" do
expect_issue subject, <<-CRYSTAL
def foo
<<-HEREDOC
# ^^^^^^^^^^ error: Literal value is unused
this is a heredoc
HEREDOC
return
end
CRYSTAL
end
it "fails if a number literal is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
1234
# ^^^^ error: Literal value is unused
1234_f32
# ^^^^^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a string literal is in void of if statement body" do
expect_issue subject, <<-'CRYSTAL'
if true
"hello world"
# ^^^^^^^^^^^^^ error: Literal value is unused
"foo #{bar}"
# ^^^^^^^^^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if an array literal is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
[1, 2, 3, 4, 5]
# ^^^^^^^^^^^^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a hash literal is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
{"foo" => "bar"}
# ^^^^^^^^^^^^^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a char literal is in void of if statement body" do
expect_issue subject, <<-'CRYSTAL'
if true
'\t'
# ^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a range literal is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
1..2
# ^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a tuple literal is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
{1, 2, 3}
# ^^^^^^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a named tuple literal is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
{foo: bar}
# ^^^^^^^^^^ error: Literal value is unused
nil
end
CRYSTAL
end
it "fails if a heredoc is in void of if statement body" do
expect_issue subject, <<-CRYSTAL
if true
<<-HEREDOC
# ^^^^^^^^^^ error: Literal value is unused
this is a heredoc
HEREDOC
nil
end
CRYSTAL
end
it "fails if an unused literal is in begin or ensure body" do
expect_issue subject, <<-CRYSTAL
a = begin
1234
# ^^^^ error: Literal value is unused
rescue Foo
1234
else
1234
ensure
1234
# ^^^^ error: Literal value is unused
end
CRYSTAL
end
it "fails if an unused literal is in void of class body" do
expect_issue subject, <<-CRYSTAL
class MyClass
1234
# ^^^^ error: Literal value is unused
end
CRYSTAL
end
it "passes if an unused method call is the last line of a method with a Nil return type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo : Nil
bar("baz")
end
CRYSTAL
end
it "fails if an unused literal is the last line of a method with a Nil return type restriction" do
expect_issue subject, <<-CRYSTAL
def foo : Nil
1234
# ^^^^ error: Literal value is unused
end
CRYSTAL
end
it "fails if an unused literal is the last line of an initialize method" do
expect_issue subject, <<-CRYSTAL
def initialize
1234
# ^^^^ error: Literal value is unused
end
CRYSTAL
end
it "passes if a literal is used in outputting macro expression" do
expect_no_issues subject, <<-CRYSTAL
{{ "foo" }}
CRYSTAL
end
it "fails if a literal is used in non-outputting macro expression" do
expect_issue subject, <<-CRYSTAL
{% "foo" %}
# ^^^^^ error: Literal value is unused
CRYSTAL
end
it "fails if a literal is unused in macro expressions inside of a macro if" do
expect_issue subject, <<-CRYSTAL
{% if true %}
{% "foo" %}
# ^^^^^ error: Literal value is unused
{% end %}
CRYSTAL
end
it "fails if a literal is unused in macro expressions inside of a macro for" do
expect_issue subject, <<-CRYSTAL
{% for i in [1, 2, 3] %}
{% "foo" %}
# ^^^^^ error: Literal value is unused
{% end %}
CRYSTAL
end
it "fails if a literal is unused in macro defs" do
expect_issue subject, <<-CRYSTAL
macro name(foo)
{% "bar" %}
# ^^^^^ error: Literal value is unused
end
CRYSTAL
end
it "fails if a regex literal is unused" do
expect_issue subject, <<-'CRYSTAL'
foo = /hello world/
/goodnight moon/
# ^^^^^^^^^^^^^^ error: Literal value is unused
bar = /goodnight moon, #{foo}/
/goodnight moon, #{foo}/
# ^^^^^^^^^^^^^^^^^^^^^^ error: Literal value is unused
CRYSTAL
end
end
context "local variable access" do
it "passes if local variables are used in assign" do
expect_no_issues subject, <<-CRYSTAL
foo = 1
foo += 1
foo, bar = 2, 3
CRYSTAL
end
it "passes if a local variable is a call argument" do
expect_no_issues subject, <<-CRYSTAL
foo = 1
puts foo
CRYSTAL
end
it "passes if local variable on left side of a comparison" do
expect_no_issues subject, <<-CRYSTAL
def hello
foo = 1
foo || (puts "foo is falsey")
foo
end
CRYSTAL
end
it "passes if skip_file is used in a macro" do
expect_no_issues subject, <<-CRYSTAL
{% skip_file %}
CRYSTAL
end
it "passes if debug is used in a macro" do
expect_no_issues subject, <<-CRYSTAL
{% debug %}
CRYSTAL
end
it "fails if a local variable is in a void context" do
expect_issue subject, <<-CRYSTAL
foo = 1
begin
foo
# ^^^ error: Local variable access is unused
puts foo
end
CRYSTAL
end
it "fails if a parameter is in a void context" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
if bar > 0
bar
# ^^^ error: Local variable access is unused
end
nil
end
CRYSTAL
end
end
context "pseudo-method call" do
it "passes if typeof is unused" do
expect_no_issues subject, <<-CRYSTAL
typeof(1)
CRYSTAL
end
it "passes if as is unused" do
expect_no_issues subject, <<-CRYSTAL
as(Int32)
CRYSTAL
end
it "fails if pointerof is unused" do
expect_issue subject, <<-CRYSTAL
pointerof(Int32)
# ^^^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if sizeof is unused" do
expect_issue subject, <<-CRYSTAL
sizeof(Int32)
# ^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if instance_sizeof is unused" do
expect_issue subject, <<-CRYSTAL
instance_sizeof(Int32)
# ^^^^^^^^^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if alignof is unused" do
expect_issue subject, <<-CRYSTAL
alignof(Int32)
# ^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if instance_alignof is unused" do
expect_issue subject, <<-CRYSTAL
instance_alignof(Int32)
# ^^^^^^^^^^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if offsetof is unused" do
expect_issue subject, <<-CRYSTAL
offsetof(Int32, 1)
# ^^^^^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if is_a? is unused" do
expect_issue subject, <<-CRYSTAL
foo = 1
foo.is_a?(Int32)
# ^^^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if as? is unused" do
expect_issue subject, <<-CRYSTAL
foo = 1
foo.as?(Int32)
# ^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if responds_to? is unused" do
expect_issue subject, <<-CRYSTAL
foo = 1
foo.responds_to?(:bar)
# ^^^^^^^^^^^^^^^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if nil? is unused" do
expect_issue subject, <<-CRYSTAL
foo = 1
foo.nil?
# ^^^^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if prefix not is unused" do
expect_issue subject, <<-CRYSTAL
foo = 1
!foo
# ^^ error: Pseudo-method call is unused
CRYSTAL
end
it "fails if suffix not is unused" do
expect_issue subject, <<-CRYSTAL
foo = 1
foo.!
# ^^^ error: Pseudo-method call is unused
CRYSTAL
end
it "passes if pointerof is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = pointerof(Int32)
CRYSTAL
end
it "passes if sizeof is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = sizeof(Int32)
CRYSTAL
end
it "passes if instance_sizeof is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = instance_sizeof(Int32)
CRYSTAL
end
it "passes if alignof is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = alignof(Int32)
CRYSTAL
end
it "passes if instance_alignof is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = instance_alignof(Int32)
CRYSTAL
end
it "passes if offsetof is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = offsetof(Int32, 1)
CRYSTAL
end
it "passes if is_a? is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = is_a?(Int32)
CRYSTAL
end
it "passes if as? is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = as?(Int32)
CRYSTAL
end
it "passes if responds_to? is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = responds_to?(:foo)
CRYSTAL
end
it "passes if nil? is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = nil?
CRYSTAL
end
it "passes if prefix not is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = !true
CRYSTAL
end
it "passes if suffix not is used as an assign value" do
expect_no_issues subject, <<-CRYSTAL
var = true.!
CRYSTAL
end
end
context "self access" do
it "passes if self is used as receiver for a method def" do
expect_no_issues subject, <<-CRYSTAL
def self.foo
end
CRYSTAL
end
it "passes if self is used as object of call" do
expect_no_issues subject, <<-CRYSTAL
self.foo
CRYSTAL
end
it "passes if self is used as method of call" do
expect_no_issues subject, <<-CRYSTAL
foo.self
CRYSTAL
end
it "fails if self is unused in void context of class body" do
expect_issue subject, <<-CRYSTAL
class MyClass
self
# ^^^^ error: `self` access is unused
end
CRYSTAL
end
it "fails if self is unused in void context of begin" do
expect_issue subject, <<-CRYSTAL
begin
self
# ^^^^ error: `self` access is unused
break
end
CRYSTAL
end
it "fails if self is unused in void context of method def" do
expect_issue subject, <<-CRYSTAL
def foo
self
# ^^^^ error: `self` access is unused
"bar"
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/unused_rescue_variable_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UnusedRescueVariable do
subject = UnusedRescueVariable.new
it "passes if rescue has no variable" do
expect_no_issues subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue MyException
puts "Rescued MyException"
end
CRYSTAL
end
it "passes if rescue variable is used" do
expect_no_issues subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts ex.message
end
CRYSTAL
end
it "passes if rescue variable is used in multiple statements" do
expect_no_issues subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts ex.class
puts ex.message
end
CRYSTAL
end
it "passes if rescue variable is within `MacroIf`" do
expect_no_issues subject, <<-'CRYSTAL'
begin
raise MyException.new("OH NO!")
rescue ex : MyException
{% if flag?(:debug) %}
STDERR.puts "Error: #{ex.message}"
{% end %}
end
CRYSTAL
end
it "passes if rescue variable is within `MacroFor`" do
expect_no_issues subject, <<-'CRYSTAL'
begin
raise MyException.new("OH NO!")
rescue ex : MyException
{% for key in %w[foo bar] %}
STDERR.puts "{{ key.id }}: #{ex.message}"
{% end %}
end
CRYSTAL
end
it "fails if rescue variable is not used" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
# ^^ error: Unused `rescue` variable `ex`
puts "Rescued MyException"
end
CRYSTAL
end
it "fails if rescue variable is not used with multiple exception types" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException | ArgumentError
# ^^ error: Unused `rescue` variable `ex`
puts "Rescued exception"
end
CRYSTAL
end
it "fails if rescue variable is not used with generic exception type" do
expect_issue subject, <<-CRYSTAL
begin
raise Exception.new("OH NO!")
rescue ex : Exception
# ^^ error: Unused `rescue` variable `ex`
puts "Rescued Exception"
end
CRYSTAL
end
it "passes when variable is used in nested block" do
expect_no_issues subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
[1, 2, 3].each do |i|
puts ex.message
end
end
CRYSTAL
end
it "fails when variable is shadowed by nested block parameter" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
# ^^ error: Unused `rescue` variable `ex`
([1, 2, 3]).each do |ex|
puts ex
end
end
CRYSTAL
end
it "fails when variable is shadowed by an uninitialized variable" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
# ^^ error: Unused `rescue` variable `ex`
ex = uninitialized ArgumentError
end
CRYSTAL
end
it "fails when variable is shadowed by an assignment" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
# ^^ error: Unused `rescue` variable `ex`
ex = 42
end
CRYSTAL
end
it "fails when variable is shadowed by an multiple assignment" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
# ^^ error: Unused `rescue` variable `ex`
ex, ox = 42, 24
end
CRYSTAL
end
it "passes when variable is used in interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts "Error: #{ex.message}"
end
CRYSTAL
end
it "handles multiple rescue blocks correctly" do
expect_issue subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
# ^^ error: Unused `rescue` variable `ex`
puts "Rescued MyException"
rescue e : ArgumentError
puts e.message
end
CRYSTAL
end
it "passes when all rescue blocks use their variables" do
expect_no_issues subject, <<-CRYSTAL
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts ex.message
rescue e : ArgumentError
puts e.message
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/useless_assign_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UselessAssign do
subject = UselessAssign.new
it "does not report used assignments" do
expect_no_issues subject, <<-CRYSTAL
def method
foo = 2
foo
end
CRYSTAL
end
it "reports a useless assignment in a method" do
expect_issue subject, <<-CRYSTAL
def method
foo = 2
# ^^^ error: Useless assignment to variable `foo`
end
CRYSTAL
end
it "reports a useless assignment in a proc" do
expect_issue subject, <<-CRYSTAL
-> {
foo = 2
# ^^^ error: Useless assignment to variable `foo`
}
CRYSTAL
end
it "reports a useless assignment in a proc passed as an argument to a call" do
expect_issue subject, <<-CRYSTAL
foo -> {
bar = 1
# ^^^ error: Useless assignment to variable `bar`
}
CRYSTAL
end
it "reports a useless assignment in a proc passed as a named argument to a call" do
expect_issue subject, <<-CRYSTAL
foo bar: -> {
baz = 1
# ^^^ error: Useless assignment to variable `baz`
}
CRYSTAL
end
it "reports a useless assignment nested within an argument to a call" do
expect_issue subject, <<-CRYSTAL
foo Proc(Nil).new do
bar %w[foo bar].map do |v|
baz = 1
# ^^^ error: Useless assignment to variable `baz`
end
end
CRYSTAL
end
it "reports a useless assignment nested within an argument to a call (2)" do
expect_issue subject, <<-CRYSTAL
foo Proc(Nil).new do
bar(%w[foo bar].map do |v|
baz = 1
# ^^^ error: Useless assignment to variable `baz`
end)
end
CRYSTAL
end
it "reports a useless assignment in a block" do
expect_issue subject, <<-CRYSTAL
def method
3.times do
foo = 1
# ^^^ error: Useless assignment to variable `foo`
end
end
CRYSTAL
end
it "reports a useless assignment in a proc inside def" do
expect_issue subject, <<-CRYSTAL
def method
-> {
foo = 2
# ^^^ error: Useless assignment to variable `foo`
}
end
CRYSTAL
end
it "does not report ignored assignments" do
expect_no_issues subject, <<-CRYSTAL
payload, _header = decode
puts payload
CRYSTAL
end
it "reports a useless assignment in a proc inside a block" do
expect_issue subject, <<-CRYSTAL
def method
3.times do
-> {
foo = 2
# ^^^ error: Useless assignment to variable `foo`
}
end
end
CRYSTAL
end
it "does not report if variable is used by bare super" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar = super
bar
end
CRYSTAL
end
it "does not report if variable is used by bare previous_def" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar)
bar = previous_def
bar
end
CRYSTAL
end
it "reports useless assignment before super with explicit args" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
baz = 1
# ^^^ error: Useless assignment to variable `baz`
super(42)
end
CRYSTAL
end
it "does not report useless assignment of instance var" do
expect_no_issues subject, <<-CRYSTAL
class Cls
def initialize(@name)
end
end
CRYSTAL
end
it "does not report if assignment used in the inner block scope" do
expect_no_issues subject, <<-CRYSTAL
def method
var = true
3.times { var = false }
end
CRYSTAL
end
it "reports if assigned is not referenced in the inner block scope" do
expect_issue subject, <<-CRYSTAL
def method
var = true
# ^^^ error: Useless assignment to variable `var`
3.times {}
end
CRYSTAL
end
it "doesn't report if assignment in referenced in inner block" do
expect_no_issues subject, <<-CRYSTAL
def method
two = true
3.times do
mutex.synchronize do
two = 2
end
end
two.should be_true
end
CRYSTAL
end
it "reports if first assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method
var = true
# ^^^ error: Useless assignment to variable `var`
var = false
var
end
CRYSTAL
end
it "reports if variable reassigned and not used" do
expect_issue subject, <<-CRYSTAL
def method
var = true
# ^^^ error: Useless assignment to variable `var`
var = false
# ^^^ error: Useless assignment to variable `var`
end
CRYSTAL
end
it "does not report if variable used in a condition" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 1
if a
nil
end
end
CRYSTAL
end
it "reports second assignment as useless" do
expect_issue subject, <<-CRYSTAL
def method
a = 1
a = a + 1
# ^ error: Useless assignment to variable `a`
end
CRYSTAL
end
it "does not report if variable is referenced in other assignment" do
expect_no_issues subject, <<-CRYSTAL
def method
if f = get_something
@f = f
end
end
CRYSTAL
end
it "does not report if variable is referenced in a setter" do
expect_no_issues subject, <<-CRYSTAL
def method
foo = 2
table[foo] ||= "bar"
end
CRYSTAL
end
it "does not report if variable is reassigned but not referenced" do
expect_issue subject, <<-CRYSTAL
def method
foo = 1
puts foo
foo = 2
# ^^^ error: Useless assignment to variable `foo`
end
CRYSTAL
end
it "does not report if variable is referenced in a call" do
expect_no_issues subject, <<-CRYSTAL
def method
if f = FORMATTER
@formatter = f.new
end
end
CRYSTAL
end
it "does not report if a setter is invoked with operator assignment" do
expect_no_issues subject, <<-CRYSTAL
def method
obj = {} of Symbol => Int32
obj[:name] = 3
end
CRYSTAL
end
context "block unpacking" do
it "does not report if the first arg is transformed and not used" do
expect_no_issues subject, <<-CRYSTAL
collection.each do |(a, b)|
puts b
end
CRYSTAL
end
it "does not report if the second arg is transformed and not used" do
expect_no_issues subject, <<-CRYSTAL
collection.each do |(a, b)|
puts a
end
CRYSTAL
end
it "does not report if all transformed args are not used in a block" do
expect_no_issues subject, <<-CRYSTAL
collection.each do |(foo, bar), (baz, _qux), index, object|
end
CRYSTAL
end
end
it "does not report if assignment is referenced in a proc" do
expect_no_issues subject, <<-CRYSTAL
def method
called = false
-> { called = true }
called
end
CRYSTAL
end
it "reports if variable is shadowed in inner scope" do
expect_issue subject, <<-CRYSTAL
def method
i = 1
# ^ error: Useless assignment to variable `i`
3.times do |i|
i + 1
end
end
CRYSTAL
end
it "does not report if parameter is referenced after the branch" do
expect_no_issues subject, <<-CRYSTAL
def method(param)
3.times do
param = 3
end
param
end
CRYSTAL
end
describe "is aware of separate variable scopes (#623)" do
it "def" do
expect_issue subject, <<-CRYSTAL
x = 1
def bar
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
CRYSTAL
end
it "fun" do
expect_issue subject, <<-CRYSTAL
x = 1
fun bar
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
CRYSTAL
end
it "macro" do
expect_no_issues subject, <<-CRYSTAL
x = 1
macro bar
x = 2
end
puts x
CRYSTAL
end
context "assigns" do
it "does not report outer variable used after const initializer (#632)" do
expect_no_issues subject, <<-CRYSTAL
x = 1
BAR = begin
42
end
puts x
CRYSTAL
end
it "path" do
expect_issue subject, <<-CRYSTAL
x = 1
BAR = begin
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
CRYSTAL
end
it "ivar" do
expect_issue subject, <<-CRYSTAL
class Foo
x = 1
@bar = begin
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
end
CRYSTAL
end
it "ivar in def" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo
x = 1
# ^ error: Useless assignment to variable `x`
@bar = begin
x = 2
end
puts x
end
end
CRYSTAL
end
it "cvar" do
expect_issue subject, <<-CRYSTAL
class Foo
x = 1
@@bar = begin
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
end
CRYSTAL
end
it "cvar in def" do
expect_issue subject, <<-CRYSTAL
class Foo
def foo
x = 1
# ^ error: Useless assignment to variable `x`
@@bar = begin
x = 2
end
puts x
end
end
CRYSTAL
end
it "does not report inner variable used within initializer" do
expect_no_issues subject, <<-CRYSTAL
BAR = begin
x = 2
x + 1
end
CRYSTAL
end
it "path in class body" do
expect_issue subject, <<-CRYSTAL
class Foo
x = 1
BAR = begin
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
end
CRYSTAL
end
it "does not report unrelated variables" do
expect_issue subject, <<-CRYSTAL
x = 1
y = 2
BAR = begin
x = 3
# ^ error: Useless assignment to variable `x`
end
puts x
puts y
CRYSTAL
end
it "cvar op-assign" do
expect_issue subject, <<-CRYSTAL
class Foo
x = 1
@@bar ||= begin
x = 2
# ^ error: Useless assignment to variable `x`
end
puts x
end
CRYSTAL
end
end
end
context "op assigns" do
it "does not report if variable is referenced below the op assign" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 1
a += 1
a
end
CRYSTAL
end
it "does not report if variable is referenced in op assign few times" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 1
a += 1
a += 1
a = a + 1
a
end
CRYSTAL
end
it "reports if variable is not referenced below the op assign" do
expect_issue subject, <<-CRYSTAL
def method
a = 1
a += 1
# ^ error: Useless assignment to variable `a`
end
CRYSTAL
end
end
context "multi assigns" do
it "does not report if all assigns are referenced" do
expect_no_issues subject, <<-CRYSTAL
def method
a, b = {1, 2}
a + b
end
CRYSTAL
end
it "reports if one assign is not referenced" do
expect_issue subject, <<-CRYSTAL
def method
a, b = {1, 2}
# ^ error: Useless assignment to variable `b`
a
end
CRYSTAL
end
it "reports if both assigns are reassigned and useless" do
expect_issue subject, <<-CRYSTAL
def method
a, b = {1, 2}
# ^ error: Useless assignment to variable `a`
# ^ error: Useless assignment to variable `b`
a, b = {3, 4}
# ^ error: Useless assignment to variable `a`
# ^ error: Useless assignment to variable `b`
end
CRYSTAL
end
it "reports if both assigns are not referenced" do
expect_issue subject, <<-CRYSTAL
def method
a, b = {1, 2}
# ^ error: Useless assignment to variable `a`
# ^ error: Useless assignment to variable `b`
end
CRYSTAL
end
end
context "top level" do
it "reports if assignment is not referenced" do
expect_issue subject, <<-CRYSTAL
a = 1
# ^{} error: Useless assignment to variable `a`
a = 2
# ^{} error: Useless assignment to variable `a`
CRYSTAL
end
it "doesn't report if assignments are referenced" do
expect_no_issues subject, <<-CRYSTAL
a = 1
a += 1
a
b, c = {1, 2}
b
c
CRYSTAL
end
it "doesn't report if assignment is captured by block" do
expect_no_issues subject, <<-CRYSTAL
a = 1
3.times do
a = 2
end
CRYSTAL
end
it "doesn't report if assignment initialized and captured by block" do
expect_no_issues subject, <<-CRYSTAL
a : String? = nil
1.times do
a = "Fotis"
end
CRYSTAL
end
it "doesn't report record declaration" do
expect_no_issues subject, <<-CRYSTAL
record Foo, foo : String
record Foo, foo = "foo"
CRYSTAL
end
it "doesn't report record declarations (generics)" do
expect_no_issues subject, <<-CRYSTAL
record Foo(T), foo : T
record Foo(T), foo = T.new
CRYSTAL
end
it "doesn't report type declaration as a call argument" do
expect_no_issues subject, <<-CRYSTAL
foo Foo(T), foo : T
foo Foo, foo : Nil
foo foo : String
foo foo : String, bar : Int32?, baz : Bool
foo bar : String = ""
foo bar : String = baz
foo bar = baz
foo bar = ""
CRYSTAL
end
it "doesn't report if variable is used inside an array literal in a call" do
expect_no_issues subject, <<-CRYSTAL
def method
source = get_source
run([source])
end
CRYSTAL
end
it "doesn't report special variables like $~" do
expect_no_issues subject, <<-CRYSTAL
class Regex
def ===(other)
value = self === other.raw
$~ = $~
value
end
end
CRYSTAL
end
it "doesn't report accessor declarations" do
accessor_macros = %w[setter class_setter]
%w[getter class_getter property class_property].each do |name|
accessor_macros << name
accessor_macros << "#{name}?"
accessor_macros << "#{name}!"
end
accessor_macros.each do |accessor|
expect_no_issues subject, <<-CRYSTAL
class Foo
#{accessor} foo : String?
#{accessor} bar = "bar"
end
CRYSTAL
end
end
it "does not report if assignment is referenced after the record declaration" do
expect_no_issues subject, <<-CRYSTAL
foo = 2
record Bar, foo = 3 # foo = 3 is not parsed as assignment
puts foo
CRYSTAL
end
it "reports if assignment is not referenced after the record declaration" do
expect_issue subject, <<-CRYSTAL
foo = 2
# ^ error: Useless assignment to variable `foo`
record Bar, foo = 3
CRYSTAL
end
it "doesn't report if type declaration assigned inside module and referenced" do
expect_no_issues subject, <<-CRYSTAL
module A
foo : String? = "foo"
bar do
foo = "bar"
end
p foo
end
CRYSTAL
end
it "reports if type declaration assigned inside class" do
expect_issue subject, <<-CRYSTAL
class A
foo : String? = "foo"
# ^^^ error: Useless assignment to variable `foo`
def method
foo = "bar"
# ^^^ error: Useless assignment to variable `foo`
end
end
CRYSTAL
end
end
context "branching" do
context "if-then-else" do
it "reports initial assignment as dead when overwritten in all branches" do
expect_issue subject, <<-CRYSTAL
def method
a = 0
# ^ error: Useless assignment to variable `a`
if something
a = 1
else
a = 2
end
a
end
CRYSTAL
end
it "doesn't report if assignment is in one branch" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 0
if something
a = 1
else
nil
end
a
end
CRYSTAL
end
it "doesn't report if assignment is in one line branch" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 0
a = 1 if something
a
end
CRYSTAL
end
it "doesn't report if assignment is referenced within a method call" do
expect_no_issues subject, <<-CRYSTAL
if v = rand
puts(v = 1)
end
v
CRYSTAL
expect_no_issues subject, <<-CRYSTAL
puts v = 1 unless v = rand
v
CRYSTAL
end
it "reports if assignment is useless in the branch" do
expect_issue subject, <<-CRYSTAL
def method(a)
if a
a = 2
# ^ error: Useless assignment to variable `a`
end
end
CRYSTAL
end
it "reports if only last assignment is referenced in a branch" do
expect_issue subject, <<-CRYSTAL
def method(a)
a = 1
if a
a = 2
# ^ error: Useless assignment to variable `a`
a = 3
end
a
end
CRYSTAL
end
it "does not report of assignments are referenced in all branches" do
expect_no_issues subject, <<-CRYSTAL
def method
if matches
matches = owner.lookup_matches signature
else
matches = owner.lookup_matches signature
end
matches
end
CRYSTAL
end
it "reports initial assignment as dead when overwritten in all branches" do
expect_issue subject, <<-CRYSTAL
def method
has_newline = false
# ^^^^^^^^^^^ error: Useless assignment to variable `has_newline`
if something
do_something unless false
has_newline = false
else
do_something if true
has_newline = true
end
has_newline
end
CRYSTAL
end
end
context "unless-then-else" do
it "reports initial assignment as dead when overwritten in all branches" do
expect_issue subject, <<-CRYSTAL
def method
a = 0
# ^ error: Useless assignment to variable `a`
unless something
a = 1
else
a = 2
end
a
end
CRYSTAL
end
it "reports if there is a useless assignment in a branch" do
expect_issue subject, <<-CRYSTAL
def method
a = 0
# ^ error: Useless assignment to variable `a`
unless something
a = 1
# ^ error: Useless assignment to variable `a`
a = 2
else
a = 2
end
a
end
CRYSTAL
end
end
context "case" do
context "when" do
it "does not report if assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
case
when a = foo
when a = bar
end
puts a
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
case
when a = foo
# ^ error: Useless assignment to variable `a`
when a = bar
# ^ error: Useless assignment to variable `a`
end
end
CRYSTAL
end
end
it "does not report if assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
case a
when /foo/
a = 1
when /bar/
a = 2
end
puts a
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
case a
when /foo/
a = 1
# ^ error: Useless assignment to variable `a`
when /bar/
a = 2
# ^ error: Useless assignment to variable `a`
end
end
CRYSTAL
end
it "doesn't report if assignment is referenced in cond" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 2
case a
when /foo/
end
end
CRYSTAL
end
end
context "select" do
context "when" do
it "does not report if assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
select
when a = foo
when a = bar
end
puts a
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
select
when a = foo
# ^ error: Useless assignment to variable `a`
when a = bar
# ^ error: Useless assignment to variable `a`
end
end
CRYSTAL
end
end
end
context "binary operator" do
it "does not report if assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
(a = 1) && (b = 1)
a + b
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
(a = 1) || (b = 1)
# ^ error: Useless assignment to variable `b`
a
end
CRYSTAL
end
end
context "while" do
it "does not report if assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
while a < 10
a = a + 1
end
a
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
while a < 10
b = a
# ^ error: Useless assignment to variable `b`
end
end
CRYSTAL
end
it "does not report if assignment is referenced in a loop" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 3
result = 0
while result < 10
result += a
a = a + 1
end
result
end
CRYSTAL
end
it "does not report if assignment is referenced as param in a loop" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
result = 0
while result < 10
result += a
a = a + 1
end
result
end
CRYSTAL
end
it "does not report if assignment is referenced in loop and inner branch" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
result = 0
while result < 10
result += a
if result > 0
a = a + 1
else
a = 3
end
end
result
end
CRYSTAL
end
it "works properly if there is branch with blank node" do
expect_no_issues subject, <<-CRYSTAL
def visit
count = 0
while true
break if count == 1
case something
when :any
else
:anything_else
end
count += 1
end
end
CRYSTAL
end
it "does not report if assignment is used after break" do
expect_no_issues subject, <<-CRYSTAL
def method
found = false
while true
if something
found = true
break
end
end
found
end
CRYSTAL
end
it "does not report if assignment before next is used in next iteration" do
expect_no_issues subject, <<-CRYSTAL
def method
atomic = parse_atomic
while true
if @token.instance_var?
atomic = parse_ivar(atomic)
next
end
break
end
atomic
end
CRYSTAL
end
end
context "until" do
it "does not report if assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
until a > 10
a = a + 1
end
a
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
until a > 10
b = a + 1
# ^ error: Useless assignment to variable `b`
end
end
CRYSTAL
end
end
context "exception handler" do
it "does not report if assignment is referenced in body" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
a = 2
rescue
a
end
CRYSTAL
end
it "doesn't report if assignment is referenced in ensure" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
a = 2
ensure
a
end
CRYSTAL
end
it "doesn't report if assignment is referenced in else" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
a = 2
rescue
else
a
end
CRYSTAL
end
it "reports if assignment is useless" do
expect_issue subject, <<-CRYSTAL
def method(a)
rescue
a = 2
# ^ error: Useless assignment to variable `a`
end
CRYSTAL
end
it "does not report if variable is referenced in rescue with break in body" do
expect_no_issues subject, <<-'CRYSTAL'
3.times do
start = Time.instant
begin
perform_foo
break
rescue IO::TimeoutError
puts "Timeout [#{start.elapsed.to_i}s]"
end
end
CRYSTAL
end
end
end
context "typeof" do
it "reports useless assignments in typeof" do
expect_issue subject, <<-CRYSTAL
typeof(begin
foo = 1
# ^^^ error: Useless assignment to variable `foo`
bar = 2
# ^^^ error: Useless assignment to variable `bar`
end)
CRYSTAL
end
end
context "macro" do
it "doesn't report if assignment is referenced in macro" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 2
{% if flag?(:bits64) %}
a.to_s
{% else %}
a
{% end %}
end
CRYSTAL
end
it "doesn't report referenced assignments in macro literal" do
expect_no_issues subject, <<-CRYSTAL
def method
a = 2
{% if flag?(:bits64) %}
a = 3
{% else %}
a = 4
{% end %}
puts a
end
CRYSTAL
end
it "doesn't report if assignment is referenced in macro def" do
expect_no_issues subject, <<-CRYSTAL
macro macro_call
puts x
end
def foo
x = 1
macro_call
end
CRYSTAL
end
it "doesn't report if assignment is referenced in a macro below" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
a = 1
macro_call
end
macro macro_call
puts a
end
end
CRYSTAL
end
it "doesn't report if assignment is referenced in a macro expression as string" do
expect_no_issues subject, <<-CRYSTAL
foo = 1
puts {{ "foo".id }}
CRYSTAL
end
it "doesn't report if assignment is referenced in for macro in exp" do
expect_no_issues subject, <<-CRYSTAL
foo = 22
{% for x in %w[foo] %}
add({{ x.id }})
{% end %}
CRYSTAL
end
it "doesn't report if assignment is referenced in for macro in body" do
expect_no_issues subject, <<-CRYSTAL
foo = 22
{% for x in %w[bar] %}
puts {{ "foo".id }}
{% end %}
CRYSTAL
end
it "doesn't report if assignment is referenced in if macro in cond" do
expect_no_issues subject, <<-CRYSTAL
foo = 22
{% if "foo".id %}
{% end %}
CRYSTAL
end
it "doesn't report if assignment is referenced in if macro in then" do
expect_no_issues subject, <<-CRYSTAL
foo = 22
{% if true %}
puts {{ "foo".id }}
{% end %}
CRYSTAL
end
it "doesn't report if assignment is referenced in if macro in else" do
expect_no_issues subject, <<-CRYSTAL
foo = 22
{% if true %}
{% else %}
puts {{ "foo".id }}
{% end %}
CRYSTAL
end
end
it "does not report if variable is referenced and there is a deep level scope" do
expect_no_issues subject, <<-CRYSTAL
response = JSON.build do |json|
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
json.object do
anything
end
end
end
end
end
end
end
end
end
end
end
end
end
end
end
end
response = JSON.parse(response)
response
CRYSTAL
end
context "type declaration" do
it "doesn't report if it's not referenced at a top level" do
expect_no_issues subject, <<-CRYSTAL
a : String?
CRYSTAL
end
it "doesn't report if it's not referenced at a top level + in a method" do
expect_no_issues subject, <<-CRYSTAL
a : String?
def foo
b : String?
end
CRYSTAL
end
it "doesn't report if it's not referenced in a method" do
expect_no_issues subject, <<-CRYSTAL
def foo
a : String?
end
CRYSTAL
end
it "doesn't report if it's not referenced in a class" do
expect_no_issues subject, <<-CRYSTAL
class Foo
a : String?
end
CRYSTAL
end
it "doesn't report if it's referenced in a lib" do
expect_no_issues subject, <<-CRYSTAL
lib LibFoo
struct Foo
a : Int32
end
end
CRYSTAL
end
it "doesn't report if it's referenced" do
expect_no_issues subject, <<-CRYSTAL
def foo
a : String?
a
end
CRYSTAL
end
it "doesn't report if it's used after conditional assignment" do
expect_no_issues subject, <<-CRYSTAL
def foo
a : Foo?
if bar?
a = Foo.new
else
a = nil
end
puts a
end
CRYSTAL
end
it "reports if type declaration with value is not referenced" do
expect_issue subject, <<-CRYSTAL
def foo
a : String? = "foo"
# ^ error: Useless assignment to variable `a`
end
CRYSTAL
end
end
context "uninitialized" do
it "reports if uninitialized assignment is not referenced at a top level" do
expect_issue subject, <<-CRYSTAL
a = uninitialized U
# ^{} error: Useless assignment to variable `a`
CRYSTAL
end
it "reports if uninitialized assignment is not referenced at a top level + in a method" do
expect_issue subject, <<-CRYSTAL
a = uninitialized U
# ^{} error: Useless assignment to variable `a`
def foo
b = uninitialized U
# ^ error: Useless assignment to variable `b`
end
CRYSTAL
end
it "reports if uninitialized assignment is not referenced in a method" do
expect_issue subject, <<-CRYSTAL
def foo
a = uninitialized U
# ^ error: Useless assignment to variable `a`
end
CRYSTAL
end
it "doesn't report if uninitialized assignment is referenced" do
expect_no_issues subject, <<-CRYSTAL
def foo
a = uninitialized U
a
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/lint/useless_condition_in_when_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UselessConditionInWhen do
subject = UselessConditionInWhen.new
it "passes if there is not useless condition" do
expect_no_issues subject, <<-CRYSTAL
case
when utc?
io << " UTC"
when local?
Format.new(" %:z").format(self, io) if utc?
end
CRYSTAL
end
it "fails if there is useless if condition" do
expect_issue subject, <<-CRYSTAL
case
when utc?
io << " UTC" if utc?
# ^^^^ error: Useless condition in `when` detected
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/useless_visibility_modifier_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe UselessVisibilityModifier do
subject = UselessVisibilityModifier.new
it "passes for procs" do
expect_no_issues subject, <<-CRYSTAL
-> { nil }
CRYSTAL
end
it "passes for definitions with a receiver" do
expect_no_issues subject, <<-CRYSTAL
class Foo
end
protected def Foo.foo
end
CRYSTAL
end
it "passes for calls" do
expect_no_issues subject, <<-CRYSTAL
record Foo do
protected def foo
end
end
CRYSTAL
end
it "passes if a `protected` method visibility modifier is not used" do
expect_no_issues subject, <<-CRYSTAL
private def foo; end
def bar; end
CRYSTAL
end
{% for keyword in %w[enum class module].map(&.id) %}
it "passes if a `protected` method visibility modifier is used within a {{ keyword }}" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} Foo
protected def foo
end
end
CRYSTAL
end
{% end %}
it "fails if a `protected` method visibility modifier is used at the top level" do
source = expect_issue subject, <<-CRYSTAL
protected def foo
# ^^^^^^^ error: Useless visibility modifier
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/void_outside_lib_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe VoidOutsideLib do
subject = VoidOutsideLib.new
it "passes if Void is used in a fun def" do
expect_no_issues subject, <<-CRYSTAL
lib LibFoo
fun foo(foo : Void) : Void
fun bar(bar : Void*) : Void
end
CRYSTAL
end
it "passes if `Pointer(Void)` is used as a parameter type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar : Pointer(Void))
end
CRYSTAL
end
it "fails if `Void` is used as a parameter type restriction" do
expect_issue subject, <<-CRYSTAL
def foo(bar : Void)
# ^^^^ error: `Void` is not allowed in this context
end
CRYSTAL
end
it "passes if `Pointer(Void)` is used as return type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar) : Pointer(Void)
end
CRYSTAL
end
it "passes if `Pointer(Void) | Nil` is used as return type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar) : Pointer(Void) | Nil
end
CRYSTAL
end
it "fails if `Pointer(Void | Int32)` is used as return type restriction" do
expect_issue subject, <<-CRYSTAL
def foo(bar) : Pointer(Void | Int32)
# ^^^^ error: `Void` is not allowed in this context
end
CRYSTAL
end
it "fails if `Array(Void)` is used as return type restriction" do
expect_issue subject, <<-CRYSTAL
def foo(bar) : Array(Void)
# ^^^^ error: `Void` is not allowed in this context
end
CRYSTAL
end
it "passes if Void is used as name of a class" do
expect_no_issues subject, <<-CRYSTAL
class Foo
class Void
end
end
CRYSTAL
end
it "fails if Void is inherited from" do
expect_issue subject, <<-CRYSTAL
struct Foo < Void
# ^^^^ error: `Void` is not allowed in this context
end
CRYSTAL
end
it "passes if Void is name of alias" do
expect_no_issues subject, <<-CRYSTAL
alias Void = Foo
CRYSTAL
end
it "fails if Void is value of alias" do
expect_issue subject, <<-CRYSTAL
alias Foo = Void
# ^^^^ error: `Void` is not allowed in this context
CRYSTAL
end
it "fails if `Void` is used for an uninitialized var" do
expect_issue subject, <<-CRYSTAL
var = uninitialized Void
# ^^^^ error: `Void` is not allowed in this context
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/lint/whitespace_around_macro_expression_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Lint
describe WhitespaceAroundMacroExpression do
subject = WhitespaceAroundMacroExpression.new
it "passes if macro expression is wrapped with whitespace" do
expect_no_issues subject, <<-CRYSTAL
{{ foo }}
CRYSTAL
end
it "passes if macro expression is multiline" do
expect_no_issues subject, <<-CRYSTAL
{{
if foo > 1
foo
else
"default"
end
}}
CRYSTAL
end
it "reports macro expression without whitespace around" do
source = expect_issue subject, <<-CRYSTAL
{{foo}}
# ^^^^^ error: Missing spaces around macro expression
{{ bar}}
# ^^^^^^ error: Missing spaces around macro expression
{{baz }}
# ^^^^^^ error: Missing spaces around macro expression
CRYSTAL
expect_correction source, <<-CRYSTAL
{{ foo }}
{{ bar }}
{{ baz }}
CRYSTAL
end
# https://github.com/crystal-lang/crystal/pull/15524
it "reports macro expression without whitespace around within a macro body" do
source = expect_issue subject, <<-CRYSTAL
{% begin %}
{{foo}}
# ^^^^^^^ error: Missing spaces around macro expression
{{ bar}}
# ^^^^^^^^ error: Missing spaces around macro expression
{{baz }}
# ^^^^^^^^ error: Missing spaces around macro expression
{% end %}
CRYSTAL
expect_correction source, <<-CRYSTAL
{% begin %}
{{ foo }}
{{ bar }}
{{ baz }}
{% end %}
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/metrics/cyclomatic_complexity_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Metrics
describe CyclomaticComplexity do
subject = CyclomaticComplexity.new
complex_method = <<-CRYSTAL
def hello(a, b, c)
if a && b && c
begin
while true
return if false && b
end
""
rescue
""
end
end
end
CRYSTAL
it "passes for empty methods" do
expect_no_issues subject, <<-CRYSTAL
def hello
end
CRYSTAL
end
it "reports one issue for a complex method" do
rule = CyclomaticComplexity.new
rule.max_complexity = 5
source = Source.new(complex_method, "source.cr")
rule.catch(source).should_not be_valid
issue = source.issues.first
issue.rule.should eq rule
issue.location.to_s.should eq "source.cr:1:5"
issue.end_location.to_s.should eq "source.cr:1:9"
issue.message.should eq "Cyclomatic complexity too high [8/5]"
end
it "doesn't report an issue for an increased threshold" do
rule = CyclomaticComplexity.new
rule.max_complexity = 100
expect_no_issues rule, complex_method
end
end
end
================================================
FILE: spec/ameba/rule/naming/accessor_method_name_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe AccessorMethodName do
subject = AccessorMethodName.new
it "passes if accessor method name is correct" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def self.instance
end
def self.instance=(value)
end
def user
end
def user=(user)
end
end
CRYSTAL
end
it "passes if accessor method is defined in top-level scope" do
expect_no_issues subject, <<-CRYSTAL
def get_user
end
def set_user(user)
end
CRYSTAL
end
it "fails if accessor method is defined with receiver in top-level scope" do
expect_issue subject, <<-CRYSTAL
def Foo.get_user
# ^^^^^^^^ error: Favour method name `user` over `get_user`
end
def Foo.set_user(user)
# ^^^^^^^^ error: Favour method name `user=` over `set_user`
end
CRYSTAL
end
it "fails if accessor method name is wrong" do
expect_issue subject, <<-CRYSTAL
class Foo
def self.get_instance
# ^^^^^^^^^^^^ error: Favour method name `instance` over `get_instance`
end
def self.set_instance(value)
# ^^^^^^^^^^^^ error: Favour method name `instance=` over `set_instance`
end
def get_user
# ^^^^^^^^ error: Favour method name `user` over `get_user`
end
def set_user(user)
# ^^^^^^^^ error: Favour method name `user=` over `set_user`
end
end
CRYSTAL
end
it "ignores if alternative name isn't valid syntax" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_404
end
def set_404(value)
end
end
CRYSTAL
end
it "ignores if the method has unexpected arity" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_user(type)
end
def set_user(user, type)
end
end
CRYSTAL
end
it "ignores if the method has unexpected arity (splat)" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_user(*props)
end
end
CRYSTAL
end
it "ignores if the method has unexpected arity (double splat)" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_user(**kwargs)
end
end
CRYSTAL
end
it "ignores if the method has block" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_user(&)
yield self
end
end
CRYSTAL
end
it "ignores if the method has block argument" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_user(&block)
end
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/naming/ascii_identifiers_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe AsciiIdentifiers do
subject = AsciiIdentifiers.new
it "reports classes with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
class BigAwesome🐺
# ^^^^^^^^^^^ error: Identifier contains non-ascii characters
@🐺_name : String
# ^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports modules with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
module Bąk
# ^^^ error: Identifier contains non-ascii characters
@@bąk_name : String
# ^^^^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports enums with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
enum TypeOf🔥
# ^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports defs with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
def łódź
# ^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports defs with parameter names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
def forest_adventure(include_🐺 = true, include_🐿 = true)
# ^^^^^^^^^ error: Identifier contains non-ascii characters
# ^^^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports defs with parameter default values containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
def forest_adventure(animal_type = :🐺)
# ^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports argument names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
%w[wensleydale cheddar brie].each { |🧀| nil }
# ^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports calls with arguments containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
%i[🐺 🐿].index!(:🐺)
# ^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports calls with named arguments containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
%i[🐺 🐿].index!(obj: :🐺)
# ^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports aliases with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
alias JSON🧀 = JSON::Any
# ^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports constants with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
I_LOVE_🍣 = true
# ^^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports assignments with variable names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
space_👾 = true
# ^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports multiple assignments with variable names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
foo, space_👾 = true, true
# ^^^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports assignments with symbol literals containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
foo = :신장
# ^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports multiple assignments with symbol literals containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
foo, bar = :신장, true
# ^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "passes for strings with non-ascii characters" do
expect_no_issues subject, <<-CRYSTAL
space = "👾"
space = :invader # 👾
CRYSTAL
end
context "properties" do
context "#ignore_symbols" do
it "returns `false` by default" do
rule = AsciiIdentifiers.new
rule.ignore_symbols?.should be_false
end
it "stops reporting symbol literals if set to `true`" do
rule = AsciiIdentifiers.new
rule.ignore_symbols = true
expect_no_issues rule, <<-CRYSTAL
def forest_adventure(animal_type = :🐺); end
%i[🐺 🐿].index!(:🐺)
foo, bar = :신장, true
foo = :신장
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/naming/binary_operator_parameter_name_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe BinaryOperatorParameterName do
subject = BinaryOperatorParameterName.new
it "ignores `other` parameter name in binary method definitions" do
expect_no_issues subject, <<-CRYSTAL
def +(other); end
def -(other); end
def *(other); end
CRYSTAL
end
it "ignores binary method definitions with arity other than 1" do
expect_no_issues subject, <<-CRYSTAL
def +; end
def +(foo, bar); end
def -; end
def -(foo, bar); end
CRYSTAL
end
it "ignores non-binary method definitions" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar); end
def bąk(genus); end
CRYSTAL
end
it "reports binary methods definitions with incorrectly named parameter" do
expect_issue subject, <<-CRYSTAL
def +(foo); end
# ^^^ error: When defining the `+` operator, name its argument `other`
def -(foo); end
# ^^^ error: When defining the `-` operator, name its argument `other`
def *(foo); end
# ^^^ error: When defining the `*` operator, name its argument `other`
CRYSTAL
end
it "ignores methods from #excluded_operators" do
subject.excluded_operators.each do |op|
expect_no_issues subject, <<-CRYSTAL
def #{op}(foo); end
CRYSTAL
end
end
context "properties" do
context "#allowed_names" do
it "uses `other` as the default" do
expect_issue subject, <<-CRYSTAL
def +(foo); end
# ^^^ error: When defining the `+` operator, name its argument `other`
CRYSTAL
end
it "allows setting custom names" do
rule = BinaryOperatorParameterName.new
rule.allowed_names = %w[a b c]
expect_issue rule, <<-CRYSTAL
def +(foo); end
# ^^^ error: When defining the `+` operator, name its argument `a` or `b` or `c`
CRYSTAL
rule.allowed_names = %w[foo bar baz]
expect_no_issues rule, <<-CRYSTAL
def +(foo); end
def -(bar); end
def /(baz); end
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/naming/block_parameter_name_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe BlockParameterName do
subject = BlockParameterName.new
subject.min_name_length = 3
subject.allowed_names = %w[e i j k v]
it "passes if block parameter name matches #allowed_names" do
subject.allowed_names.each do |name|
expect_no_issues subject, <<-CRYSTAL
%w[].each { |#{name}| }
CRYSTAL
end
end
it "passes if block parameter name starts with '_'" do
expect_no_issues subject, <<-CRYSTAL
%w[].each { |_, _foo, _bar| }
CRYSTAL
end
it "fails if block parameter name doesn't match #allowed_names" do
expect_issue subject, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
end
context "properties" do
context "#min_name_length" do
it "allows setting custom values" do
rule = BlockParameterName.new
rule.allowed_names = %w[a b c]
rule.min_name_length = 3
expect_issue rule, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.min_name_length = 1
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x| }
CRYSTAL
end
end
context "#allow_names_ending_in_numbers" do
it "allows setting custom values" do
rule = BlockParameterName.new
rule.min_name_length = 1
rule.allowed_names = %w[]
rule.allow_names_ending_in_numbers = false
expect_issue rule, <<-CRYSTAL
%w[].each { |x1| }
# ^^ error: Disallowed block parameter name found
CRYSTAL
rule.allow_names_ending_in_numbers = true
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x1| }
CRYSTAL
end
end
context "#allowed_names" do
it "allows setting custom names" do
rule = BlockParameterName.new
rule.min_name_length = 3
rule.allowed_names = %w[a b c]
expect_issue rule, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.allowed_names = %w[x y z]
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x| }
CRYSTAL
end
end
context "#forbidden_names" do
it "allows setting custom names" do
rule = BlockParameterName.new
rule.min_name_length = 1
rule.allowed_names = %w[]
rule.forbidden_names = %w[x y z]
expect_issue rule, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.forbidden_names = %w[a b c]
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x| }
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/naming/constant_names_spec.cr
================================================
require "../../../spec_helper"
private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__)
it "reports constant name #{expected}", file, line do
rule = Ameba::Rule::Naming::ConstantNames.new
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
%{name} = #{value}
# ^{name} error: Constant name should be screaming-cased: `#{expected}`, not `#{name}`
CRYSTAL
end
end
module Ameba::Rule::Naming
describe ConstantNames do
subject = ConstantNames.new
it "passes if type names are screaming-cased" do
expect_no_issues subject, <<-CRYSTAL
LUCKY_NUMBERS = [3, 7, 11]
DOCUMENTATION_URL = "https://crystal-lang.org/docs"
Int32
s : String = "str"
def works(n : Int32)
end
Log = ::Log.for("db")
a = 1
myVar = 2
m_var = 3
CRYSTAL
end
it "doesn't report single-line constant assigns inside namespaces" do
expect_no_issues subject, <<-CRYSTAL
Example::FOO_BAR = "bar"
CRYSTAL
end
# it_reports_constant "MyBadConstant", "1", "MYBADCONSTANT"
it_reports_constant "Wrong_NAME", "2", "WRONG_NAME"
it_reports_constant "Wrong_Name", "3", "WRONG_NAME"
end
end
================================================
FILE: spec/ameba/rule/naming/filename_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe Filename do
subject = Filename.new
it "passes if filename is correct" do
expect_no_issues subject, code: "", path: "src/foo.cr"
expect_no_issues subject, code: "", path: "src/foo_bar.cr"
end
it "fails if filename is wrong" do
expect_issue subject, <<-CRYSTAL, path: "src/fooBar.cr"
# ^{} error: Filename should be underscore-cased: `foo_bar.cr`, not `fooBar.cr`
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/naming/method_names_spec.cr
================================================
require "../../../spec_helper"
private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__)
it "reports method name #{expected}", file, line do
rule = Ameba::Rule::Naming::MethodNames.new
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
def %{name}; end
# ^{name} error: Method name should be underscore-cased: `#{expected}`, not `%{name}`
CRYSTAL
end
end
module Ameba::Rule::Naming
describe MethodNames do
subject = MethodNames.new
it "passes if method names are underscore-cased" do
expect_no_issues subject, <<-CRYSTAL
class Person
def first_name
end
def date_of_birth
end
def homepage_url
end
def valid?
end
def name
end
end
CRYSTAL
end
it_reports_method_name "firstName", "first_name"
it_reports_method_name "date_of_Birth", "date_of_birth"
it_reports_method_name "homepageURL", "homepage_url"
end
end
================================================
FILE: spec/ameba/rule/naming/predicate_name_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe PredicateName do
subject = PredicateName.new
it "passes if predicate name is correct" do
expect_no_issues subject, <<-CRYSTAL
def valid?(x)
end
class Image
def picture?(x)
end
end
def allow_this_picture?
end
CRYSTAL
end
it "fails if predicate name is wrong" do
expect_issue subject, <<-CRYSTAL
class Image
def self.is_valid?(x)
# ^^^^^^^^^ error: Favour method name `valid?` over `is_valid?`
end
end
def is_valid?(x)
# ^^^^^^^^^ error: Favour method name `valid?` over `is_valid?`
end
def is_valid(x)
# ^^^^^^^^ error: Favour method name `valid?` over `is_valid`
end
CRYSTAL
end
it "ignores if alternative name isn't valid syntax" do
expect_no_issues subject, <<-CRYSTAL
class Image
def is_404?(x)
true
end
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/naming/query_bool_methods_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe QueryBoolMethods do
subject = QueryBoolMethods.new
it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL
class Foo
class_property? foo = true
property? foo = true
property foo2 : Bool? = true
setter panda = true
end
module Bar
class_getter? bar : Bool = true
getter? bar : Bool
getter bar2 : Bool? = true
setter panda : Bool = true
def initialize(@bar = true)
end
end
CRYSTAL
end
it "reports only valid properties" do
expect_issue subject, <<-CRYSTAL
class Foo
class_property? foo = true
class_property bar = true
# ^^^ error: Consider using `class_property?` for `bar`
class_property baz = true
# ^^^ error: Consider using `class_property?` for `baz`
end
CRYSTAL
end
{% for call in %w[getter class_getter property class_property] %}
it "reports `{{ call.id }}` assign with Bool" do
expect_issue subject, <<-CRYSTAL, call: {{ call }}
class Foo
%{call} foo = true
_{call} # ^^^ error: Consider using `%{call}?` for `foo`
end
CRYSTAL
end
it "reports `{{ call.id }}` type declaration assign with Bool" do
expect_issue subject, <<-CRYSTAL, call: {{ call }}
class Foo
%{call} foo : Bool = true
_{call} # ^^^ error: Consider using `%{call}?` for `foo`
end
CRYSTAL
end
it "reports `{{ call.id }}` type declaration with Bool" do
expect_issue subject, <<-CRYSTAL, call: {{ call }}
class Foo
%{call} foo : Bool
_{call} # ^^^ error: Consider using `%{call}?` for `foo`
def initialize(@foo = true)
end
end
CRYSTAL
end
{% end %}
end
end
================================================
FILE: spec/ameba/rule/naming/rescued_exceptions_variable_name_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Naming
describe RescuedExceptionsVariableName do
subject = RescuedExceptionsVariableName.new
it "passes if exception handler variable name matches #allowed_names" do
subject.allowed_names.each do |name|
expect_no_issues subject, <<-CRYSTAL
def foo
raise "foo"
rescue #{name}
nil
end
CRYSTAL
end
end
it "fails if exception handler variable name doesn't match #allowed_names" do
expect_issue subject, <<-CRYSTAL
def foo
raise "foo"
rescue wtf : ArgumentError
# ^^^ error: Disallowed variable name, use one of these instead: `e`, `ex`, `exception`, `err`, `error`
nil
end
CRYSTAL
end
context "properties" do
context "#allowed_names" do
it "returns sensible defaults" do
rule = RescuedExceptionsVariableName.new
rule.allowed_names.should eq %w[e ex exception err error]
end
it "allows setting custom names" do
rule = RescuedExceptionsVariableName.new
rule.allowed_names = %w[foo]
expect_issue rule, <<-CRYSTAL
def foo
raise "foo"
rescue e
# ^ error: Disallowed variable name, use `foo` instead
nil
end
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/naming/type_names_spec.cr
================================================
require "../../../spec_helper"
private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__)
it "reports type name #{expected}", file, line do
rule = Ameba::Rule::Naming::TypeNames.new
expect_issue rule, <<-CRYSTAL, type: type, name: name, file: file, line: line
%{type} %{name}; end
_{type} # ^{name} error: Type name should be camelcased: `#{expected}`, not `%{name}`
CRYSTAL
end
end
module Ameba::Rule::Naming
describe TypeNames do
subject = TypeNames.new
it "passes if type names are camelcased" do
expect_no_issues subject, <<-CRYSTAL
class ParseError < Exception
end
module HTTP
class RequestHandler
end
end
alias NumericValue = Float32 | Float64 | Int32 | Int64
lib LibYAML
end
struct TagDirective
end
enum Time::DayOfWeek
end
CRYSTAL
end
it_reports_name "class", "My_class", "MyClass"
it_reports_name "module", "HTT_p", "HTTP"
it_reports_name "lib", "Lib_YAML", "LibYAML"
it_reports_name "struct", "Tag_directive", "TagDirective"
it_reports_name "enum", "Time_enum::Day_of_week", "TimeEnum::DayOfWeek"
it "reports alias name" do
expect_issue subject, <<-CRYSTAL
alias Numeric_value = Int32
# ^^^^^^^^^^^^^ error: Type name should be camelcased: `NumericValue`, not `Numeric_value`
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/naming/variable_names_spec.cr
================================================
require "../../../spec_helper"
private def it_reports_var_name(name, value, expected, *, file = __FILE__, line = __LINE__)
it "reports variable name #{expected}", file, line do
rule = Ameba::Rule::Naming::VariableNames.new
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
%{name} = #{value}
# ^{name} error: Variable name should be underscore-cased: `#{expected}`, not `%{name}`
CRYSTAL
end
end
module Ameba::Rule::Naming
describe VariableNames do
subject = VariableNames.new
it "passes if var names are underscore-cased" do
expect_no_issues subject, <<-CRYSTAL
class Greeting
@@default_greeting = "Hello world"
def initialize(@custom_greeting = nil)
end
def print_greeting
greeting = @custom_greeting || @@default_greeting
puts greeting
end
end
CRYSTAL
end
it_reports_var_name "myBadNamedVar", "1", "my_bad_named_var"
it_reports_var_name "wrong_Name", "'y'", "wrong_name"
it "reports instance variable name" do
expect_issue subject, <<-CRYSTAL
class Greeting
def initialize(@badNamed = nil)
# ^^^^^^^^^ error: Variable name should be underscore-cased: `@bad_named`, not `@badNamed`
end
end
CRYSTAL
end
it "reports method with multiple instance variables" do
expect_issue subject, <<-CRYSTAL
class Location
def at(@startLocation = nil, @endLocation = nil)
# ^^^^^^^^^^^^^^ error: Variable name should be underscore-cased: `@start_location`, not `@startLocation`
# ^^^^^^^^^^^^ error: Variable name should be underscore-cased: `@end_location`, not `@endLocation`
end
end
CRYSTAL
end
it "reports class variable name" do
expect_issue subject, <<-CRYSTAL
class Greeting
@@defaultGreeting = "Hello world"
# ^^^^^^^^^^^^^^^^^ error: Variable name should be underscore-cased: `@@default_greeting`, not `@@defaultGreeting`
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/performance/any_after_filter_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe AnyAfterFilter do
subject = AnyAfterFilter.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 1 }.any?(&.zero?)
[1, 2, 3].reject { |e| e > 1 }.any?(&.zero?)
[1, 2, 3].select { |e| e > 1 }.any?(&block)
[1, 2, 3].select { |e| e > 1 }
[1, 2, 3].reject { |e| e > 1 }
[1, 2, 3].any? { |e| e > 1 }
CRYSTAL
end
it "reports if there is select followed by any? without a block" do
source = expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.any?
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `any? {...}` instead of `select {...}.any?`
CRYSTAL
expect_no_corrections source
end
it "does not report if source is a spec" do
expect_no_issues subject, <<-CRYSTAL, "source_spec.cr"
[1, 2, 3].select { |e| e > 2 }.any?
CRYSTAL
end
it "reports if there is reject followed by any? without a block" do
source = expect_issue subject, <<-CRYSTAL
[1, 2, 3].reject { |e| e > 2 }.any?
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `any? {...}` instead of `reject {...}.any?`
CRYSTAL
expect_no_corrections source
end
it "does not report if any? calls contains a block" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.any?(&.zero?)
[1, 2, 3].reject { |e| e > 2 }.any?(&.zero?)
CRYSTAL
end
context "properties" do
it "#filter_names" do
rule = AnyAfterFilter.new
rule.filter_names = %w[select]
expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].reject { |e| e > 2 }.any?
CRYSTAL
end
end
context "macro" do
it "reports in macro scope" do
source = expect_issue subject, <<-CRYSTAL
{{ [1, 2, 3].reject { |e| e > 2 }.any? }}
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `any? {...}` instead of `reject {...}.any?`
CRYSTAL
expect_no_corrections source
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/any_instead_of_present_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe AnyInsteadOfPresent do
subject = AnyInsteadOfPresent.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].any?(&.zero?)
[1, 2, 3].any?(&block)
[1, 2, 3].any?(String)
[1, 2, 3].any?(1..3)
[1, 2, 3].any? { |e| e > 1 }
CRYSTAL
end
it "reports if there is any? call without a block nor argument" do
source = expect_issue subject, <<-CRYSTAL
%w[foo bar].any?
# ^^^^ error: Use `{...}.present?` instead of `{...}.any?`
CRYSTAL
expect_correction source, <<-CRYSTAL
%w[foo bar].present?
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, <<-CRYSTAL, "source_spec.cr"
[1, 2, 3].any?
CRYSTAL
end
context "macro" do
it "does not report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].any? }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/base_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe Base do
subject = PerfRule.new
describe "#catch" do
it "ignores spec files" do
source = Source.new path: "source_spec.cr"
subject.catch(source).should be_valid
end
it "reports perf issues for non-spec files" do
source = Source.new path: "source.cr"
subject.catch(source).should_not be_valid
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/chained_call_with_no_bang_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe ChainedCallWithNoBang do
subject = ChainedCallWithNoBang.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
(1..3).select { |e| e > 1 }.sort!
(1..3).select { |e| e > 1 }.sort_by!(&.itself)
(1..3).select { |e| e > 1 }.uniq!
(1..3).select { |e| e > 1 }.shuffle!
(1..3).select { |e| e > 1 }.reverse!
(1..3).select { |e| e > 1 }.rotate!
CRYSTAL
end
it "reports if there is select followed by reverse" do
source = expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 1 }.reverse
# ^^^^^^^ error: Use bang method variant `reverse!` after chained `select` call
CRYSTAL
expect_correction source, <<-CRYSTAL
[1, 2, 3].select { |e| e > 1 }.reverse!
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, <<-CRYSTAL, "source_spec.cr"
[1, 2, 3].select { |e| e > 1 }.reverse
CRYSTAL
end
it "reports if there is select followed by reverse followed by other call" do
source = expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.reverse.size
# ^^^^^^^ error: Use bang method variant `reverse!` after chained `select` call
CRYSTAL
expect_correction source, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.reverse!.size
CRYSTAL
end
context "properties" do
it "#call_names" do
rule = ChainedCallWithNoBang.new
rule.call_names = %w[uniq]
expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.reverse
CRYSTAL
end
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].select { |e| e > 2 }.reverse }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/compact_after_map_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe CompactAfterMap do
subject = CompactAfterMap.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
(1..3).compact_map(&.itself)
(1..3).compact_map(&block)
CRYSTAL
end
it "passes if there is map followed by a bang call" do
expect_no_issues subject, <<-CRYSTAL
(1..3).map(&.itself).compact!
CRYSTAL
end
it "reports if there is map followed by compact call" do
expect_issue subject, <<-CRYSTAL
(1..3).map(&.itself).compact
# ^^^^^^^^^^^^^^^^^^^^^ error: Use `compact_map {...}` instead of `map {...}.compact`
(1..3).map(&block).compact
# ^^^^^^^^^^^^^^^^^^^ error: Use `compact_map {...}` instead of `map {...}.compact`
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
(1..3).map(&.itself).compact
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].map(&.to_s).compact }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/excessive_allocations_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe ExcessiveAllocations do
subject = ExcessiveAllocations.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
"Alice".chars.each(arg) { |c| puts c }
"Alice".chars(arg).each { |c| puts c }
"Alice\nBob".lines.each(arg) { |l| puts l }
"Alice\nBob".lines(arg).each { |l| puts l }
CRYSTAL
end
it "reports if there is a collection method followed by each" do
source = expect_issue subject, <<-CRYSTAL
"Alice".chars.each { |c| puts c }
# ^^^^^^^^^^ error: Use `each_char {...}` instead of `chars.each {...}` to avoid excessive allocation
"Alice".chars.each(&block)
# ^^^^^^^^^^ error: Use `each_char {...}` instead of `chars.each {...}` to avoid excessive allocation
"Alice\nBob".lines.each { |l| puts l }
# ^^^^^^^^^^ error: Use `each_line {...}` instead of `lines.each {...}` to avoid excessive allocation
CRYSTAL
expect_correction source, <<-CRYSTAL
"Alice".each_char { |c| puts c }
"Alice".each_char(&block)
"Alice\nBob".each_line { |l| puts l }
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, <<-CRYSTAL, "source_spec.cr"
"Alice".chars.each { |c| puts c }
CRYSTAL
end
context "properties" do
it "#call_names" do
rule = ExcessiveAllocations.new
rule.call_names = {
"children" => "each_child",
}
expect_no_issues rule, <<-CRYSTAL
"Alice".chars.each { |c| puts c }
CRYSTAL
end
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ "Alice".chars.each { |c| puts c } }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/first_last_after_filter_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe FirstLastAfterFilter do
subject = FirstLastAfterFilter.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 1 }
[1, 2, 3].reverse.select { |e| e > 1 }
[1, 2, 3].reverse.last
[1, 2, 3].reverse.first
[1, 2, 3].reverse.first
CRYSTAL
end
it "reports if there is select followed by last" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.last
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `reverse_each.find {...}` instead of `select {...}.last`
[1, 2, 3].select(&block).last
# ^^^^^^^^^^^^^^^^^^^ error: Use `reverse_each.find {...}` instead of `select {...}.last`
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.last
CRYSTAL
end
it "reports if there is select followed by last?" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.last?
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `reverse_each.find {...}` instead of `select {...}.last?`
CRYSTAL
end
it "reports if there is select followed by first" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.first
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `find {...}` instead of `select {...}.first`
CRYSTAL
end
it "does not report if there is selected followed by first with arguments" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].select { |n| n % 2 == 0 }.first(2)
CRYSTAL
end
it "reports if there is select followed by first?" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.first?
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `find {...}` instead of `select {...}.first?`
CRYSTAL
end
it "does not report if there is select followed by any other call" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.size
[1, 2, 3].select { |e| e > 2 }.any?
CRYSTAL
end
context "properties" do
it "#filter_names" do
rule = FirstLastAfterFilter.new
rule.filter_names = %w[reject]
expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.first
CRYSTAL
end
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].select { |e| e > 2 }.last }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/flatten_after_map_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe FlattenAfterMap do
subject = FlattenAfterMap.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
%w[Alice Bob].flat_map(&.chars)
CRYSTAL
end
it "reports if there is map followed by flatten call" do
expect_issue subject, <<-CRYSTAL
%w[Alice Bob].map(&.chars).flatten
# ^^^^^^^^^^^^^^^^^^^^ error: Use `flat_map {...}` instead of `map {...}.flatten`
%w[Alice Bob].map(&block).flatten
# ^^^^^^^^^^^^^^^^^^^ error: Use `flat_map {...}` instead of `map {...}.flatten`
CRYSTAL
end
it "does not report is source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
%w[Alice Bob].map(&.chars).flatten
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ %w[Alice Bob].map(&.chars).flatten }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/map_instead_of_block_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe MapInsteadOfBlock do
subject = MapInsteadOfBlock.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
(1..3).sum(&.*(2))
(1..3).product(&.*(2))
CRYSTAL
end
it "reports if there is map followed by sum without a block" do
expect_issue subject, <<-CRYSTAL
(1..3).map(&.to_u64).sum
# ^^^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum`
(1..3).map(&block).sum
# ^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum`
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
(1..3).map(&.to_s).join
CRYSTAL
end
it "reports if there is map followed by sum without a block (with argument)" do
expect_issue subject, <<-CRYSTAL
(1..3).map(&.to_u64).sum(0)
# ^^^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum`
CRYSTAL
end
it "reports if there is map followed by sum with a block" do
expect_issue subject, <<-CRYSTAL
(1..3).map(&.to_u64).sum(&.itself)
# ^^^^^^^^^^^^^^^^^ error: Use `sum {...}` instead of `map {...}.sum`
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, 3].map(&.to_u64).sum }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/minmax_after_map_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe MinMaxAfterMap do
subject = MinMaxAfterMap.new
it "passes if there are no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
%w[Alice Bob].map { |name| name.size }.min(2)
%w[Alice Bob].map { |name| name.size }.max(2)
CRYSTAL
end
it "reports if there is a `min/max/minmax` call followed by `map`" do
source = expect_issue subject, <<-CRYSTAL
%w[Alice Bob].map { |name| name.size }.min
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `min_of {...}` instead of `map {...}.min`
%w[Alice Bob].map(&.size).max.zero?
# ^^^^^^^^^^^^^^^ error: Use `max_of {...}` instead of `map {...}.max`
%w[Alice Bob].map(&.size).minmax?
# ^^^^^^^^^^^^^^^^^^^ error: Use `minmax_of? {...}` instead of `map {...}.minmax?`
%w[Alice Bob].map(&block).minmax?
# ^^^^^^^^^^^^^^^^^^^ error: Use `minmax_of? {...}` instead of `map {...}.minmax?`
CRYSTAL
expect_correction source, <<-CRYSTAL
%w[Alice Bob].min_of { |name| name.size }
%w[Alice Bob].max_of(&.size).zero?
%w[Alice Bob].minmax_of?(&.size)
%w[Alice Bob].minmax_of?(&block)
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
%w[Alice Bob].map(&.size).min
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ %w[Alice Bob].map(&.size).min }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/size_after_filter_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe SizeAfterFilter do
subject = SizeAfterFilter.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }
[1, 2, 3].reject { |e| e < 2 }
[1, 2, 3].count { |e| e > 2 && e.odd? }
[1, 2, 3].count { |e| e < 2 && e.even? }
User.select("field AS name").count
Company.select(:value).count
CRYSTAL
end
it "reports if there is a select followed by size" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.size
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `count {...}` instead of `select {...}.size`
[1, 2, 3].select(&block).size
# ^^^^^^^^^^^^^^^^^^^ error: Use `count {...}` instead of `select {...}.size`
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.size
CRYSTAL
end
it "reports if there is a reject followed by size" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].reject { |e| e < 2 }.size
# ^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `count {...}` instead of `reject {...}.size`
CRYSTAL
end
it "reports if a block shorthand used" do
expect_issue subject, <<-CRYSTAL
[1, 2, 3].reject(&.empty?).size
# ^^^^^^^^^^^^^^^^^^^^^ error: Use `count {...}` instead of `reject {...}.size`
CRYSTAL
end
context "properties" do
it "#filter_names" do
rule = SizeAfterFilter.new
rule.filter_names = %w[select]
expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].reject(&.empty?).size
CRYSTAL
end
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{[1, 2, 3].select { |v| v > 1 }.size}}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/performance/times_map_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Performance
describe TimesMap do
subject = TimesMap.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
3.times.map { |i| i * i }.to_a { |i| i * -1 }
3.times.map { |i| i * i }
3.times { |i| i * i }
CRYSTAL
end
it "reports if there is map followed by flatten call" do
source = expect_issue subject, <<-CRYSTAL
foo.bar.times.map do |i|
# ^^^^^^^^^^^^^^^^^^^^^^ error: Use `Array.new(foo.bar) {...}` instead of `foo.bar.times.map {...}.to_a`
i * i
end.to_a
3.times.map { |i| i * i }.to_a
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `Array.new(3) {...}` instead of `3.times.map {...}.to_a`
3.times.map(&block).to_a
# ^^^^^^^^^^^^^^^^^^^^^^ error: Use `Array.new(3) {...}` instead of `3.times.map {...}.to_a`
3.times.map(&block).to_a.select(&.odd?)
# ^^^^^^^^^^^^^^^^^^^^^^ error: Use `Array.new(3) {...}` instead of `3.times.map {...}.to_a`
CRYSTAL
expect_correction source, <<-CRYSTAL
Array.new(foo.bar) do |i|
i * i
end
Array.new(3) { |i| i * i }
Array.new(3, &block)
Array.new(3, &block).select(&.odd?)
CRYSTAL
end
it "does not report is source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
3.times.map { |i| i * i }.to_a
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ 3.times.map { |i| i * i }.to_a }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/style/array_literal_syntax_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe ArrayLiteralSyntax do
subject = ArrayLiteralSyntax.new
it "passes for an array literal with elements" do
expect_no_issues subject, <<-CRYSTAL
def print_numbers
numbers = [1, 2] of Int32
puts numbers
end
CRYSTAL
end
it "passes for an array-like literal" do
expect_no_issues subject, <<-CRYSTAL
Array{1, 2}
CRYSTAL
end
# Array literals in macros are semantically different from `Array(T).new`
it "passes for an empty array literal in a macro" do
expect_no_issues subject, <<-CRYSTAL
macro foo(bar = [] of String)
{% for b in bar %}
{{ b.id }}
{% end %}
{% baz = [] of Int32 %}
end
{% qux = [] of Int32 %}
CRYSTAL
end
it "fails for an empty array literal" do
source = expect_issue subject, <<-CRYSTAL
def print_numbers
numbers = [] of Int32
# ^^^^^^^^^^^ error: Use `Array(Int32).new` for creating an empty array
numbers << 1
numbers << 2
puts numbers
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def print_numbers
numbers = Array(Int32).new
numbers << 1
numbers << 2
puts numbers
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/call_parentheses_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe CallParentheses do
subject = CallParentheses.new
it "ignores ECR files" do
expect_no_issues subject, <<-ECR, path: "foo.ecr"
<%= foo bar %>
ECR
end
it "passes for valid method calls" do
expect_no_issues subject, <<-CRYSTAL
foo
foo.bar?
foo[0] = bar
foo.bar = baz
foo + bar
foo.+ bar
foo.bar(&.== baz)
CRYSTAL
end
it "passes if method call has parentheses" do
expect_no_issues subject, <<-CRYSTAL
foo(bar: 1)
CRYSTAL
end
it "fails for method call with positional arguments" do
source = expect_issue subject, <<-CRYSTAL
foo bar
# ^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar)
CRYSTAL
end
it "fails for method call with named arguments" do
source = expect_issue subject, <<-CRYSTAL
foo bar: 1
# ^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar: 1)
CRYSTAL
end
it "fails for nested method call with named arguments" do
source = expect_issue subject, <<-CRYSTAL
foo(bar path: "bar.cr")
# ^^^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar(path: "bar.cr"))
CRYSTAL
end
it "fails for method call with positional + named arguments" do
source = expect_issue subject, <<-CRYSTAL
foo bar, baz: 1
# ^^^^^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar, baz: 1)
CRYSTAL
end
it "fails for method call with positional + named arguments" do
source = expect_issue subject, <<-CRYSTAL
bats = bats [Bat.new path: "bat.cr"]
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
# ^^^^^^^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
bats = bats([Bat.new(path: "bat.cr")])
CRYSTAL
end
it "fails for method call with positional + named arguments" do
source = expect_issue subject, <<-CRYSTAL
foo bar, baz: baz if baz.fooable?
# ^^^^^^^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar, baz: baz) if baz.fooable?
CRYSTAL
end
it "fails for method call with block arg" do
source = expect_issue subject, <<-CRYSTAL
foo &proc
# ^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(&proc)
CRYSTAL
end
it "fails for method call with block (short)" do
source = expect_issue subject, <<-CRYSTAL
foo &.baz?
# ^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(&.baz?)
CRYSTAL
end
it "fails for method call with block" do
source = expect_issue subject, <<-CRYSTAL
foo bar: 1 do |x, y|
# ^^^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
baz(x, y)
end
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar: 1) do |x, y|
baz(x, y)
end
CRYSTAL
end
it "fails for method call with block (single line)" do
source = expect_issue subject, <<-CRYSTAL
foo bar: 1 { |x, y| baz(x, y) }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar: 1) { |x, y| baz(x, y) }
CRYSTAL
end
it "fails for method call with heredoc argument" do
source = expect_issue subject, <<-CRYSTAL
foo <<-HEREDOC
# ^^^^^^^^^^^^ error: Missing parentheses in method call
HEREDOC
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(<<-HEREDOC)
HEREDOC
CRYSTAL
end
it "fails for method call with multiple heredoc arguments" do
source = expect_issue subject, <<-CRYSTAL
foo <<-FOO, <<-BAR
# ^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
FOO
BAR
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(<<-FOO, <<-BAR)
FOO
BAR
CRYSTAL
end
it "fails for method call with heredoc named argument" do
source = expect_issue subject, <<-CRYSTAL
foo 123,
# ^^^^^^ error: Missing parentheses in method call
bar: <<-HEREDOC,
bar
HEREDOC
baz: <<-HEREDOC
baz
HEREDOC
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(123,
bar: <<-HEREDOC,
bar
HEREDOC
baz: <<-HEREDOC)
baz
HEREDOC
CRYSTAL
end
it "removes stray backslash from the end of a first line" do
source = expect_issue subject, <<-CRYSTAL
foo \\
# ^^^ error: Missing parentheses in method call
bar: 1,
baz: 2
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(
bar: 1,
baz: 2)
CRYSTAL
end
it "fails for method call with named + heredoc argument" do
source = expect_issue subject, <<-CRYSTAL
foo <<-HEREDOC, bar: 42
# ^^^^^^^^^^^^^^^^^^^^^ error: Missing parentheses in method call
HEREDOC
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(<<-HEREDOC, bar: 42)
HEREDOC
CRYSTAL
end
it "fails for method call with heredoc argument + more arguments following" do
source = expect_issue subject, <<-CRYSTAL
foo <<-HEREDOC,
# ^^^^^^^^^^^^^ error: Missing parentheses in method call
foo
HEREDOC
"bar"
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(<<-HEREDOC,
foo
HEREDOC
"bar")
CRYSTAL
end
context "properties" do
context "#exclude_type_declarations" do
it "ignores type declarations when enabled" do
rule = CallParentheses.new
rule.exclude_type_declarations = true
expect_no_issues rule, <<-CRYSTAL
foo bar : Symbol
CRYSTAL
end
it "reports type declarations when disabled" do
rule = CallParentheses.new
rule.exclude_type_declarations = false
source = expect_issue rule, <<-CRYSTAL
foo bar : Symbol
# ^^^^^^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
expect_correction source, <<-CRYSTAL
foo(bar : Symbol)
CRYSTAL
end
end
context "#exclude_heredocs" do
it "ignores calls with heredoc arguments when enabled" do
rule = CallParentheses.new
rule.exclude_heredocs = true
expect_no_issues rule, <<-CRYSTAL
foo bar : Symbol
CRYSTAL
end
it "reports calls with heredoc arguments when disabled" do
rule = CallParentheses.new
rule.exclude_heredocs = false
source = expect_issue rule, <<-CRYSTAL
foo.should eq <<-HEREDOC
# ^^^^^^^^^^^^^ error: Missing parentheses in method call
HEREDOC
CRYSTAL
expect_correction source, <<-CRYSTAL
foo.should eq(<<-HEREDOC)
HEREDOC
CRYSTAL
end
end
context "#excluded_toplevel_call_names" do
it "ignores top level calls" do
rule = CallParentheses.new
rule.excluded_toplevel_call_names = %w[foo bar]
expect_no_issues rule, <<-CRYSTAL
foo bar
bar baz
CRYSTAL
expect_issue rule, <<-CRYSTAL
foo.bar baz
# ^^^^^^^^^ error: Missing parentheses in method call
CRYSTAL
end
end
context "#excluded_call_names" do
it "ignores non-top level calls" do
rule = CallParentheses.new
rule.excluded_call_names = %w[foo bar]
expect_no_issues rule, <<-CRYSTAL
foo.bar baz
bar.foo baz
CRYSTAL
expect_issue rule, <<-CRYSTAL
foo bar
# ^^^^^ error: Missing parentheses in method call
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/elsif_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe Elsif do
subject = Elsif.new
it "does not report an issue for if statements" do
expect_no_issues subject, <<-CRYSTAL
def func
if something
foo
end
end
CRYSTAL
end
it "does not report an issue for if/else statements" do
expect_no_issues subject, <<-CRYSTAL
def func
if something
foo
else
bar
end
end
CRYSTAL
end
it "does not report an issue for if/else statements with suffix if by default" do
expect_no_issues subject, <<-CRYSTAL
def func
if something
foo
else
bar if something_else
end
end
CRYSTAL
end
it "does not report an issue for if statements (ternary)" do
expect_no_issues subject, <<-CRYSTAL
def func
something ? foo : bar
end
CRYSTAL
end
it "does not report an issue for if statements (else + ternary if)" do
expect_no_issues subject, <<-CRYSTAL
def func
something ? foo : something_else ? bar : baz
end
CRYSTAL
end
it "reports an issue for if/elsif statements" do
expect_issue subject, <<-CRYSTAL
def func
if something
# ^^^^^^^^^^^^ error: Prefer `case/when` over `if/elsif`
foo
elsif something_else
bar
end
end
CRYSTAL
end
context "properties" do
it "#ignore_suffix" do
rule = Elsif.new
rule.ignore_suffix = false
expect_issue rule, <<-CRYSTAL
def func
if something
# ^^^^^^^^^^^^ error: Prefer `case/when` over `if/elsif`
foo
else
bar if something_else
end
end
CRYSTAL
end
it "#allowed_branches" do
rule = Elsif.new
rule.max_branches = 1
expect_no_issues rule, <<-CRYSTAL
def func
if something
foo
elsif something_else
bar
end
end
CRYSTAL
expect_issue rule, <<-CRYSTAL
def func
if something
# ^^^^^^^^^^^^ error: Prefer `case/when` over `if/elsif`
foo
elsif something_bar
bar
elsif something_baz
baz
end
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/style/guard_clause_spec.cr
================================================
require "../../../spec_helper"
private def it_reports_body(body, *, file = __FILE__, line = __LINE__)
rule = Ameba::Rule::Style::GuardClause.new
it "reports an issue if method body is if / unless without else", file, line do
source = expect_issue rule, <<-CRYSTAL, file: file, line: line
def func
if something
# ^^ error: Use a guard clause (`return unless something`) instead of wrapping the code inside a conditional expression
#{body}
end
end
def func
unless something
# ^^^^^^ error: Use a guard clause (`return if something`) instead of wrapping the code inside a conditional expression
#{body}
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL, file: file, line: line
def func
return unless something
#{body}
#{trailing_whitespace}
end
def func
return if something
#{body}
#{trailing_whitespace}
end
CRYSTAL
end
it "reports an issue if method body ends with if / unless without else", file, line do
source = expect_issue rule, <<-CRYSTAL, file: file, line: line
def func
test
if something
# ^^ error: Use a guard clause (`return unless something`) instead of wrapping the code inside a conditional expression
#{body}
end
end
def func
test
unless something
# ^^^^^^ error: Use a guard clause (`return if something`) instead of wrapping the code inside a conditional expression
#{body}
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL, file: file, line: line
def func
test
return unless something
#{body}
#{trailing_whitespace}
end
def func
test
return if something
#{body}
#{trailing_whitespace}
end
CRYSTAL
end
end
private def it_reports_control_expression(kw, *, file = __FILE__, line = __LINE__)
rule = Ameba::Rule::Style::GuardClause.new
it "reports an issue with #{kw} in the if branch", file, line do
source = expect_issue rule, <<-CRYSTAL, file: file, line: line
def func
if something
# ^^ error: Use a guard clause (`#{kw} if something`) instead of wrapping the code inside a conditional expression
#{kw}
else
puts "hello"
end
end
CRYSTAL
expect_no_corrections source, file: file, line: line
end
it "reports an issue with #{kw} in the else branch", file, line do
source = expect_issue rule, <<-CRYSTAL, file: file, line: line
def func
if something
# ^^ error: Use a guard clause (`#{kw} unless something`) instead of wrapping the code inside a conditional expression
puts "hello"
else
#{kw}
end
end
CRYSTAL
expect_no_corrections source, file: file, line: line
end
it "doesn't report an issue if condition has multiple lines", file, line do
expect_no_issues rule, <<-CRYSTAL, file: file, line: line
def func
if something &&
something_else
#{kw}
else
puts "hello"
end
end
CRYSTAL
end
it "does not report an issue if #{kw} is inside elsif", file, line do
expect_no_issues rule, <<-CRYSTAL, file: file, line: line
def func
if something
a
elsif something_else
#{kw}
end
end
CRYSTAL
end
it "does not report an issue if #{kw} is inside if..elsif..else..end", file, line do
expect_no_issues rule, <<-CRYSTAL, file: file, line: line
def func
if something
a
elsif something_else
b
else
#{kw}
end
end
CRYSTAL
end
it "doesn't report an issue if control flow expr has multiple lines", file, line do
expect_no_issues rule, <<-CRYSTAL, file: file, line: line
def func
if something
#{kw} \\
"blah blah blah" \\
"blah blah blah"
else
puts "hello"
end
end
CRYSTAL
end
it "reports an issue if non-control-flow branch has multiple lines", file, line do
source = expect_issue rule, <<-CRYSTAL, file: file, line: line
def func
if something
# ^^ error: Use a guard clause (`#{kw} if something`) instead of wrapping the code inside a conditional expression
#{kw}
else
puts "hello" \\
"blah blah blah"
end
end
CRYSTAL
expect_no_corrections source, file: file, line: line
end
end
module Ameba::Rule::Style
describe GuardClause do
subject = GuardClause.new
it_reports_body "work"
it_reports_body "# TODO"
pending "does not report an issue if `else` branch is present but empty" do
expect_no_issues subject, <<-CRYSTAL
def method
if bar = foo
puts bar
else
# nothing
end
end
CRYSTAL
end
it "does not report an issue if body is if..elsif..end" do
expect_no_issues subject, <<-CRYSTAL
def func
if something
a
elsif something_else
b
end
end
CRYSTAL
end
it "doesn't report an issue if condition has multiple lines" do
expect_no_issues subject, <<-CRYSTAL
def func
if something &&
something_else
work
end
end
def func
unless something &&
something_else
work
end
end
CRYSTAL
end
it "accepts a method body that is if / unless with else" do
expect_no_issues subject, <<-CRYSTAL
def func
if something
work
else
test
end
end
def func
unless something
work
else
test
end
end
CRYSTAL
end
it "reports an issue when using `|| raise` in `then` branch" do
source = expect_issue subject, <<-CRYSTAL
def func
if something
# ^^ error: Use a guard clause (`work || raise("message") if something`) instead of [...]
work || raise("message")
else
test
end
end
CRYSTAL
expect_no_corrections source
end
it "reports an issue when using `|| raise` in `else` branch" do
source = expect_issue subject, <<-CRYSTAL
def func
if something
# ^^ error: Use a guard clause (`test || raise("message") unless something`) instead of [...]
work
else
test || raise("message")
end
end
CRYSTAL
expect_no_corrections source
end
it "reports an issue when using `&& return` in `then` branch" do
source = expect_issue subject, <<-CRYSTAL
def func
if something
# ^^ error: Use a guard clause (`work && return if something`) instead of [...]
work && return
else
test
end
end
CRYSTAL
expect_no_corrections source
end
it "reports an issue when using `&& return` in `else` branch" do
source = expect_issue subject, <<-CRYSTAL
def func
if something
# ^^ error: Use a guard clause (`test && return unless something`) instead of [...]
work
else
test && return
end
end
CRYSTAL
expect_no_corrections source
end
it "accepts a method body that does not end with if / unless" do
expect_no_issues subject, <<-CRYSTAL
def func
if something
work
end
test
end
def func
unless something
work
end
test
end
CRYSTAL
end
it "accepts a method body that is a modifier if / unless" do
expect_no_issues subject, <<-CRYSTAL
def func
work if something
end
def func
work unless something
end
CRYSTAL
end
it "accepts a method with empty parentheses as its body" do
expect_no_issues subject, <<-CRYSTAL
def func
()
end
CRYSTAL
end
it "does not report an issue when assigning the result of a guard condition with `else`" do
expect_no_issues subject, <<-CRYSTAL
def func
result =
if something
work || raise("message")
else
test
end
end
CRYSTAL
end
it_reports_control_expression "return"
it_reports_control_expression "next"
it_reports_control_expression "break"
it_reports_control_expression %(raise "error")
context "method in module" do
it "reports an issue for instance method" do
source = expect_issue subject, <<-CRYSTAL
module CopTest
def test
if something
# ^^ error: Use a guard clause (`return unless something`) instead of [...]
work
end
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
module CopTest
def test
return unless something
work
#{trailing_whitespace}
end
end
CRYSTAL
end
it "reports an issue for singleton methods" do
source = expect_issue subject, <<-CRYSTAL
module CopTest
def self.test
if something && something_else
# ^^ error: Use a guard clause (`return unless something && something_else`) instead of [...]
work
end
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
module CopTest
def self.test
return unless something && something_else
work
#{trailing_whitespace}
end
end
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/style/hash_literal_syntax_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe HashLiteralSyntax do
subject = HashLiteralSyntax.new
it "passes for an hash literal with elements" do
expect_no_issues subject, <<-CRYSTAL
{1 => 2, 3 => 4} of Int32 => Int32
CRYSTAL
end
it "passes for an hash-like literal" do
expect_no_issues subject, <<-CRYSTAL
Hash{1 => 2, 3 => 4}
CRYSTAL
end
# Hash literals in macros are semantically different from `Hash(K, V).new`
it "passes for an empty hash literal in a macro" do
expect_no_issues subject, <<-CRYSTAL
macro foo(bar = {} of String => String)
{% for b, c in bar %}
{{ b.id }} % {{ c.id }}
{% end %}
{% baz = {} of Int32 => Int32 %}
end
{% qux = {} of Int32 => Int32 %}
CRYSTAL
end
it "fails for an hash literal without elements" do
source = expect_issue subject, <<-CRYSTAL
{} of Int32 => Int32
# ^^^^^^^^^^^^^^^^^^ error: Use `Hash(Int32, Int32).new` for creating an empty hash
CRYSTAL
expect_correction source, <<-CRYSTAL
Hash(Int32, Int32).new
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/heredoc_escape_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe HeredocEscape do
subject = HeredocEscape.new
it "passes if a heredoc doesn't contain interpolation" do
expect_no_issues subject, <<-CRYSTAL
<<-HEREDOC
foo
HEREDOC
CRYSTAL
end
it "passes if a heredoc contains interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
<<-HEREDOC
foo #{:bar}
HEREDOC
CRYSTAL
end
it "passes if a heredoc contains normal and escaped interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
<<-HEREDOC
foo \#{:bar} #{:baz}
HEREDOC
CRYSTAL
end
it "passes if a heredoc contains an escape sequence and escaped interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
<<-HEREDOC
foo \377 \xFF \uFFFF \u{0} \t \n \#{:baz}
HEREDOC
CRYSTAL
end
it "passes if a heredoc contains an escaped escape sequence and interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
<<-HEREDOC
foo \\377 \\xFF \\uFFFF \\u{0} \\t \\n #{:baz}
HEREDOC
CRYSTAL
end
it "fails if a heredoc contains escaped interpolation" do
expect_issue subject, <<-'CRYSTAL'
<<-HEREDOC
# ^^^^^^^^ error: Use an escaped heredoc marker: `<<-'HEREDOC'`
foo \#{:bar}
HEREDOC
CRYSTAL
end
it "fails if a heredoc contains escaped interpolation and escaped escape sequences" do
expect_issue subject, <<-'CRYSTAL'
<<-HEREDOC
# ^^^^^^^^ error: Use an escaped heredoc marker: `<<-'HEREDOC'`
foo \\t \#{:bar}
HEREDOC
CRYSTAL
end
it "passes if a heredoc contains normal and escaped escape sequences" do
expect_no_issues subject, <<-'CRYSTAL'
<<-HEREDOC
foo \t \n | \\t \\n
HEREDOC
CRYSTAL
end
it "fails if a heredoc contains escaped escape sequences" do
expect_issue subject, <<-'CRYSTAL'
<<-HEREDOC
# ^^^^^^^^ error: Use an escaped heredoc marker: `<<-'HEREDOC'`
\\t \\n
HEREDOC
CRYSTAL
end
it "passes if an escaped heredoc contains interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
<<-'HEREDOC'
foo #{:bar}
HEREDOC
CRYSTAL
end
it "passes if an escaped heredoc contains escaped interpolation" do
expect_no_issues subject, <<-'CRYSTAL'
<<-'HEREDOC'
foo \#{:bar}
HEREDOC
CRYSTAL
end
it "passes if an escaped heredoc contains escape sequences" do
expect_no_issues subject, <<-'CRYSTAL'
<<-'HEREDOC'
foo \377 \xFF \uFFFF \u{0} \t \n
HEREDOC
CRYSTAL
end
it "passes if an escaped heredoc contains Regex escape sequences" do
expect_no_issues subject, <<-'CRYSTAL'
<<-'REGEX'
^(?:(?\d+)(?:d|\s*days?),?\s*)?
(?:(?\d+)(?:h|\s*hours?),?\s*)?
(?:(?\d+)(?:m|\s*min(?:utes?)?),?\s*)?
(?:(?\d+)(?:s|\s*sec(?:onds?)?))?$
REGEX
CRYSTAL
end
it "passes if an escaped heredoc contains escaped escape sequences" do
expect_no_issues subject, <<-'CRYSTAL'
<<-'HEREDOC'
foo \\377 \\xFF \\uFFFF \\u{0} \\t \\n
HEREDOC
CRYSTAL
end
it "fails if an escaped heredoc doesn't contain interpolation" do
expect_issue subject, <<-CRYSTAL
<<-'HEREDOC'
# ^^^^^^^^^^ error: Use an unescaped heredoc marker: `<<-HEREDOC`
foo
HEREDOC
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/heredoc_indent_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe HeredocIndent do
subject = HeredocIndent.new
it "passes if heredoc body indented one level" do
expect_no_issues subject, <<-CRYSTAL
<<-HEREDOC
hello world
HEREDOC
<<-HEREDOC
hello world
HEREDOC
CRYSTAL
end
it "fails if the heredoc body is indented incorrectly" do
source = expect_issue subject, <<-CRYSTAL
<<-ONE
# ^^^^ error: Heredoc body should be indented by 2 spaces
hello world
ONE
<<-TWO
# ^^^^^^ error: Heredoc body should be indented by 2 spaces
hello world
TWO
<<-THREE
# ^^^^^^^^ error: Heredoc body should be indented by 2 spaces
hello world
THREE
<<-FOUR
# ^^^^^^^ error: Heredoc body should be indented by 2 spaces
hello world
FOUR
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-ONE
hello world
ONE
<<-TWO
hello world
TWO
<<-THREE
hello world
THREE
<<-FOUR
hello world
FOUR
CRYSTAL
end
it "keeps the indentation within the heredoc string" do
source = expect_issue subject, <<-CRYSTAL
<<-HTML
# ^^^^^ error: Heredoc body should be indented by 2 spaces
HTML
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-HTML
HTML
CRYSTAL
end
it "fixes the indentation within the heredoc string" do
source = expect_issue subject, <<-CRYSTAL
<<-HTML
# ^^^^^ error: Heredoc body should be indented by 2 spaces
HTML
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-HTML
HTML
CRYSTAL
end
it "fixes the indentation within the heredoc string with empty line" do
source = expect_issue subject, <<-CRYSTAL
<<-HTML
# ^^^^^ error: Heredoc body should be indented by 2 spaces
HTML
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-HTML
HTML
CRYSTAL
end
it "works with empty heredoc string" do
source = expect_issue subject, <<-CRYSTAL
<<-HTML
# ^^^^^ error: Heredoc body should be indented by 2 spaces
HTML
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-HTML
HTML
CRYSTAL
end
context "properties" do
context "#indent_by" do
rule = HeredocIndent.new
rule.indent_by = 0
it "passes if heredoc body has the same indent level" do
expect_no_issues rule, <<-CRYSTAL
<<-HEREDOC
hello world
HEREDOC
<<-HEREDOC
hello world
HEREDOC
CRYSTAL
end
it "fails if the heredoc body is indented incorrectly" do
source = expect_issue rule, <<-CRYSTAL
<<-ONE
# ^^^^ error: Heredoc body should be indented by 0 spaces
hello world
ONE
<<-TWO
# ^^^^^^ error: Heredoc body should be indented by 0 spaces
hello world
TWO
<<-FOUR
# ^^^^^^^ error: Heredoc body should be indented by 0 spaces
hello world
FOUR
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-ONE
hello world
ONE
<<-TWO
hello world
TWO
<<-FOUR
hello world
FOUR
CRYSTAL
end
end
context "#body_auto_dedent" do
rule = HeredocIndent.new
rule.body_auto_dedent = false
it "leaves the indentation within the heredoc string" do
source = expect_issue rule, <<-CRYSTAL
<<-HTML
# ^^^^^ error: Heredoc body should be indented by 2 spaces
HTML
CRYSTAL
expect_correction source, <<-CRYSTAL
<<-HTML
HTML
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/is_a_filter_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe IsAFilter do
subject = IsAFilter.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
[1, 2, nil].select(Int32)
[1, 2, nil].reject(Nil)
CRYSTAL
end
it "reports if there is .is_a? call within select" do
source = expect_issue subject, <<-CRYSTAL
[1, 2, nil].select(&.is_a?(Int32))
# ^^^^^^^^^^^^^^^^^^^^^^ error: Use `select(Int32)` instead of `select {...}`
CRYSTAL
expect_correction source, <<-CRYSTAL
[1, 2, nil].select(Int32)
CRYSTAL
end
it "reports if there is .nil? call within reject" do
source = expect_issue subject, <<-CRYSTAL
[1, 2, nil].reject(&.nil?)
# ^^^^^^^^^^^^^^ error: Use `reject(Nil)` instead of `reject {...}`
CRYSTAL
expect_correction source, <<-CRYSTAL
[1, 2, nil].reject(Nil)
CRYSTAL
end
it "does not report if there .is_a? call within block with multiple arguments" do
expect_no_issues subject, <<-CRYSTAL
t.all? { |_, v| v.is_a?(String) }
t.all? { |foo, bar| foo.is_a?(String) }
t.all? { |foo, bar| bar.is_a?(String) }
CRYSTAL
end
context "properties" do
it "#filter_names" do
rule = IsAFilter.new
rule.filter_names = %w[select]
expect_no_issues rule, <<-CRYSTAL
[1, 2, nil].reject(&.nil?)
CRYSTAL
end
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ [1, 2, nil].reject(&.nil?) }}
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/style/is_a_nil_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe IsANil do
subject = IsANil.new
it "doesn't report if there are no is_a?(Nil) calls" do
expect_no_issues subject, <<-CRYSTAL
a = 1
a.nil?
a.is_a?(NilLiteral)
a.is_a?(Custom::Nil)
CRYSTAL
end
it "reports if there is a call to is_a?(Nil) without receiver" do
source = expect_issue subject, <<-CRYSTAL
a = is_a?(Nil)
# ^^^ error: Use `nil?` instead of `is_a?(Nil)`
CRYSTAL
expect_correction source, <<-CRYSTAL
a = self.nil?
CRYSTAL
end
it "reports if there is a call to is_a?(Nil) with receiver" do
source = expect_issue subject, <<-CRYSTAL
a.is_a?(Nil)
# ^^^ error: Use `nil?` instead of `is_a?(Nil)`
CRYSTAL
expect_correction source, <<-CRYSTAL
a.nil?
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/large_numbers_spec.cr
================================================
require "../../../spec_helper"
private def it_transforms(number, expected, *, file = __FILE__, line = __LINE__)
it "transforms large number #{number}", file, line do
rule = Ameba::Rule::Style::LargeNumbers.new
rule.int_min_digits = 5
source = expect_issue rule, <<-CRYSTAL, number: number, file: file, line: line
number = %{number}
# ^{number} error: Large numbers should be written with underscores: `#{expected}`
CRYSTAL
expect_correction source, <<-CRYSTAL
number = #{expected}
CRYSTAL
end
end
module Ameba::Rule::Style
describe LargeNumbers do
subject = LargeNumbers.new
it "passes if large number does not require underscore" do
expect_no_issues subject, <<-CRYSTAL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16 17 18 19 20 30 40 50 60 70 80 90
100
999
1000
1_000
9999
9_999
10_000
100_000
200_000
300_000
400_000
500_000
600_000
700_000
800_000
900_000
1_000_000
-9_223_372_036_854_775_808
9_223_372_036_854_775_807
141_592_654
141_592_654.0
141_592_654.001
141_592_654.001_2
141_592_654.001_23
141_592_654.001_234
141_592_654.001_234_5
0b1101
0o123
0xFE012D
0xfe012d
0xfe012dd11
1_i8
12_i16
123_i32
1_234_i64
12_u8
123_u16
1_234_u32
9_223_372_036_854_775_808_u64
9_223_372_036_854_775_808.000_123_456_789_f64
+100_u32
-900_000_i32
1_234.5e-7
11_234e10_f32
+1.123
-0.000_5
1200.0
1200.01
1200.012
CRYSTAL
end
it_transforms "10000", "10_000"
it_transforms "+10000", "+10_000"
it_transforms "-10000", "-10_000"
it_transforms "9223372036854775808", "9_223_372_036_854_775_808"
it_transforms "-9223372036854775808", "-9_223_372_036_854_775_808"
it_transforms "+9223372036854775808", "+9_223_372_036_854_775_808"
it_transforms "1_00000", "100_000"
it_transforms "10000_i16", "10_000_i16"
it_transforms "10000_i32", "10_000_i32"
it_transforms "10000_i64", "10_000_i64"
it_transforms "10000_i128", "10_000_i128"
it_transforms "10000_u16", "10_000_u16"
it_transforms "10000_u32", "10_000_u32"
it_transforms "10000_u64", "10_000_u64"
it_transforms "10000_u128", "10_000_u128"
it_transforms "123456_f32", "123_456_f32"
it_transforms "123456_f64", "123_456_f64"
it_transforms "123456.5e-7_f32", "123_456.5e-7_f32"
it_transforms "123456e10_f64", "123_456e10_f64"
it_transforms "123456.5e-7", "123_456.5e-7"
it_transforms "123456e10", "123_456e10"
it_transforms "3.00_1", "3.001"
it_transforms "3.0012", "3.001_2"
it_transforms "3.00123", "3.001_23"
it_transforms "3.001234", "3.001_234"
it_transforms "3.0012345", "3.001_234_5"
context "properties" do
it "#int_min_digits" do
rule = Rule::Style::LargeNumbers.new
rule.int_min_digits = 10
expect_no_issues rule, "1200000"
end
end
end
end
================================================
FILE: spec/ameba/rule/style/multiline_curly_block_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe MultilineCurlyBlock do
subject = MultilineCurlyBlock.new
it "doesn't report if a curly block is on a single line" do
expect_no_issues subject, <<-CRYSTAL
foo { :bar }
CRYSTAL
end
it "doesn't report for `do`...`end` blocks" do
expect_no_issues subject, <<-CRYSTAL
foo do
:bar
end
CRYSTAL
end
it "doesn't report for `do`...`end` blocks on a single line" do
expect_no_issues subject, <<-CRYSTAL
foo do :bar end
CRYSTAL
end
it "reports if there is a multi-line curly block" do
expect_issue subject, <<-CRYSTAL
foo {
# ^ error: Use `do`...`end` instead of curly brackets for multi-line blocks
:bar
}
CRYSTAL
end
it "reports if there is a multi-line curly block with arguments" do
expect_issue subject, <<-CRYSTAL
foo(:bar) {
# ^ error: Use `do`...`end` instead of curly brackets for multi-line blocks
:baz
}
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/multiline_string_literal_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe MultilineStringLiteral do
subject = MultilineStringLiteral.new
context "string literals" do
it "doesn't report if a string is on a single line" do
expect_no_issues subject, <<-CRYSTAL
"foo"
CRYSTAL
end
it "doesn't report if a string containing `\\n` is on a single line" do
expect_no_issues subject, <<-'CRYSTAL'
"\nfoo\n"
CRYSTAL
end
it "doesn't report heredocs" do
expect_no_issues subject, <<-CRYSTAL
<<-FOO
foo
bar
FOO
CRYSTAL
end
it "reports if there is a multi-line string" do
expect_issue subject, <<-CRYSTAL
"
# ^{} error: Use `<<-HEREDOC` markers for multiline strings
foo
bar
"
CRYSTAL
end
it "reports if there is a multi-line percent string: `%()`" do
expect_issue subject, <<-CRYSTAL
%(
# ^{} error: Use `<<-HEREDOC` markers for multiline strings
foo
bar
)
CRYSTAL
end
it "reports if there is a multi-line percent string: `%q()`" do
expect_issue subject, <<-CRYSTAL
%q(
# ^ error: Use `<<-HEREDOC` markers for multiline strings
foo
bar
)
CRYSTAL
end
end
context "string interpolations" do
it "doesn't report if a string is on a single line" do
expect_no_issues subject, <<-CRYSTAL
"#{"foo"}"
CRYSTAL
end
it "doesn't report regex literals" do
expect_no_issues subject, <<-'CRYSTAL'
/\A#{foo}\Z/
CRYSTAL
end
it "doesn't report multiline regex literals" do
expect_no_issues subject, <<-'CRYSTAL'
%r{
\A#{foo}\Z
}x
CRYSTAL
end
it "doesn't report multiline command literals" do
expect_no_issues subject, <<-'CRYSTAL'
%x{
echo "#{foo}"
}
CRYSTAL
end
it "doesn't report command literals" do
expect_no_issues subject, <<-'CRYSTAL'
`#{foo}`
CRYSTAL
end
it "doesn't report if a string containing `\\n` is on a single line" do
expect_no_issues subject, <<-'CRYSTAL'
"#{"\nfoo\n"}"
CRYSTAL
end
it "doesn't report heredocs" do
expect_no_issues subject, <<-CRYSTAL
<<-FOO
foo
#{"bar"}
baz
FOO
CRYSTAL
end
it "reports if there is a multi-line string" do
expect_issue subject, <<-'CRYSTAL'
"
# ^{} error: Use `<<-HEREDOC` markers for multiline strings
#{"foo"}
bar
"
CRYSTAL
end
end
context "properties" do
describe "#allow_backslash_split_strings" do
it "passes on formatter errors by default" do
rule = MultilineStringLiteral.new
expect_no_issues rule, <<-CRYSTAL
"foo" \\
"bar" \\
"baz"
CRYSTAL
end
it "reports on formatter errors when disabled" do
rule = MultilineStringLiteral.new
rule.allow_backslash_split_strings = false
expect_issue rule, <<-CRYSTAL
"foo" \\
# ^^^^^ error: Use `<<-HEREDOC` markers for multiline strings
"bar" \\
"baz"
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/negated_conditions_in_unless_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe NegatedConditionsInUnless do
subject = NegatedConditionsInUnless.new
it "passes with a unless without negated condition" do
expect_no_issues subject, <<-CRYSTAL
unless a
:ok
end
:ok unless b
unless s.empty?
:ok
end
CRYSTAL
end
it "fails if there is a negated condition in unless" do
expect_issue subject, <<-CRYSTAL
unless !a
# ^^^^^^^ error: Avoid negated conditions in unless blocks
:nok
end
CRYSTAL
end
it "fails if one of AND conditions is negated" do
expect_issue subject, <<-CRYSTAL
unless a && !b
# ^^^^^^^^^^^^ error: Avoid negated conditions in unless blocks
:nok
end
CRYSTAL
end
it "fails if one of OR conditions is negated" do
expect_issue subject, <<-CRYSTAL
unless a || !b
# ^^^^^^^^^^^^ error: Avoid negated conditions in unless blocks
:nok
end
CRYSTAL
end
it "fails if one of inner conditions is negated" do
expect_issue subject, <<-CRYSTAL
unless a && (b || !c)
# ^^^^^^^^^^^^^^^^^^^ error: Avoid negated conditions in unless blocks
:nok
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/parentheses_around_condition_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe ParenthesesAroundCondition do
subject = ParenthesesAroundCondition.new
{% for keyword in %w[if unless while until] %}
context "{{ keyword.id }}" do
it "reports if redundant parentheses are found" do
source = expect_issue subject, <<-CRYSTAL, keyword: {{ keyword }}
%{keyword} (foo > 10)
_{keyword} # ^^^^^^^^^^ error: Redundant parentheses
foo
end
CRYSTAL
expect_correction source, <<-CRYSTAL
{{ keyword.id }} foo > 10
foo
end
CRYSTAL
end
end
{% end %}
{% for keyword in %w[if unless].map(&.id) %}
context "{{ keyword }}" do
it "ignores expressions with `rescue`" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} (foo rescue nil)
foo
end
CRYSTAL
end
it "ignores postfix expressions with `rescue`" do
expect_no_issues subject, <<-CRYSTAL
foo {{ keyword }} (foo rescue nil)
CRYSTAL
end
it "ignores expressions with `ensure`" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} (foo ensure bar)
foo
end
CRYSTAL
end
it "ignores postfix expressions with `ensure`" do
expect_no_issues subject, <<-CRYSTAL
foo {{ keyword }} (foo ensure bar)
CRYSTAL
end
it "ignores expressions with `if`" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} (foo if bar)
foo
end
CRYSTAL
end
it "ignores postfix expressions with `if`" do
expect_no_issues subject, <<-CRYSTAL
foo {{ keyword }} (foo if bar)
CRYSTAL
end
it "ignores expressions with `unless`" do
expect_no_issues subject, <<-CRYSTAL
{{ keyword }} (foo unless bar)
foo
end
CRYSTAL
end
it "ignores postfix expressions with `unless`" do
expect_no_issues subject, <<-CRYSTAL
foo {{ keyword }} (foo unless bar)
CRYSTAL
end
end
{% end %}
context "case" do
it "reports if redundant parentheses are found" do
source = expect_issue subject, <<-CRYSTAL
case (foo = @foo)
# ^^^^^^^^^^^^ error: Redundant parentheses
when String then "string"
when Symbol then "symbol"
end
CRYSTAL
expect_correction source, <<-CRYSTAL
case foo = @foo
when String then "string"
when Symbol then "symbol"
end
CRYSTAL
end
end
context "properties" do
context "#exclude_ternary" do
it "reports ternary control expressions by default" do
expect_issue subject, <<-CRYSTAL
(foo.empty?) ? true : false
# ^^^^^^^^^^ error: Redundant parentheses
CRYSTAL
expect_no_issues subject, <<-CRYSTAL
(foo && bar) ? true : false
(foo || bar) ? true : false
(foo = @foo) ? true : false
foo == 42 ? true : false
(foo = 42) ? true : false
(foo > 42) ? true : false
(foo >= 42) ? true : false
(3 >= foo >= 42) ? true : false
(3.in? 0..42) ? true : false
(yield 42) ? true : false
(foo rescue 42) ? true : false
CRYSTAL
end
it "allows to skip ternary control expressions" do
rule = ParenthesesAroundCondition.new
rule.exclude_ternary = true
expect_no_issues rule, <<-CRYSTAL
(foo.empty?) ? true : false
CRYSTAL
end
end
context "#exclude_multiline" do
it "reports multiline expressions by default" do
expect_issue subject, <<-CRYSTAL
if (
# ^ error: Redundant parentheses
foo.empty? ||
bar.empty?
)
baz
end
CRYSTAL
end
it "allows to skip multiline expressions" do
rule = ParenthesesAroundCondition.new
rule.exclude_multiline = true
expect_no_issues rule, <<-CRYSTAL
if (
foo.empty? ||
bar.empty?
)
baz
end
CRYSTAL
end
end
context "#allow_safe_assignment" do
it "reports assignments by default" do
expect_issue subject, <<-CRYSTAL
if (foo = @foo)
# ^^^^^^^^^^^^ error: Redundant parentheses
foo
end
CRYSTAL
expect_no_issues subject, <<-CRYSTAL
if !(foo = @foo)
foo
end
CRYSTAL
expect_no_issues subject, <<-CRYSTAL
if foo = @foo
foo
end
CRYSTAL
end
it "allows to configure assignments" do
rule = ParenthesesAroundCondition.new
rule.allow_safe_assignment = true
source = expect_issue rule, <<-CRYSTAL
if foo = @foo
# ^^^^^^^^^^ error: Missing parentheses
foo
end
CRYSTAL
expect_correction source, <<-CRYSTAL
if (foo = @foo)
foo
end
CRYSTAL
expect_no_issues rule, <<-CRYSTAL
if (foo = @foo)
foo
end
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/percent_literal_delimiters_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe PercentLiteralDelimiters do
subject = PercentLiteralDelimiters.new
it "passes if percent literal delimiters are written correctly" do
expect_no_issues subject, <<-CRYSTAL
%(one two three)
%w[one two three]
%i[one two three]
%r{one(two )?three[!]}
CRYSTAL
end
it "fails if percent literal delimiters are written incorrectly" do
expect_issue subject, <<-CRYSTAL
puts %[one two three]
# ^ error: `%`-literals should be delimited by `(` and `)`
puts %w(one two three)
# ^^ error: `%w`-literals should be delimited by `[` and `]`
puts %i(one two three)
# ^^ error: `%i`-literals should be delimited by `[` and `]`
puts %r|one two three|
# ^^ error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL
end
it "corrects incorrect percent literal delimiters" do
source = expect_issue subject, <<-CRYSTAL
puts %[[] () {}]
# ^ error: `%`-literals should be delimited by `(` and `)`
puts %w(
# ^^ error: `%w`-literals should be delimited by `[` and `]`
one two three
)
puts %i(
# ^^ error: `%i`-literals should be delimited by `[` and `]`
one
two
three
)
puts %r|one(two )?three[!]|
# ^^ error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL
expect_correction source, <<-CRYSTAL
puts %([] () {})
puts %w[
one two three
]
puts %i[
one
two
three
]
puts %r{one(two )?three[!]}
CRYSTAL
end
context "properties" do
context "#default_delimiters" do
it "allows setting custom values" do
rule = PercentLiteralDelimiters.new
rule.default_delimiters = "||"
rule.preferred_delimiters = {
"%w" => "{}",
} of String => String?
expect_no_issues rule, <<-CRYSTAL
%w{one two three}
%i|one two three|
CRYSTAL
end
it "allows ignoring default delimiters by setting them to `nil`" do
rule = PercentLiteralDelimiters.new
rule.default_delimiters = nil
rule.preferred_delimiters = {
"%Q" => "{}",
} of String => String?
expect_no_issues rule, <<-CRYSTAL
%w(one two three)
%i|one two three|
%r
CRYSTAL
expect_issue rule, <<-CRYSTAL
%Q[one two three]
# ^{} error: `%Q`-literals should be delimited by `{` and `}`
CRYSTAL
end
end
context "#preferred_delimiters" do
it "allows setting custom values" do
rule = PercentLiteralDelimiters.new
rule.preferred_delimiters = {
"%w" => "()",
"%i" => "||",
} of String => String?
expect_no_issues rule, <<-CRYSTAL
%w(one two three)
%i|one two three|
CRYSTAL
end
it "allows ignoring certain delimiters by setting them to `nil`" do
rule = PercentLiteralDelimiters.new
rule.preferred_delimiters["%r"] = nil
expect_no_issues rule, <<-CRYSTAL
%r[foo(bar)?]
%r{foo(bar)?}
%r
CRYSTAL
end
end
context "#ignore_literals_containing_delimiters?" do
it "ignores different delimiters if enabled" do
rule = PercentLiteralDelimiters.new
rule.ignore_literals_containing_delimiters = true
expect_issue rule, <<-CRYSTAL
%[one two three]
# ^{} error: `%`-literals should be delimited by `(` and `)`
%w(one two three)
# ^{} error: `%w`-literals should be delimited by `[` and `]`
%i(one two three)
# ^{} error: `%i`-literals should be delimited by `[` and `]`
%r
# ^{} error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL
expect_no_issues rule, <<-CRYSTAL
%[one (two) three]
%w([] []?)
%i([] []?)
%r
CRYSTAL
end
it "ignores different delimiters if disabled" do
rule = PercentLiteralDelimiters.new
rule.ignore_literals_containing_delimiters = false
expect_issue rule, <<-CRYSTAL
%[(one two three)]
# ^{} error: `%`-literals should be delimited by `(` and `)`
%w([] []?)
# ^{} error: `%w`-literals should be delimited by `[` and `]`
%i([] []?)
# ^{} error: `%i`-literals should be delimited by `[` and `]`
%r
# ^{} error: `%r`-literals should be delimited by `{` and `}`
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/redundant_begin_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe RedundantBegin do
subject = RedundantBegin.new
it "passes if there is no redundant begin blocks" do
expect_no_issues subject, <<-CRYSTAL
def method
do_something
rescue
do_something_else
end
def method
do_something
do_something_else
ensure
handle_something
end
def method
yield
rescue
end
def method; end
def method; a = 1; rescue; end
def method; begin; rescue; end; end
CRYSTAL
end
it "passes if there is a correct begin block in a handler" do
expect_no_issues subject, <<-CRYSTAL
def handler_and_expression
begin
open_file
rescue
close_file
end
do_some_stuff
end
def multiple_handlers
begin
begin1
rescue
end
begin
begin2
rescue
end
rescue
do_something_else
end
def assign_and_begin
@result ||=
begin
do_something
do_something_else
returnit
end
rescue
end
def inner_handler
s = begin
rescue
end
rescue
end
def begin_and_expression
begin
a = 1
b = 2
end
expr
end
CRYSTAL
end
it "fails if there is a redundant begin block" do
source = expect_issue subject, <<-CRYSTAL
def method(a : String) : String
begin
# ^^^^^ error: Redundant `begin` block detected
open_file
do_some_stuff
ensure
close_file
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a : String) : String
#{trailing_whitespace}
open_file
do_some_stuff
ensure
close_file
#{trailing_whitespace}
end
CRYSTAL
end
it "fails if there is a redundant begin block in a method without args" do
source = expect_issue subject, <<-CRYSTAL
def method
begin
# ^^^^^ error: Redundant `begin` block detected
open_file
ensure
close_file
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method
#{trailing_whitespace}
open_file
ensure
close_file
#{trailing_whitespace}
end
CRYSTAL
end
it "fails if there is a redundant block in a method with return type" do
source = expect_issue subject, <<-CRYSTAL
def method : String
begin
# ^^^^^ error: Redundant `begin` block detected
open_file
ensure
close_file
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method : String
#{trailing_whitespace}
open_file
ensure
close_file
#{trailing_whitespace}
end
CRYSTAL
end
it "fails if there is a redundant block in a method with multiple args" do
source = expect_issue subject, <<-CRYSTAL
def method(a : String,
b : String)
begin
# ^^^^^ error: Redundant `begin` block detected
open_file
ensure
close_file
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a : String,
b : String)
#{trailing_whitespace}
open_file
ensure
close_file
#{trailing_whitespace}
end
CRYSTAL
end
it "fails if there is a redundant block in a method with multiple args" do
source = expect_issue subject, <<-CRYSTAL
def method(a : String,
b : String
)
begin
# ^^^^^ error: Redundant `begin` block detected
open_file
ensure
close_file
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a : String,
b : String
)
#{trailing_whitespace}
open_file
ensure
close_file
#{trailing_whitespace}
end
CRYSTAL
end
it "doesn't report if there is an inner redundant block" do
expect_no_issues subject, <<-CRYSTAL
def method
begin
open_file
ensure
close_file
end
rescue
end
CRYSTAL
end
it "fails if there is a redundant block with yield" do
source = expect_issue subject, <<-CRYSTAL
def method
begin
# ^^^^^ error: Redundant `begin` block detected
yield
ensure
close_file
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method
#{trailing_whitespace}
yield
ensure
close_file
#{trailing_whitespace}
end
CRYSTAL
end
it "fails if there is a redundant block with string with inner quotes" do
source = expect_issue subject, <<-CRYSTAL
def method
begin
# ^^^^^ error: Redundant `begin` block detected
"'"
rescue
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method
#{trailing_whitespace}
"'"
rescue
#{trailing_whitespace}
end
CRYSTAL
end
it "fails if there is top level redundant block in a method" do
source = expect_issue subject, <<-CRYSTAL
def method
begin
# ^^^^^ error: Redundant `begin` block detected
a = 1
b = 2
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method
#{trailing_whitespace}
a = 1
b = 2
#{trailing_whitespace}
end
CRYSTAL
end
it "doesn't report if begin-end block in a proc literal" do
expect_no_issues subject, <<-CRYSTAL
foo = ->{
begin
raise "Foo!"
rescue ex
pp ex
end
}
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/redundant_next_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe RedundantNext do
subject = RedundantNext.new
it "does not report if there is no redundant next" do
expect_no_issues subject, <<-CRYSTAL
array.map { |x| x + 1 }
CRYSTAL
end
it "reports if there is redundant next with argument in the block" do
source = expect_issue subject, <<-CRYSTAL
block do |v|
next v + 1
# ^^^^^^^^^^ error: Redundant `next` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |v|
v + 1
end
CRYSTAL
end
context "if" do
it "doesn't report if there is not redundant next in if branch" do
expect_no_issues subject, <<-CRYSTAL
block do |v|
next if v > 10
end
CRYSTAL
end
it "reports if there is redundant next in if/else branch" do
source = expect_issue subject, <<-CRYSTAL
block do |a|
if a > 0
next a + 1
# ^^^^^^^^^^ error: Redundant `next` detected
else
next a + 2
# ^^^^^^^^^^ error: Redundant `next` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |a|
if a > 0
a + 1
else
a + 2
end
end
CRYSTAL
end
end
context "unless" do
it "doesn't report if there is no redundant next in unless branch" do
expect_no_issues subject, <<-CRYSTAL
block do |v|
next unless v > 10
end
CRYSTAL
end
it "reports if there is redundant next in unless/else branch" do
source = expect_issue subject, <<-CRYSTAL
block do |a|
unless a > 0
next a + 1
# ^^^^^^^^^^ error: Redundant `next` detected
else
next a + 2
# ^^^^^^^^^^ error: Redundant `next` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |a|
unless a > 0
a + 1
else
a + 2
end
end
CRYSTAL
end
end
context "expressions" do
it "doesn't report if there is no redundant next in expressions" do
expect_no_issues subject, <<-CRYSTAL
block do |v|
a = 1
a + v
end
CRYSTAL
end
it "reports if there is redundant next in expressions" do
source = expect_issue subject, <<-CRYSTAL
block do |a|
a = 1
next a
# ^^^^^^ error: Redundant `next` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |a|
a = 1
a
end
CRYSTAL
end
end
context "binary-op" do
it "doesn't report if there is no redundant next in binary op" do
expect_no_issues subject, <<-CRYSTAL
block do |v|
a && v
end
CRYSTAL
end
it "reports if there is redundant next in binary op" do
source = expect_issue subject, <<-CRYSTAL
block do |a|
a && next a
# ^^^^^^ error: Redundant `next` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |a|
a && a
end
CRYSTAL
end
end
context "exception handler" do
it "doesn't report if there is no redundant next in exception handler" do
expect_no_issues subject, <<-CRYSTAL
block do |v|
v + 1
rescue
next v if v > 0
end
CRYSTAL
end
it "reports if there is redundant next in exception handler" do
source = expect_issue subject, <<-CRYSTAL
block do |a|
next a + 1
# ^^^^^^^^^^ error: Redundant `next` detected
rescue ArgumentError
next a + 2
# ^^^^^^^^^^ error: Redundant `next` detected
rescue Exception
a + 2
next a
# ^^^^^^ error: Redundant `next` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |a|
a + 1
rescue ArgumentError
a + 2
rescue Exception
a + 2
a
end
CRYSTAL
end
end
context "properties" do
context "#allow_multi_next" do
it "allows multi next statements by default" do
expect_no_issues subject, <<-CRYSTAL
block do |a, b|
next a, b
end
CRYSTAL
end
it "allows to configure multi next statements" do
rule = RedundantNext.new
rule.allow_multi_next = false
source = expect_issue rule, <<-CRYSTAL
block do |a, b|
next a, b
# ^^^^^^^^^ error: Redundant `next` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
block do |a, b|
{a, b}
end
CRYSTAL
end
end
context "#allow_empty_next" do
it "allows empty next statements by default" do
expect_no_issues subject, <<-CRYSTAL
block do
next
end
CRYSTAL
end
it "allows to configure empty next statements" do
rule = RedundantNext.new
rule.allow_empty_next = false
source = expect_issue rule, <<-CRYSTAL
block do
next
# ^^^^ error: Redundant `next` detected
end
CRYSTAL
expect_no_corrections source
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/redundant_nil_in_control_expression_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe RedundantNilInControlExpression do
subject = RedundantNilInControlExpression.new
it "passes for return with no expression" do
expect_no_issues subject, <<-CRYSTAL
def foo
return if empty?
end
CRYSTAL
end
it "passes for return with expression" do
expect_no_issues subject, <<-CRYSTAL
def foo
return :nil if empty?
end
CRYSTAL
end
it "reports `return nil` constructs" do
source = expect_issue subject, <<-CRYSTAL
def foo
return nil if empty?
# ^^^ error: Redundant `nil` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo
return if empty?
end
CRYSTAL
end
it "reports `return(nil)` constructs" do
source = expect_issue subject, <<-CRYSTAL
def foo
return(nil) if empty?
# ^^^ error: Redundant `nil` detected
end
CRYSTAL
expect_no_corrections source
end
it "reports `return nil` constructs (deep)" do
expect_issue subject, <<-CRYSTAL
def foo
if foo?
%w[foo bar].each do |v|
return nil if v.empty?
# ^^^ error: Redundant `nil` detected
end
end
end
CRYSTAL
end
it "reports `break nil` constructs" do
source = expect_issue subject, <<-CRYSTAL
def foo
%w[foo bar].any? do |word|
break nil if word == "foo"
# ^^^ error: Redundant `nil` detected
word.ascii_only?
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo
%w[foo bar].any? do |word|
break if word == "foo"
word.ascii_only?
end
end
CRYSTAL
end
it "reports `next nil` constructs" do
source = expect_issue subject, <<-CRYSTAL
def foo
%w[foo bar].any? do |word|
next nil if word == "foo"
# ^^^ error: Redundant `nil` detected
word.ascii_only?
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo
%w[foo bar].any? do |word|
next if word == "foo"
word.ascii_only?
end
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/redundant_return_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe RedundantReturn do
subject = RedundantReturn.new
it "does not report if there is no return" do
expect_no_issues subject, <<-CRYSTAL
def inc(a)
a + 1
end
CRYSTAL
end
it "reports if there is redundant return in method body" do
source = expect_issue subject, <<-CRYSTAL
def inc(a)
return a + 1
# ^^^^^^^^^^^^ error: Redundant `return` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def inc(a)
a + 1
end
CRYSTAL
end
it "doesn't report if it returns tuple literal" do
expect_no_issues subject, <<-CRYSTAL
def foo(a)
return a, a + 2
end
CRYSTAL
end
it "doesn't report if there are other expressions after control flow" do
expect_no_issues subject, <<-CRYSTAL
def method(a)
case a
when true then return true
when .nil? then return :nil
end
false
rescue
nil
end
CRYSTAL
end
context "if" do
it "doesn't report if there is return in if branch" do
expect_no_issues subject, <<-CRYSTAL
def inc(a)
return a + 1 if a > 0
end
CRYSTAL
end
it "reports if there are returns in if/else branch" do
source = expect_issue subject, <<-CRYSTAL
def inc(a)
do_something(a)
if a > 0
return :positive
# ^^^^^^^^^^^^^^^^ error: Redundant `return` detected
else
return :negative
# ^^^^^^^^^^^^^^^^ error: Redundant `return` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def inc(a)
do_something(a)
if a > 0
:positive
else
:negative
end
end
CRYSTAL
end
end
context "unless" do
it "doesn't report if there is return in unless branch" do
expect_no_issues subject, <<-CRYSTAL
def inc(a)
return a + 1 unless a > 0
end
CRYSTAL
end
it "reports if there are returns in unless/else branch" do
source = expect_issue subject, <<-CRYSTAL
def inc(a)
do_something(a)
unless a < 0
return :positive
# ^^^^^^^^^^^^^^^^ error: Redundant `return` detected
else
return :negative
# ^^^^^^^^^^^^^^^^ error: Redundant `return` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def inc(a)
do_something(a)
unless a < 0
:positive
else
:negative
end
end
CRYSTAL
end
end
context "binary op" do
it "doesn't report if there is no return in the right binary op node" do
expect_no_issues subject, <<-CRYSTAL
def can_create?(a)
valid? && a > 0
end
CRYSTAL
end
it "reports if there is return in the right binary op node" do
source = expect_issue subject, <<-CRYSTAL
def can_create?(a)
valid? && return a > 0
# ^^^^^^^^^^^^ error: Redundant `return` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def can_create?(a)
valid? && a > 0
end
CRYSTAL
end
end
context "case" do
it "reports if there are returns in whens" do
source = expect_issue subject, <<-CRYSTAL
def foo(a)
case a
when .nil?
puts "blah"
return nil
# ^^^^^^^^^^ error: Redundant `return` detected
when .blank?
return ""
# ^^^^^^^^^ error: Redundant `return` detected
when true
true
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo(a)
case a
when .nil?
puts "blah"
nil
when .blank?
""
when true
true
end
end
CRYSTAL
end
it "reports if there is return in else" do
source = expect_issue subject, <<-CRYSTAL
def foo(a)
do_something_with(a)
case a
when true
true
else
return false
# ^^^^^^^^^^^^ error: Redundant `return` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo(a)
do_something_with(a)
case a
when true
true
else
false
end
end
CRYSTAL
end
end
context "exception handler" do
it "reports if there are returns in body" do
source = expect_issue subject, <<-CRYSTAL
def foo(a)
return true
# ^^^^^^^^^^^ error: Redundant `return` detected
rescue
false
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo(a)
true
rescue
false
end
CRYSTAL
end
it "reports if there are returns in rescues" do
source = expect_issue subject, <<-CRYSTAL
def foo(a)
true
rescue ArgumentError
return false
# ^^^^^^^^^^^^ error: Redundant `return` detected
rescue RuntimeError
""
rescue Exception
return nil
# ^^^^^^^^^^ error: Redundant `return` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo(a)
true
rescue ArgumentError
false
rescue RuntimeError
""
rescue Exception
nil
end
CRYSTAL
end
it "reports if there are returns in else" do
source = expect_issue subject, <<-CRYSTAL
def foo(a)
true
rescue Exception
nil
else
puts "else branch"
return :bar
# ^^^^^^^^^^^ error: Redundant `return` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def foo(a)
true
rescue Exception
nil
else
puts "else branch"
:bar
end
CRYSTAL
end
end
context "properties" do
context "#allow_multi_return" do
it "allows multi returns by default" do
expect_no_issues subject, <<-CRYSTAL
def method(a, b)
return a, b
end
CRYSTAL
end
it "allows to configure multi returns" do
rule = RedundantReturn.new
rule.allow_multi_return = false
source = expect_issue rule, <<-CRYSTAL
def method(a, b)
return a, b
# ^^^^^^^^^^^ error: Redundant `return` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
def method(a, b)
{a, b}
end
CRYSTAL
end
end
context "#allow_empty_return" do
it "allows empty returns by default" do
expect_no_issues subject, <<-CRYSTAL
def method
return
end
CRYSTAL
end
it "allows to configure empty returns" do
rule = RedundantReturn.new
rule.allow_empty_return = false
source = expect_issue rule, <<-CRYSTAL
def method
return
# ^^^^^^ error: Redundant `return` detected
end
CRYSTAL
expect_no_corrections source
end
end
end
end
end
================================================
FILE: spec/ameba/rule/style/redundant_self_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe RedundantSelf do
subject = RedundantSelf.new
it "does not report solitary `self` reference" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def bar
self
end
end
CRYSTAL
end
it "does not report calls without a receiver" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def foo!
foo || 42
end
end
CRYSTAL
end
it "does not report if `self` is used in a method call with a reserved keyword" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
if self.responds_to?(:with)
self.with { 42 }
end
self.is_a?(Foo) ? self.class : self.as?(Foo)
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a method argument with the same name" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def bar(foo)
foo || self.foo
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a block argument with the same name" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
42
end
def bar
[1, 11, 111].map { |foo| self.foo + foo }
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a block argument with the same name (inside block)" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
42
end
def bar
foo : Int32?
1.try do |x|
2.try do |y|
3.try do |z|
self.foo + foo
end
end
end
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a method argument with the same name inherited from the parent scope" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
42
end
def bar(foo)
[1, 11, 111].map { |n| self.foo + n }
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a proc argument with the same name" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo
42
end
def bar
-> (foo : Int32) { self.foo + foo }
end
end
CRYSTAL
end
it "does not report if `self` is used within the definition of a variable with the same name" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def foo!
foo = self.foo
foo
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a variable with the same name" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def foo!
foo = 42
bar = self.foo
bar
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a type declaration variable with the same name" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def foo!
foo : Int32 = 42
bar = self.foo
bar
end
end
CRYSTAL
end
it "does not report if `self` is used in the presence of a type declaration variable with the same name (2)" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo; end
def foo!
foo : Int32?
bar = self.foo
bar
end
end
CRYSTAL
end
it "does not report if `self` is used with a setter" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo=(value); end
def foo!
self.foo = 42
end
end
CRYSTAL
end
it "does not report if `self` is used with an operator assign" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def foo=(value); end
def foo!
self.foo ||= 42
self.foo &&= 42
self.foo += 42
self.foo -= 42
self.foo *= 42
self.foo /= 42
self.foo %= 42
end
end
CRYSTAL
end
it "does not report if `self` is used with an operator" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def +(value); end
def %(value); end
def <<(value); end
def foo
self + "foo"
self % "foo"
self << "foo"
end
self | String
end
CRYSTAL
end
it "does not report if `self` is used with a square bracket operator" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def []?(i); end
def [](i); end
def []=(i, value); end
def foo?
self[0]?
end
def foo
self[0]
end
def foo!
self[0] = 42
end
end
CRYSTAL
end
it "reports if `self` is used in the presence of a type declaration with the same name in outer scope" do
expect_issue subject, <<-CRYSTAL
class Foo
foo : Int32?
def foo; end
def foo!
bar = self.foo
# ^^^^ error: Redundant `self` detected
bar
end
end
CRYSTAL
end
it "reports if there is redundant `self` used in a method body" do
source = expect_issue subject, <<-CRYSTAL
class Foo
def foo(&); end
def foo!
self.foo { nil } || 42
# ^^^^ error: Redundant `self` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
class Foo
def foo(&); end
def foo!
foo { nil } || 42
end
end
CRYSTAL
end
it "correctly replaces multiline calls" do
source = expect_issue subject, <<-CRYSTAL
class Foo
property bar : String
def foo
self
# ^^^^ error: Redundant `self` detected
.bar
.chars.map do |char|
char.upcase
end
.each { |char| raise "Boom!" if char.empty? }
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
class Foo
property bar : String
def foo
bar
.chars.map do |char|
char.upcase
end
.each { |char| raise "Boom!" if char.empty? }
end
end
CRYSTAL
end
it "reports if there is redundant `self` used in a method arguments' default values" do
source = expect_issue subject, <<-CRYSTAL
class Foo
def foo
42
end
def foo!(bar = self.foo, baz = false)
# ^^^^ error: Redundant `self` detected
end
end
CRYSTAL
expect_correction source, <<-CRYSTAL
class Foo
def foo
42
end
def foo!(bar = foo, baz = false)
end
end
CRYSTAL
end
it "reports if there is redundant `self` used in a string interpolation" do
source = expect_issue subject, <<-'CRYSTAL'
class Foo
def foo; end
def foo!
"#{self.foo || 42}"
# ^^^^ error: Redundant `self` detected
end
end
CRYSTAL
expect_correction source, <<-'CRYSTAL'
class Foo
def foo; end
def foo!
"#{foo || 42}"
end
end
CRYSTAL
end
{% for keyword in %w[class module].map(&.id) %}
it "reports if there is redundant `self` within {{ keyword }} definition" do
source = expect_issue subject, <<-CRYSTAL
{{ keyword }} Foo
def self.foo; end
self.foo
# ^^^^ error: Redundant `self` detected
end
CRYSTAL
expect_correction source, <<-CRYSTAL
{{ keyword }} Foo
def self.foo; end
foo
end
CRYSTAL
end
{% end %}
end
end
================================================
FILE: spec/ameba/rule/style/unless_else_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe UnlessElse do
subject = UnlessElse.new
it "passes if unless hasn't else" do
expect_no_issues subject, <<-CRYSTAL
unless something
:ok
end
CRYSTAL
end
it "fails if unless has else" do
source = expect_issue subject, <<-CRYSTAL
unless something
# ^^^^^^^^^^^^^^ error: Favour `if` over `unless` with `else`
:one
else
:two
end
CRYSTAL
expect_correction source, <<-CRYSTAL
if something
:two
else
:one
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/verbose_block_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe VerboseBlock do
subject = VerboseBlock.new
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
(1..3).any?(&.odd?)
(1..3).join('.', &.to_s)
(1..3).each_with_index { |i, idx| i * idx }
(1..3).map { |i| typeof(i) }
(1..3).map { |i| i || 0 }
(1..3).map { |i| :foo }
(1..3).map { |i| :foo.to_s.split.join('.') }
(1..3).map { :foo }
CRYSTAL
end
it "passes if the block argument is used within the body" do
expect_no_issues subject, <<-CRYSTAL
(1..3).map { |i| i * i }
(1..3).map { |j| j * j.to_i64 }
(1..3).map { |k| k.to_i64 * k }
(1..3).map { |l| l.to_i64 * l.to_i64 }
(1..3).map { |m| m.to_s[start: m.to_i64, count: 3]? }
(1..3).map { |n| n.to_s.split.map { |z| n.to_i * z.to_i }.join }
(1..3).map { |o| o.foo = foos[o.abs]? || 0 }
CRYSTAL
end
it "reports if there is a call with a collapsible block" do
source = expect_issue subject, <<-CRYSTAL
(1..3).any? { |i| i.odd? }
# ^^^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `any?(&.odd?)`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).any?(&.odd?)
CRYSTAL
end
it "reports if there is a call with an argument + collapsible block" do
source = expect_issue subject, <<-CRYSTAL
(1..3).join('.') { |i| i.to_s }
# ^^^^^^^^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `join('.', &.to_s)`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).join('.', &.to_s)
CRYSTAL
end
it "reports if there is a call with a collapsible block (with chained call)" do
source = expect_issue subject, <<-CRYSTAL
(1..3).map { |i| i.to_s.split.reverse.join.strip }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `map(&.to_s.split.reverse.join.strip)`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).map(&.to_s.split.reverse.join.strip)
CRYSTAL
end
context "properties" do
it "#exclude_calls_with_block" do
rule = VerboseBlock.new
rule.exclude_calls_with_block = true
expect_no_issues rule, <<-CRYSTAL
(1..3).in_groups_of(1) { |i| i.map(&.to_s) }
CRYSTAL
rule.exclude_calls_with_block = false
source = expect_issue rule, <<-CRYSTAL
(1..3).in_groups_of(1) { |i| i.map(&.to_s) }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `in_groups_of(1, &.map(&.to_s))`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).in_groups_of(1, &.map(&.to_s))
CRYSTAL
end
it "#exclude_multiple_line_blocks" do
rule = VerboseBlock.new
rule.exclude_multiple_line_blocks = true
expect_no_issues rule, <<-CRYSTAL
(1..3).any? do |i|
i.odd?
end
CRYSTAL
rule.exclude_multiple_line_blocks = false
source = expect_issue rule, <<-CRYSTAL
(1..3).any? do |i|
# ^^^^^^^^^^^ error: Use short block notation instead: `any?(&.odd?)`
i.odd?
end
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).any?(&.odd?)
CRYSTAL
end
it "#exclude_prefix_operators" do
rule = VerboseBlock.new
rule.exclude_prefix_operators = true
expect_no_issues rule, <<-CRYSTAL
(1..3).sum { |i| +i }
(1..3).sum { |i| -i }
(1..3).sum { |i| ~i }
CRYSTAL
rule.exclude_prefix_operators = false
rule.exclude_operators = false
source = expect_issue rule, <<-CRYSTAL
(1..3).sum { |i| +i }
# ^^^^^^^^^^^^^^ error: Use short block notation instead: `sum(&.+)`
(1..3).sum { |i| -i }
# ^^^^^^^^^^^^^^ error: Use short block notation instead: `sum(&.-)`
(1..3).sum { |i| ~i }
# ^^^^^^^^^^^^^^ error: Use short block notation instead: `sum(&.~)`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).sum(&.+)
(1..3).sum(&.-)
(1..3).sum(&.~)
CRYSTAL
end
it "#exclude_operators" do
rule = VerboseBlock.new
rule.exclude_operators = true
expect_no_issues rule, <<-CRYSTAL
(1..3).sum { |i| i * 2 }
CRYSTAL
rule.exclude_operators = false
source = expect_issue rule, <<-CRYSTAL
(1..3).sum { |i| i * 2 }
# ^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `sum(&.*(2))`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).sum(&.*(2))
CRYSTAL
end
it "#exclude_setters" do
rule = VerboseBlock.new
rule.exclude_setters = true
expect_no_issues rule, <<-CRYSTAL
Char::Reader.new("abc").tap { |reader| reader.pos = 0 }
CRYSTAL
rule.exclude_setters = false
source = expect_issue rule, <<-CRYSTAL
Char::Reader.new("abc").tap { |reader| reader.pos = 0 }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `tap(&.pos=(0))`
CRYSTAL
expect_correction source, <<-CRYSTAL
Char::Reader.new("abc").tap(&.pos=(0))
CRYSTAL
end
it "#max_line_length" do
rule = VerboseBlock.new
rule.exclude_multiple_line_blocks = false
rule.max_line_length = 60
expect_no_issues rule, <<-CRYSTAL
(1..3).tap &.tap &.tap &.tap &.tap &.tap &.tap do |i|
i.to_s.reverse.strip.blank?
end
CRYSTAL
rule.max_line_length = nil
source = expect_issue rule, <<-CRYSTAL
(1..3).tap &.tap &.tap &.tap &.tap &.tap &.tap do |i|
# ^^^^^^^^^^ error: Use short block notation instead: `tap(&.to_s.reverse.strip.blank?)`
i.to_s.reverse.strip.blank?
end
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).tap &.tap &.tap &.tap &.tap &.tap &.tap(&.to_s.reverse.strip.blank?)
CRYSTAL
end
it "#max_length" do
rule = VerboseBlock.new
rule.max_length = 30
expect_no_issues rule, <<-CRYSTAL
(1..3).tap { |i| i.to_s.split.reverse.join.strip.blank? }
CRYSTAL
rule.max_length = nil
source = expect_issue rule, <<-CRYSTAL
(1..3).tap { |i| i.to_s.split.reverse.join.strip.blank? }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `tap(&.to_s.split.reverse.join.strip.blank?)`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).tap(&.to_s.split.reverse.join.strip.blank?)
CRYSTAL
end
end
context "macro" do
it "reports in macro scope" do
source = expect_issue subject, <<-CRYSTAL
{{ (1..3).any? { |i| i.odd? } }}
# ^^^^^^^^^^^^^^^^^^^ error: Use short block notation instead: `any?(&.odd?)`
CRYSTAL
expect_correction source, <<-CRYSTAL
{{ (1..3).any?(&.odd?) }}
CRYSTAL
end
end
it "reports call args and named_args" do
rule = VerboseBlock.new
rule.exclude_operators = false
source = expect_issue rule, <<-CRYSTAL
(1..3).map { |i| i.to_s[start: 0.to_i64, count: 3]? }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.[start: 0.to_i64, count: 3]?)`
(1..3).map { |i| i.to_s[0.to_i64, count: 3]? }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.[0.to_i64, count: 3]?)`
(1..3).map { |i| i.to_s[0.to_i64, 3]? }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.[0.to_i64, 3]?)`
(1..3).map { |i| i.to_s[start: 0.to_i64, count: 3] = "foo" }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.[start: 0.to_i64, count: 3]=("foo"))`
(1..3).map { |i| i.to_s[0.to_i64, count: 3] = "foo" }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.[0.to_i64, count: 3]=("foo"))`
(1..3).map { |i| i.to_s[0.to_i64, 3] = "foo" }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.[0.to_i64, 3]=("foo"))`
(1..3).map { |i| i.to_s.camelcase(lower: true) }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.camelcase(lower: true))`
(1..3).map { |i| i.to_s.camelcase }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.camelcase)`
(1..3).map { |i| i.to_s.gsub('_', '-') }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.to_s.gsub('_', '-'))`
(1..3).map { |i| i.in?(*{1, 2, 3}, **{foo: :bar}) }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.in?(*{1, 2, 3}, **{foo: :bar}))`
(1..3).map { |i| i.in?(1, *foo, 3, **bar) }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `map(&.in?(1, *foo, 3, **bar))`
(1..3).join(separator: '.') { |i| i.to_s }
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: [...] `join(separator: '.', &.to_s)`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).map(&.to_s.[start: 0.to_i64, count: 3]?)
(1..3).map(&.to_s.[0.to_i64, count: 3]?)
(1..3).map(&.to_s.[0.to_i64, 3]?)
(1..3).map(&.to_s.[start: 0.to_i64, count: 3]=("foo"))
(1..3).map(&.to_s.[0.to_i64, count: 3]=("foo"))
(1..3).map(&.to_s.[0.to_i64, 3]=("foo"))
(1..3).map(&.to_s.camelcase(lower: true))
(1..3).map(&.to_s.camelcase)
(1..3).map(&.to_s.gsub('_', '-'))
(1..3).map(&.in?(*{1, 2, 3}, **{foo: :bar}))
(1..3).map(&.in?(1, *foo, 3, **bar))
(1..3).join(separator: '.', &.to_s)
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/style/verbose_nil_type_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe VerboseNilType do
subject = VerboseNilType.new
it "passes if there are no issues" do
expect_no_issues subject, <<-CRYSTAL
foo : String | Number | NilableType? = nil
bar : NilableType | String | Number? = nil
baz : String | NilableType | Number
bat : String?
bun : Nil
CRYSTAL
end
it "passes if the union includes metaclass (Foo.class)" do
expect_no_issues subject, <<-CRYSTAL
foo : Foo.class | Nil = nil
CRYSTAL
end
it "reports if there is a verbose nil type (simple)" do
source = expect_issue subject, <<-CRYSTAL
foo : String | Nil = nil
# ^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
bar : Nil | String
# ^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
baz : String | Nil | Int
# ^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
def bat : Symbol | Nil | String
# ^^^^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
end
alias Foo = Nil | Symbol
# ^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
CRYSTAL
expect_correction source, <<-CRYSTAL
foo : String? = nil
bar : String?
baz : String | Int?
def bat : Symbol | String?
end
alias Foo = Symbol?
CRYSTAL
end
it "reports if there is a verbose nil type (edge-case)" do
source = expect_issue subject, <<-CRYSTAL
foo : String? | Nil
# ^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
bar : String | Nil?
# ^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
baz : Nil | String? | Symbol
# ^^^^^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
CRYSTAL
expect_correction source, <<-CRYSTAL
foo : String?
bar : String?
baz : String | Symbol?
CRYSTAL
end
it "reports if there is a verbose nil type (generics + nested unions)" do
source = expect_issue subject, <<-CRYSTAL
foo : (Array(String | Nil) | Nil) | Foo
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
# ^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
CRYSTAL
expect_correction source, <<-CRYSTAL
foo : (Array(String?) | Nil) | Foo
CRYSTAL
end
it "reports if there is a verbose nil type (generics)" do
source = expect_issue subject, <<-CRYSTAL
foo : Array(String | Nil) | Foo
# ^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
CRYSTAL
expect_correction source, <<-CRYSTAL
foo : Array(String?) | Foo
CRYSTAL
end
it "corrects the parenthesized single type unions" do
source = expect_issue subject, <<-CRYSTAL
foo : (Nil | (Symbol | Nil)) | Foo
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
# ^^^^^^^^^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
# ^^^^^^^^^^^^^^ error: Prefer `?` instead of `| Nil` in unions
CRYSTAL
expect_correction source, <<-CRYSTAL
foo : Symbol | Foo?
CRYSTAL
end
context "properties" do
it "#explicit_nil" do
rule = VerboseNilType.new
rule.explicit_nil = true
expect_no_issues rule, <<-CRYSTAL
foo : String | Number | Nil = nil
bar : String | Nil
CRYSTAL
source = expect_issue rule, <<-CRYSTAL
foo : String? = nil
# ^^^^^^^ error: Prefer `| Nil` instead of `?` in unions
bar : String?
# ^^^^^^^ error: Prefer `| Nil` instead of `?` in unions
CRYSTAL
expect_correction source, <<-CRYSTAL
foo : String | Nil = nil
bar : String | Nil
CRYSTAL
end
end
end
end
================================================
FILE: spec/ameba/rule/style/while_true_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Style
describe WhileTrue do
subject = WhileTrue.new
it "passes if there is no `while true`" do
expect_no_issues subject, <<-CRYSTAL
a = 1
loop do
a += 1
break if a > 5
end
CRYSTAL
end
it "fails if there is `while true`" do
source = expect_issue subject, <<-CRYSTAL
a = 1
while true
# ^^^^^^^^ error: While statement using `true` literal as condition
a += 1
break if a > 5
end
CRYSTAL
expect_correction source, <<-CRYSTAL
a = 1
loop do
a += 1
break if a > 5
end
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/rule/typing/macro_call_argument_type_restriction_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Typing
describe MacroCallArgumentTypeRestriction do
subject = MacroCallArgumentTypeRestriction.new
it "passes if macro call args have type restrictions" do
expect_no_issues subject, <<-CRYSTAL
class Foo
getter foo : Int32?
setter bar : Array(Int32)?
property baz : Bool?
end
record Task,
cmd : String,
args : Array(String)
CRYSTAL
end
it "passes if macro call args have default values" do
expect_no_issues subject, <<-CRYSTAL
class Foo
getter foo = 0
setter bar = [] of Int32
property baz = true
end
record Task,
cmd = "",
args = %w[]
CRYSTAL
end
it "fails if a macro call arg doesn't have a type restriction" do
expect_issue subject, <<-CRYSTAL
class Foo
getter foo
# ^^^ error: Argument should have a type restriction
getter :bar
# ^^^^ error: Argument should have a type restriction
getter "baz"
# ^^^^^ error: Argument should have a type restriction
end
CRYSTAL
end
context "properties" do
context "#default_value" do
rule = MacroCallArgumentTypeRestriction.new
rule.default_value = true
it "fails if a macro call arg with a default value doesn't have a type restriction" do
expect_issue rule, <<-CRYSTAL
class Foo
getter foo = "bar"
# ^^^ error: Argument should have a type restriction
end
CRYSTAL
end
it "fails if a record call arg with default value doesn't have a type restriction" do
expect_issue rule, <<-CRYSTAL
record Task,
cmd : String,
args = %[]
# ^^^^ error: Argument should have a type restriction
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/typing/method_parameter_type_restriction_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Typing
describe MethodParameterTypeRestriction do
subject = MethodParameterTypeRestriction.new
it "passes if a method parameter has a type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar : String, baz : _) : String
end
CRYSTAL
end
it "passes if a splat method parameter has a type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(*bar : String) : String
end
CRYSTAL
end
it "fails if a splat method parameter with a name doesn't have a type restriction" do
expect_issue subject, <<-CRYSTAL
def foo(*bar) : String
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
it "passes if a splat parameter without a name doesn't have a type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar : String, *, baz : String = "bat") : String
end
CRYSTAL
end
it "passes if a double splat method parameter doesn't have a type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar : String, **opts) : String
end
CRYSTAL
end
it "passes if a private method parameter doesn't have a type restriction" do
expect_no_issues subject, <<-CRYSTAL
private def foo(bar)
end
CRYSTAL
end
it "passes if a protected method parameter doesn't have a type restriction" do
expect_no_issues subject, <<-CRYSTAL
protected def foo(bar)
end
CRYSTAL
end
it "passes if a method has a `:nodoc:` annotation" do
expect_no_issues subject, <<-CRYSTAL
# :nodoc:
def foo(bar); end
CRYSTAL
end
it "fails if a public method parameter doesn't have a type restriction" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
it "fails if a public method external parameter doesn't have a type restriction" do
expect_issue subject, <<-CRYSTAL
def foo(bar, ext baz)
# ^^^ error: Method parameter should have a type restriction
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
it "passes if a method parameter with a default value doesn't have a type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar = "baz")
end
CRYSTAL
end
it "passes if a block parameter doesn't have a type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo(&)
end
CRYSTAL
end
context "properties" do
context "#private_methods" do
rule = MethodParameterTypeRestriction.new
rule.private_methods = true
it "passes if a method has a parameter type restriction" do
expect_no_issues rule, <<-CRYSTAL
private def foo(bar : String) : String
end
CRYSTAL
end
it "passes if a protected method parameter doesn't have a type restriction" do
expect_no_issues rule, <<-CRYSTAL
protected def foo(bar)
end
CRYSTAL
end
it "fails if a public method doesn't have a parameter type restriction" do
expect_issue rule, <<-CRYSTAL
def foo(bar)
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
it "fails if a private method doesn't have a parameter type restriction" do
expect_issue rule, <<-CRYSTAL
private def foo(bar)
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
end
context "#protected_methods" do
rule = MethodParameterTypeRestriction.new
rule.protected_methods = true
it "passes if a method has a parameter type restriction" do
expect_no_issues rule, <<-CRYSTAL
protected def foo(bar : String) : String
end
CRYSTAL
end
it "passes if a private method parameter doesn't have a type restriction" do
expect_no_issues rule, <<-CRYSTAL
private def foo(bar)
end
CRYSTAL
end
it "fails if a public method doesn't have a parameter type restriction" do
expect_issue rule, <<-CRYSTAL
def foo(bar)
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
it "fails if a protected method doesn't have a parameter type restriction" do
expect_issue rule, <<-CRYSTAL
protected def foo(bar)
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
end
context "#default_value" do
it "fails if a method parameter with a default value doesn't have a type restriction" do
rule = MethodParameterTypeRestriction.new
rule.default_value = true
expect_issue rule, <<-CRYSTAL
def foo(bar = "baz")
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
end
context "#block_parameters" do
rule = MethodParameterTypeRestriction.new
rule.block_parameters = true
it "fails if a block parameter without a name doesn't have a type restriction" do
expect_issue rule, <<-CRYSTAL
def foo(&)
# ^ error: Method parameter should have a type restriction
end
CRYSTAL
end
it "fails if a block parameter with a name doesn't have a type restriction" do
expect_issue rule, <<-CRYSTAL
def foo(&block)
# ^^^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
end
context "#nodoc_methods" do
rule = MethodParameterTypeRestriction.new
rule.nodoc_methods = true
it "fails if a public method parameter doesn't have a type restriction" do
expect_issue rule, <<-CRYSTAL
# :nodoc:
def foo(bar)
# ^^^ error: Method parameter should have a type restriction
end
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/typing/method_return_type_restriction_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Typing
describe MethodReturnTypeRestriction do
subject = MethodReturnTypeRestriction.new
it "passes if a public method has a return type restriction" do
expect_no_issues subject, <<-CRYSTAL
def foo : String
end
CRYSTAL
end
it "passes if a private method has a return type restriction" do
expect_no_issues subject, <<-CRYSTAL
private def foo : String
end
CRYSTAL
end
it "passes if a protected method has a return type restriction" do
expect_no_issues subject, <<-CRYSTAL
protected def foo : String
end
CRYSTAL
end
it "passes if a private method doesn't have a return type restriction" do
expect_no_issues subject, <<-CRYSTAL
private def foo
end
CRYSTAL
end
it "passes if a protected method doesn't have a return type restriction" do
expect_no_issues subject, <<-CRYSTAL
protected def foo
end
CRYSTAL
end
it "passes if a public method has a `:nodoc:` annotation" do
expect_no_issues subject, <<-CRYSTAL
# :nodoc:
def foo; end
CRYSTAL
end
it "fails if a public method doesn't have a return type restriction" do
expect_issue subject, <<-CRYSTAL
def foo
# ^^^^^ error: Method should have a return type restriction
end
CRYSTAL
end
context "properties" do
context "#private_methods" do
rule = MethodReturnTypeRestriction.new
rule.private_methods = true
it "passes if a public method has a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
def foo : String
end
CRYSTAL
end
it "passes if a private method has a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
private def foo : String
end
CRYSTAL
end
it "passes if a protected method has a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
protected def foo : String
end
CRYSTAL
end
it "passes if a protected method doesn't have a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
protected def foo
end
CRYSTAL
end
it "fails if a public method doesn't have a return type restriction" do
expect_issue rule, <<-CRYSTAL
def foo
# ^^^^^ error: Method should have a return type restriction
end
CRYSTAL
end
it "fails if a private method doesn't have a return type restriction" do
expect_issue rule, <<-CRYSTAL
private def foo
# ^^^^^^^ error: Method should have a return type restriction
end
CRYSTAL
end
end
context "#protected_methods" do
rule = MethodReturnTypeRestriction.new
rule.protected_methods = true
it "passes if a public method has a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
def foo : String
end
CRYSTAL
end
it "passes if a private method doesn't have a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
private def foo
end
CRYSTAL
end
it "passes if a protected method has a return type restriction" do
expect_no_issues rule, <<-CRYSTAL
protected def foo : String
end
CRYSTAL
end
it "fails if a public method doesn't have a return type restriction" do
expect_issue rule, <<-CRYSTAL
def foo
# ^^^^^ error: Method should have a return type restriction
end
CRYSTAL
end
it "fails if a protected method doesn't have a return type restriction" do
expect_issue rule, <<-CRYSTAL
protected def foo
# ^^^^^^^ error: Method should have a return type restriction
end
CRYSTAL
end
end
context "#nodoc_methods" do
rule = MethodReturnTypeRestriction.new
rule.nodoc_methods = true
it "fails if a public method doesn't have a return type restriction" do
expect_issue rule, <<-CRYSTAL
# :nodoc:
def foo
# ^^^^^ error: Method should have a return type restriction
end
CRYSTAL
end
end
end
end
end
================================================
FILE: spec/ameba/rule/typing/proc_literal_return_type_restriction_spec.cr
================================================
require "../../../spec_helper"
module Ameba::Rule::Typing
describe ProcLiteralReturnTypeRestriction do
subject = ProcLiteralReturnTypeRestriction.new
it "passes if a proc literal has a return type restriction" do
expect_no_issues subject, <<-CRYSTAL
foo = -> (bar : String) : Nil { }
CRYSTAL
end
it "fails if a proc literal doesn't have a return type restriction" do
expect_issue subject, <<-CRYSTAL
foo = -> (bar : String) { }
# ^^^^^^^^^^^^^^^^^^^^^ error: Proc literal should have a return type restriction
CRYSTAL
end
end
end
================================================
FILE: spec/ameba/runner_spec.cr
================================================
require "../spec_helper"
module Ameba
private def runner(files = [__FILE__], formatter = DummyFormatter.new)
config = Config.load
config.formatter = formatter
config.globs = files.to_set
config.update_rule VersionedRule.rule_name, enabled: false
config.update_rule ErrorRule.rule_name, enabled: false
config.update_rule PerfRule.rule_name, enabled: false
config.update_rule AtoAA.rule_name, enabled: false
config.update_rule AtoB.rule_name, enabled: false
config.update_rule BtoA.rule_name, enabled: false
config.update_rule BtoC.rule_name, enabled: false
config.update_rule CtoA.rule_name, enabled: false
config.update_rule ClassToModule.rule_name, enabled: false
config.update_rule ModuleToClass.rule_name, enabled: false
Runner.new(config)
end
describe Runner do
formatter = DummyFormatter.new
default_severity = Severity::Convention
describe "#run" do
it "returns self" do
runner.run.should be_a(Runner)
end
context "invokes hooks" do
before_each do
runner(formatter: formatter).run
end
it "calls started callback" do
formatter.started_sources.should_not be_nil
end
it "calls finished callback" do
formatter.finished_sources.should_not be_nil
end
it "calls source_started callback" do
formatter.started_source.should_not be_nil
end
it "calls source_finished callback" do
formatter.finished_source.should_not be_nil
end
end
it "checks accordingly to the rule #since_version" do
rules = [VersionedRule.new] of Rule::Base
source = Source.new path: "source.cr"
v1_0_0 = SemanticVersion.parse("1.0.0")
Runner.new(rules, [source], formatter, default_severity, false, v1_0_0).run.success?.should be_true
v1_5_0 = SemanticVersion.parse("1.5.0")
Runner.new(rules, [source], formatter, default_severity, false, v1_5_0).run.success?.should be_false
v1_10_0 = SemanticVersion.parse("1.10.0")
Runner.new(rules, [source], formatter, default_severity, false, v1_10_0).run.success?.should be_false
end
it "skips rules based on severity" do
rules = [ErrorRule.new] of Rule::Base
Source.new.tap do |source|
Runner.new(rules, [source], formatter, :convention).run
source.issues.size.should eq(1)
end
Source.new.tap do |source|
Runner.new(rules, [source], formatter, :warning).run
source.issues.should be_empty
end
end
it "skips rule check if source is excluded" do
path = "source.cr"
source = Source.new(path: path)
all_rules = ([] of Rule::Base).tap do |rules|
rule = ErrorRule.new
rule.excluded = Set{path}
rules << rule
end
Runner.new(all_rules, [source], formatter, default_severity).run.success?.should be_true
end
it "aborts because of an infinite loop" do
rules = [AtoAA.new] of Rule::Base
source = Source.new "class A; end", "source.cr"
message = "Infinite loop in source.cr caused by Ameba/AtoAA"
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
end
end
context "exception in rule" do
it "raises an exception raised in fiber while running a rule" do
rule = RaiseRule.new
rule.should_raise = true
rules = [rule] of Rule::Base
source = Source.new path: "source.cr"
expect_raises(Exception, "something went wrong") do
Runner.new(rules, [source], formatter, default_severity).run
end
end
end
context "issues sorting" do
it "sorts issues by severity, line number, and column number" do
rules = [
ErrorRule.from_yaml("{ Severity: convention, Message: foo }"),
ErrorRule.from_yaml("{ Severity: convention, Message: bar, LineNumber: 2 }"),
ErrorRule.from_yaml("{ Severity: warning, Message: baz, LineNumber: 2 }"),
ErrorRule.from_yaml("{ Severity: error, Message: bat, LineNumber: 2 }"),
ErrorRule.from_yaml("{ Severity: warning, Message: baq }"),
] of Rule::Base
source = Source.new "foo\nbar"
Runner.new(rules, [source], formatter, default_severity).run
source.should_not be_valid
source.issues.map(&.message).should eq %w[foo baq bar baz bat]
end
end
context "invalid syntax" do
it "reports a syntax error" do
rules = [Rule::Lint::Syntax.new] of Rule::Base
source = Source.new "def bad_syntax"
Runner.new(rules, [source], formatter, default_severity).run
source.should_not be_valid
source.issues.first.rule.should be_a Rule::Lint::Syntax
end
it "does not run other rules" do
rules = [Rule::Lint::Syntax.new, Rule::Naming::ConstantNames.new]
source = Source.new <<-CRYSTAL
MyBadConstant = 1
when my_bad_syntax
CRYSTAL
Runner.new(rules, [source], formatter, default_severity).run
source.should_not be_valid
source.issues.size.should eq 1
end
end
context "unneeded disables" do
it "reports an issue if such disable exists" do
rules = [
Rule::Lint::UnneededDisableDirective.new,
Rule::Lint::NotNil.new,
] of Rule::Base
source = Source.new <<-CRYSTAL
a = 1 # ameba:disable Lint/NotNil
CRYSTAL
Runner.new(rules, [source], formatter, default_severity).run
source.should_not be_valid
source.issues.first.rule.should be_a Rule::Lint::UnneededDisableDirective
end
it "does not report if the disabled rule is excluded for the source" do
path = "source.cr"
error_rule = ErrorRule.new
error_rule.excluded = Set{path}
rules = [error_rule, Rule::Lint::UnneededDisableDirective.new] of Rule::Base
source = Source.new <<-CRYSTAL, path
a = 1 # ameba:disable #{ErrorRule.rule_name}
CRYSTAL
Runner.new(rules, [source], formatter, default_severity).run
source.should be_valid
end
it "respects Excluded config of UnneededDisableDirective" do
path = "source.cr"
udd_rule = Rule::Lint::UnneededDisableDirective.new
udd_rule.excluded = Set{path}
rules = [udd_rule] of Rule::Base
source = Source.new <<-CRYSTAL, path
a = 1 # ameba:disable LineLength
CRYSTAL
Runner.new(rules, [source], formatter, default_severity).run
source.should be_valid
end
end
pending "handles rules with incompatible autocorrect" do
rules = [Rule::Performance::MinMaxAfterMap.new, Rule::Style::VerboseBlock.new]
source = Source.new "list.map { |i| i.size }.max", File.tempname("source", ".cr")
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
source.code.should eq "list.max_of(&.size)"
end
end
describe "#explain" do
output = IO::Memory.new
before_each do
output.clear
end
it "writes nothing if sources are valid" do
runner = runner(formatter: formatter).run
runner.explain(Crystal::Location.new("source.cr", 1, 2), output)
output.to_s.should be_empty
end
it "writes the explanation if sources are not valid and location found" do
rules = [ErrorRule.new] of Rule::Base
source = Source.new "a = 1", "source.cr"
runner = Runner.new(rules, [source], formatter, default_severity).run
runner.explain(Crystal::Location.new("source.cr", 1, 1), output)
output.to_s.should_not be_empty
end
it "writes nothing if sources are not valid and location is not found" do
rules = [ErrorRule.new] of Rule::Base
source = Source.new "a = 1", "source.cr"
runner = Runner.new(rules, [source], formatter, default_severity).run
runner.explain(Crystal::Location.new("source.cr", 1, 2), output)
output.to_s.should be_empty
end
end
describe "#success?" do
it "returns true if runner has not been run" do
runner.success?.should be_true
end
it "returns true if all sources are valid" do
runner.run.success?.should be_true
end
it "returns false if there are invalid sources" do
rules = Rule.rules.map &.new.as(Rule::Base)
source = Source.new "WrongConstant = 5"
Runner.new(rules, [source], formatter, default_severity).run.success?.should be_false
end
it "depends on the level of severity" do
rules = Rule.rules.map &.new.as(Rule::Base)
source = Source.new "WrongConstant = 5\n"
Runner.new(rules, [source], formatter, :error).run.success?.should be_true
Runner.new(rules, [source], formatter, :warning).run.success?.should be_true
Runner.new(rules, [source], formatter, :convention).run.success?.should be_false
end
it "returns false if issue is disabled" do
rules = [NamedRule.new] of Rule::Base
source = Source.new <<-CRYSTAL
def foo
bar = 1 # ameba:disable #{NamedRule.name}
end
CRYSTAL
source.add_issue NamedRule.new, location: {2, 1},
message: "Useless assignment"
Runner
.new(rules, [source], formatter, default_severity)
.run.success?.should be_true
end
end
describe "#run with rules autocorrecting each other" do
context "with two conflicting rules" do
context "if there is an offense in an inspected file" do
it "aborts because of an infinite loop" do
rules = [AtoB.new, BtoA.new]
source = Source.new "class A; end", "source.cr"
message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoA"
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
end
end
end
context "if there are multiple offenses in an inspected file" do
it "aborts because of an infinite loop" do
rules = [AtoB.new, BtoA.new]
source = Source.new <<-CRYSTAL, "source.cr"
class A; end
class A_A; end
CRYSTAL
message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoA"
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
end
end
end
end
context "with two pairs of conflicting rules" do
it "aborts because of an infinite loop" do
rules = [ClassToModule.new, ModuleToClass.new, AtoB.new, BtoA.new]
source = Source.new "class A_A; end", "source.cr"
message = "Infinite loop in source.cr caused by Ameba/ClassToModule, Ameba/AtoB -> Ameba/ModuleToClass, Ameba/BtoA"
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
end
end
end
context "with three rule cycle" do
it "aborts because of an infinite loop" do
rules = [AtoB.new, BtoC.new, CtoA.new]
source = Source.new "class A; end", "source.cr"
message = "Infinite loop in source.cr caused by Ameba/AtoB -> Ameba/BtoC -> Ameba/CtoA"
expect_raises(Runner::InfiniteCorrectionLoopError, message) do
Runner.new(rules, [source], formatter, default_severity, autocorrect: true).run
end
end
end
end
end
end
================================================
FILE: spec/ameba/severity_spec.cr
================================================
require "../spec_helper"
module Ameba
describe Severity do
describe "#symbol" do
it "returns the symbol for each severity in the enum" do
Severity.values.each(&.symbol.should_not(be_nil))
end
it "returns symbol for Error" do
Severity::Error.symbol.should eq 'E'
end
it "returns symbol for Warning" do
Severity::Warning.symbol.should eq 'W'
end
it "returns symbol for Convention" do
Severity::Convention.symbol.should eq 'C'
end
end
describe ".parse" do
it "creates error severity by name" do
Severity.parse("Error").should eq Severity::Error
end
it "creates warning severity by name" do
Severity.parse("Warning").should eq Severity::Warning
end
it "creates convention severity by name" do
Severity.parse("Convention").should eq Severity::Convention
end
it "raises when name is incorrect" do
expect_raises(Exception, "Incorrect severity name BadName. Try one of: Error, Warning, Convention") do
Severity.parse("BadName")
end
end
end
end
struct SeverityConvertible
include YAML::Serializable
@[YAML::Field(converter: Ameba::SeverityYamlConverter)]
property severity : Severity?
end
describe SeverityYamlConverter do
describe ".from_yaml" do
it "converts from yaml to Severity::Error" do
yaml = {severity: "error"}.to_yaml
converted = SeverityConvertible.from_yaml(yaml)
converted.severity.should eq Severity::Error
end
it "converts from yaml to Severity::Warning" do
yaml = {severity: "warning"}.to_yaml
converted = SeverityConvertible.from_yaml(yaml)
converted.severity.should eq Severity::Warning
end
it "converts from yaml to Severity::Convention" do
yaml = {severity: "convention"}.to_yaml
converted = SeverityConvertible.from_yaml(yaml)
converted.severity.should eq Severity::Convention
end
it "raises if severity is not a scalar" do
yaml = {severity: {convention: true}}.to_yaml
expect_raises(Exception, "Severity must be a scalar") do
SeverityConvertible.from_yaml(yaml)
end
end
it "raises if severity has a wrong type" do
yaml = {severity: [1, 2, 3]}.to_yaml
expect_raises(Exception, "Severity must be a scalar") do
SeverityConvertible.from_yaml(yaml)
end
end
end
describe ".to_yaml" do
it "converts Severity::Error to yaml" do
yaml = {severity: "error"}.to_yaml
converted = SeverityConvertible.from_yaml(yaml).to_yaml
converted.should eq "---\nseverity: Error\n"
end
it "converts Severity::Warning to yaml" do
yaml = {severity: "warning"}.to_yaml
converted = SeverityConvertible.from_yaml(yaml).to_yaml
converted.should eq "---\nseverity: Warning\n"
end
it "converts Severity::Convention to yaml" do
yaml = {severity: "convention"}.to_yaml
converted = SeverityConvertible.from_yaml(yaml).to_yaml
converted.should eq "---\nseverity: Convention\n"
end
end
end
end
================================================
FILE: spec/ameba/source/rewriter_spec.cr
================================================
require "../../spec_helper"
class Ameba::Source
describe Rewriter do
code = "puts(:hello, :world)"
hello = {5, 11}
comma_space = {11, 13}
world = {13, 19}
it "can remove" do
rewriter = Rewriter.new(code)
rewriter.remove(*hello)
rewriter.process.should eq "puts(, :world)"
end
it "can insert before" do
rewriter = Rewriter.new(code)
rewriter.insert_before(*world, "42, ")
rewriter.process.should eq "puts(:hello, 42, :world)"
end
it "can insert after" do
rewriter = Rewriter.new(code)
rewriter.insert_after(*hello, ", 42")
rewriter.process.should eq "puts(:hello, 42, :world)"
end
it "can wrap" do
rewriter = Rewriter.new(code)
rewriter.wrap(*hello, '[', ']')
rewriter.process.should eq "puts([:hello], :world)"
end
it "can replace" do
rewriter = Rewriter.new(code)
rewriter.replace(*hello, ":hi")
rewriter.process.should eq "puts(:hi, :world)"
end
it "accepts crossing deletions" do
rewriter = Rewriter.new(code)
rewriter.remove(hello[0], comma_space[1])
rewriter.remove(comma_space[0], world[1])
rewriter.process.should eq "puts()"
end
it "accepts multiple actions" do
rewriter = Rewriter.new(code)
rewriter.replace(*comma_space, " => ")
rewriter.wrap(hello[0], world[1], '{', '}')
rewriter.replace(*world, ":everybody")
rewriter.wrap(*world, '[', ']')
rewriter.process.should eq "puts({:hello => [:everybody]})"
end
it "can wrap the same range" do
rewriter = Rewriter.new(code)
rewriter.wrap(*hello, '(', ')')
rewriter.wrap(*hello, '[', ']')
rewriter.process.should eq "puts([(:hello)], :world)"
end
it "can insert on empty ranges" do
rewriter = Rewriter.new(code)
rewriter.insert_before(hello[0], '{')
rewriter.replace(hello[0], hello[0], 'x')
rewriter.insert_after(hello[0], '}')
rewriter.insert_before(hello[1], '[')
rewriter.replace(hello[1], hello[1], 'y')
rewriter.insert_after(hello[1], ']')
rewriter.process.should eq "puts({x}:hello[y], :world)"
end
it "can replace the same range" do
rewriter = Rewriter.new(code)
rewriter.replace(*hello, ":hi")
rewriter.replace(*hello, ":hey")
rewriter.process.should eq "puts(:hey, :world)"
end
it "can swallow insertions" do
rewriter = Rewriter.new(code)
rewriter.wrap(hello[0] + 1, hello[1], "__", "__")
rewriter.replace(world[0], world[1] - 2, "xx")
rewriter.replace(hello[0], world[1], ":hi")
rewriter.process.should eq "puts(:hi)"
end
it "rejects out-of-range ranges" do
rewriter = Rewriter.new(code)
expect_raises(IndexError) { rewriter.insert_before(0, 100, "hola") }
end
it "ignores trivial actions" do
rewriter = Rewriter.new(code)
rewriter.empty?.should be_true
# This is a trivial wrap
rewriter.wrap(2, 5, "", "")
rewriter.empty?.should be_true
# This is a trivial deletion
rewriter.remove(2, 2)
rewriter.empty?.should be_true
rewriter.remove(2, 5)
rewriter.empty?.should be_false
end
end
end
================================================
FILE: spec/ameba/source_spec.cr
================================================
require "../spec_helper"
module Ameba
describe Source do
describe ".new" do
it "allows to create a source by code and path" do
source = Source.new "code", "path"
source.path.should eq "path"
source.code.should eq "code"
source.lines.should eq ["code"]
end
end
describe "#fullpath" do
it "returns a relative path of the source" do
source = Source.new path: "./source_spec.cr"
source.fullpath.should contain "source_spec.cr"
end
it "returns fullpath if path is blank" do
source = Source.new
source.fullpath.should_not be_nil
end
end
describe "#spec?" do
it "returns true if the source is a spec file" do
source = Source.new path: "./source_spec.cr"
source.spec?.should be_true
end
it "returns false if the source is not a spec file" do
source = Source.new path: "./source.cr"
source.spec?.should be_false
end
end
describe "#pos" do
it "works" do
source = Source.new <<-CRYSTAL
foo
bar
fizz
buzz
CRYSTAL
location = Crystal::Location.new("", 2, 1)
end_location = Crystal::Location.new("", 3, 4)
range = Range.new(
source.pos(location),
source.pos(end_location, end: true),
exclusive: true
)
source.code[range].should eq <<-CRYSTAL
bar
fizz
CRYSTAL
end
end
describe "#correct!" do
it "skips disabled issues" do
source = Source.new <<-CRYSTAL
foo.match("bar").not_nil! # ameba:disable Lint/NotNilAfterNoBang
CRYSTAL
rule = Rule::Lint::NotNilAfterNoBang.new
rule.catch(source)
source.issues.size.should eq 1
source.issues.first.disabled?.should be_true
source.correct!.should be_false
source.code.should contain ".not_nil!"
end
it "corrects enabled issues" do
source = Source.new <<-CRYSTAL
foo.match("bar").not_nil!
CRYSTAL
rule = Rule::Lint::NotNilAfterNoBang.new
rule.catch(source)
source.issues.size.should eq 1
source.issues.first.enabled?.should be_true
source.correct!.should be_true
source.code.should contain ".match!"
source.code.should_not contain ".not_nil!"
end
end
describe "#ast" do
it "parses an ECR file" do
source = Source.new <<-ECR, "filename.ecr"
hello <%= "world" %>
ECR
source.ast.to_s.should eq <<-CRYSTAL
__str__ << "hello "
("world")
.to_s(__str__)
CRYSTAL
end
it "raises an exception when ECR parsing fails" do
source = Source.new <<-ECR, "filename.ecr"
hello <%= "world" >
ECR
expect_raises(Crystal::SyntaxException) do
source.ast
end
end
end
end
end
================================================
FILE: spec/ameba/spec/annotated_source_spec.cr
================================================
require "../../spec_helper"
private def dummy_issue(code,
message,
position : {Int32, Int32}?,
end_position : {Int32, Int32}?,
path = "")
location, end_location = nil, nil
location = Crystal::Location.new(path, *position) if position
end_location = Crystal::Location.new(path, *end_position) if end_position
Ameba::Issue.new(
code: code,
rule: Ameba::DummyRule.new,
location: location,
end_location: end_location,
message: message
)
end
private def expect_invalid_location(code,
position,
end_position,
message exception_message,
file = __FILE__,
line = __LINE__)
expect_raises Exception, exception_message, file, line do
Ameba::Spec::AnnotatedSource.new(
lines: code.lines,
issues: [dummy_issue(code, "Message", position, end_position, "path")]
)
end
end
module Ameba::Spec
describe AnnotatedSource do
annotated_text = <<-EOS
line 1
# ^^ error: Message 1
line 2 # error: Message 2
EOS
text_without_annotations = <<-EOS
line 1
line 2
EOS
text_without_source = <<-EOS
# ^ error: Message 1
# ^ error: Message 2
EOS
describe ".parse" do
it "accepts annotated text" do
annotated_source = AnnotatedSource.parse(annotated_text)
annotated_source.lines.should eq ["line 1", "line 2"]
annotated_source.annotations.should eq [
{1, " # ^^ error: ", "Message 1"},
{2, "", "Message 2"},
]
end
it "accepts text containing source only and no annotations" do
annotated_source = AnnotatedSource.parse(text_without_annotations)
annotated_source.lines.should eq ["line 1", "line 2"]
annotated_source.annotations.should be_empty
end
it "accepts text containing annotations only and no source" do
annotated_source = AnnotatedSource.parse(text_without_source)
annotated_source.lines.should be_empty
annotated_source.annotations.should eq [
{1, "# ^ error: ", "Message 1"},
{1, "# ^ error: ", "Message 2"},
]
end
it "accepts RuboCop-style annotations" do
annotated_source = AnnotatedSource.parse <<-EOS
line 1
^^ Message
line 2
EOS
annotated_source.lines.should eq ["line 1", "line 2"]
annotated_source.annotations.should eq [
{1, " ^^ ", "Message"},
]
end
end
describe "#==" do
it "accepts source lines ending with annotations" do
expected = AnnotatedSource.parse <<-EOS
line 1 # error: Message
line 2
EOS
actual = AnnotatedSource.parse <<-EOS
line 1
# ^^ error: Message
line 2
EOS
actual.should eq expected
end
it "accepts annotations that are abbreviated using '[...]'" do
expected = AnnotatedSource.parse <<-EOS
line 1 # error: Message [...]
line 2
# ^^ error: M[...]s[...]g[...] 2
EOS
actual = AnnotatedSource.parse <<-EOS
line 1
# ^^ error: Message 1
line 2
# ^^ error: Message 2
EOS
actual.should eq expected
end
end
describe "#to_s" do
it "accepts annotated text" do
annotated_source = AnnotatedSource.parse(annotated_text)
annotated_source.to_s.should eq annotated_text
end
it "accepts text containing source only and no annotations" do
annotated_source = AnnotatedSource.parse(text_without_annotations)
annotated_source.to_s.should eq text_without_annotations
end
it "accepts text containing annotations only and no source" do
annotated_source = AnnotatedSource.parse(text_without_source)
annotated_source.to_s.should eq text_without_source
end
end
describe ".new(lines, annotations)" do
it "sorts the annotations" do
annotated_source = AnnotatedSource.new [] of String, [
{2, "", "Annotation C"},
{1, "", "Annotation B"},
{1, "", "Annotation A"},
]
annotated_source.annotations.should eq [
{1, "", "Annotation A"},
{1, "", "Annotation B"},
{2, "", "Annotation C"},
]
end
end
describe ".new(lines, issues)" do
it "raises an exception if issue location is nil" do
expect_invalid_location text_without_annotations,
position: nil,
end_position: nil,
message: "Missing location for issue 'Message'"
end
it "raises an exception if issue starts at column 0" do
expect_invalid_location text_without_annotations,
position: {1, 0},
end_position: nil,
message: "Invalid issue location: path:1:0"
end
it "raises an exception if issue starts at line 0" do
expect_invalid_location text_without_annotations,
position: {0, 1},
end_position: nil,
message: "Invalid issue location: path:0:1"
end
it "raises an exception if issue starts at a non-existent line" do
expect_invalid_location text_without_annotations,
position: {3, 1},
end_position: nil,
message: "Invalid issue location: path:3:1"
end
it "raises an exception if issue ends at column 0" do
expect_invalid_location text_without_annotations,
position: {1, 1},
end_position: {2, 0},
message: "Invalid issue end location: path:2:0"
end
it "raises an exception if issue ends at a non-existent line" do
expect_invalid_location text_without_annotations,
position: {1, 1},
end_position: {3, 1},
message: "Invalid issue end location: path:3:1"
end
it "raises an exception if starting column number is greater than ending column number" do
expect_invalid_location text_without_annotations,
position: {1, 2},
end_position: {1, 1},
message: <<-MSG
Invalid issue location
start: path:1:2
end: path:1:1
MSG
end
it "raises an exception if starting line number is greater than ending line number" do
expect_invalid_location text_without_annotations,
position: {2, 1},
end_position: {1, 1},
message: <<-MSG
Invalid issue location
start: path:2:1
end: path:1:1
MSG
end
end
end
end
================================================
FILE: spec/ameba/tokenizer_spec.cr
================================================
require "../spec_helper"
module Ameba
private def it_tokenizes(str, expected, *, file = __FILE__, line = __LINE__)
it "tokenizes #{str}", file, line do
%w[].tap do |token_types|
Tokenizer.new(Source.new str, normalize: false)
.run { |token| token_types << token.type.to_s }
.should be_true
end.should eq(expected), file: file, line: line
end
end
describe Tokenizer do
describe "#run" do
it_tokenizes %("string"), %w[DELIMITER_START STRING DELIMITER_END EOF]
it_tokenizes %(100), %w[NUMBER EOF]
it_tokenizes %('a'), %w[CHAR EOF]
it_tokenizes %([]), %w[[] EOF]
it_tokenizes %([] of String), %w[[] SPACE IDENT SPACE CONST EOF]
it_tokenizes %q("str #{3}"), %w[
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
]
it_tokenizes %(%w[1 2]),
%w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
it_tokenizes %(%i[one two]),
%w[SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
# ameba:disable Style/MultilineStringLiteral
it_tokenizes %(
class A
def method
puts "hello"
end
end
), %w[
NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT
NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END
NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF
]
end
end
end
================================================
FILE: spec/ameba/version_spec.cr
================================================
require "../spec_helper"
private def build_ameba_version(string)
Ameba::Version.new(SemanticVersion.parse(string))
end
module Ameba
describe Version do
context "#to_s" do
it "outputs the `version` string" do
version = build_ameba_version("1.2.3")
version.to_s.should eq version.version.to_s
end
end
context "#version" do
it "matches the version format" do
version = build_ameba_version("1.2.3")
version.version.to_s.should match /^\d+\.\d+\.\d+/
end
end
context "#dev?" do
it "returns `true` if the version pre-release identifiers contain only `dev`" do
version = build_ameba_version("1.2.3-dev")
version.dev?.should be_true
end
it "returns `true` if the version pre-release identifiers contain `dev`" do
version = build_ameba_version("1.2.3-dev.arm64")
version.dev?.should be_true
end
it "returns `false` if the version pre-release identifiers do not contain `dev`" do
version = build_ameba_version("1.2.3")
version.dev?.should be_false
version = build_ameba_version("1.2.3-devo")
version.dev?.should be_false
end
end
context "#release_candidate?" do
it "returns `true` for `rc` pre-release identifier followed by a number" do
version = build_ameba_version("1.2.3-rc-1")
version.release_candidate?.should be_true
version = build_ameba_version("1.2.3-rc1")
version.release_candidate?.should be_true
version = build_ameba_version("1.2.3-RC1")
version.release_candidate?.should be_false
version = build_ameba_version("1.2.3-rc-x")
version.release_candidate?.should be_false
end
it "returns `true` if the version pre-release identifiers contain only `rc`" do
version = build_ameba_version("1.2.3-rc")
version.release_candidate?.should be_true
end
it "returns `true` if the version pre-release identifiers contain `rc`" do
version = build_ameba_version("1.2.3-rc.arm64")
version.release_candidate?.should be_true
end
it "returns `false` if the version pre-release identifiers do not contain `rc`" do
version = build_ameba_version("1.2.3-rcx")
version.release_candidate?.should be_false
end
it "returns `false` if the version pre-release identifiers are empty" do
version = build_ameba_version("1.2.3")
version.release_candidate?.should be_false
end
end
end
end
================================================
FILE: spec/ameba_spec.cr
================================================
require "./spec_helper"
describe Ameba do
context "VERSION" do
it "contains a version-like string" do
Ameba::VERSION.should match /^\d+\.\d+\.\d+/
end
end
context ".version" do
it "returns an `Ameba::Version` object" do
Ameba.version.should be_a Ameba::Version
end
it "starts with `Ameba::VERSION`" do
Ameba.version.to_s.should start_with Ameba::VERSION
end
end
end
================================================
FILE: spec/fixtures/.ameba.yml
================================================
Version: "1.5.0"
Formatter:
Name: flycheck
Lint/ComparisonToBoolean:
Enabled: true
================================================
FILE: spec/spec_helper.cr
================================================
require "spec"
require "../src/ameba"
require "../src/ameba/spec/support"
module Ameba
# Dummy Rule which does nothing.
class DummyRule < Rule::Base
properties do
description "Dummy rule that does nothing"
dummy true
end
def test(source)
end
end
class NamedRule < Rule::Base
properties do
description "A rule with a custom name"
end
def self.name
"BreakingRule"
end
end
class VersionedRule < Rule::Base
properties do
since_version "1.5.0"
description "Rule with a custom version"
end
def test(source)
issue_for({1, 1}, "This rule always adds an error")
end
end
# Rule extended description
class ErrorRule < Rule::Base
properties do
description "Always adds an error"
message "This rule always adds an error"
line_number 1
column_number 1
end
def test(source)
issue_for({line_number, column_number}, message)
end
end
class ScopeRule < Rule::Base
@[YAML::Field(ignore: true)]
getter scopes = [] of AST::Scope
properties do
description "Internal rule to test scopes"
end
def test(source, node : Crystal::VisibilityModifier, scope : AST::Scope)
end
def test(source, node : Crystal::ASTNode, scope : AST::Scope)
@scopes << scope
end
end
class SelfCallsRule < Rule::Base
@[YAML::Field(ignore: true)]
getter call_queue = {} of AST::Scope => Array(Crystal::Call)
properties do
description "Internal rule to test calls to `self` in scopes"
end
def test(source, node : Crystal::Call, scope : AST::Scope)
@call_queue[scope] ||= [] of Crystal::Call
@call_queue[scope] << node
end
end
class ElseIfRule < Rule::Base
@[YAML::Field(ignore: true)]
getter ifs = [] of {Crystal::If, Array(Crystal::If)?}
properties do
description "Internal rule to test else if branches"
end
def test(source, node : Crystal::If, ifs : Array(Crystal::If)? = nil)
@ifs << {node, ifs}
end
end
class FlowExpressionRule < Rule::Base
@[YAML::Field(ignore: true)]
getter expressions = [] of AST::FlowExpression
properties do
description "Internal rule to test flow expressions"
end
def test(source, node, flow_expression : AST::FlowExpression)
@expressions << flow_expression
end
end
class RedundantControlExpressionRule < Rule::Base
@[YAML::Field(ignore: true)]
getter nodes = [] of Crystal::ASTNode
properties do
description "Internal rule to test redundant control expressions"
end
def test(source, node, visitor : AST::RedundantControlExpressionVisitor)
nodes << node
end
end
class ImplicitReturnRule < Rule::Base
@[YAML::Field(ignore: true)]
getter unused_expressions = [] of Crystal::ASTNode
@[YAML::Field(ignore: true)]
getter macro_flags = [] of Bool
properties do
description "Internal rule to test implicit returns"
end
def test(source, node, in_macro : Bool)
@unused_expressions << node
@macro_flags << in_macro
end
end
# A rule that always raises an error
class RaiseRule < Rule::Base
property? should_raise = false
properties do
description "Internal rule that always raises"
end
def test(source)
should_raise? && raise "something went wrong"
end
end
class PerfRule < Rule::Performance::Base
properties do
description "Sample performance rule"
end
def test(source)
issue_for({1, 1}, "Poor performance")
end
end
class AtoAA < Rule::Base
include AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
return unless name = node_source(node.name, source.lines)
return unless name.includes?("A")
issue_for node.name, message: "A to AA" do |corrector|
corrector.replace(node.name, name.sub("A", "AA"))
end
end
end
class AtoB < Rule::Base
include AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
return unless name = node_source(node.name, source.lines)
return unless name.includes?("A")
issue_for node.name, message: "A to B" do |corrector|
corrector.replace(node.name, name.tr("A", "B"))
end
end
end
class BtoA < Rule::Base
include AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
return unless name = node_source(node.name, source.lines)
return unless name.includes?("B")
issue_for node.name, message: "B to A" do |corrector|
corrector.replace(node.name, name.tr("B", "A"))
end
end
end
class BtoC < Rule::Base
include AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
return unless name = node_source(node.name, source.lines)
return unless name.includes?("B")
issue_for node.name, message: "B to C" do |corrector|
corrector.replace(node.name, name.tr("B", "C"))
end
end
end
class CtoA < Rule::Base
include AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
return unless name = node_source(node.name, source.lines)
return unless name.includes?("C")
issue_for node.name, message: "C to A" do |corrector|
corrector.replace(node.name, name.tr("C", "A"))
end
end
end
class ClassToModule < Ameba::Rule::Base
include Ameba::AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ClassDef)
return unless location = node.location
end_location = location.adjust(column_number: {{ "class".size - 1 }})
issue_for location, end_location, message: "class to module" do |corrector|
corrector.replace(location, end_location, "module")
end
end
end
class ModuleToClass < Ameba::Rule::Base
include Ameba::AST::Util
properties do
description "This rule is only used to test infinite loop detection"
end
def test(source, node : Crystal::ModuleDef)
return unless location = node.location
end_location = location.adjust(column_number: {{ "module".size - 1 }})
issue_for location, end_location, message: "module to class" do |corrector|
corrector.replace(location, end_location, "class")
end
end
end
class DummyFormatter < Formatter::BaseFormatter
property started_sources : Array(Source)?
property finished_sources : Array(Source)?
property started_source : Source?
property finished_source : Source?
def started(sources)
@started_sources = sources
end
def source_started(source : Source)
@started_source = source
end
def source_finished(source : Source)
@finished_source = source
end
def finished(sources)
@finished_sources = sources
end
end
class TestNodeVisitor < Crystal::Visitor
NODES = {
Crystal::NilLiteral,
Crystal::Var,
Crystal::Assign,
Crystal::OpAssign,
Crystal::MultiAssign,
Crystal::Block,
Crystal::Macro,
Crystal::Def,
Crystal::If,
Crystal::While,
Crystal::MacroLiteral,
Crystal::Expressions,
Crystal::ControlExpression,
Crystal::Call,
}
def initialize(node)
node.accept self
end
def visit(node : Crystal::ASTNode)
true
end
{% for node in NODES %}
{% getter_name = node.stringify.split("::").last.underscore + "_nodes" %}
getter {{ getter_name.id }} = [] of {{ node }}
def visit(node : {{ node }})
{{ getter_name.id }} << node
true
end
{% end %}
end
end
def with_presenter(klass, *args, deansify = true, **kwargs, &)
io = IO::Memory.new
presenter = klass.new(io)
presenter.run(*args, **kwargs)
output = io.to_s
output = Ameba::Formatter::Util.deansify(output).to_s if deansify
yield presenter, output
end
def as_node(source, *, wants_doc = false)
Crystal::Parser.new(source)
.tap(&.wants_doc = wants_doc)
.parse
end
def as_nodes(source, *, wants_doc = false)
Ameba::TestNodeVisitor.new(as_node(source, wants_doc: wants_doc))
end
def trailing_whitespace
' '
end
================================================
FILE: src/ameba/ast/flow_expression.cr
================================================
require "./util"
module Ameba::AST
# Represents a flow expression in Crystal code.
# For example,
#
# ```
# def foobar
# a = 3
# return 42 # => flow expression
# a + 1
# end
# ```
#
# Flow expression contains an actual node of a control expression and
# a parent node, which allows easily search through the related statement
# (i.e. find unreachable code)
class FlowExpression
include Util
# Is true only if some of the nodes parents is a loop.
getter? in_loop : Bool
# The actual node of the flow expression.
getter node : Crystal::ASTNode
delegate location, end_location, to_s,
to: @node
# Creates a new flow expression.
#
# ```
# FlowExpression.new(node, parent_node)
# ```
def initialize(@node, @in_loop)
end
# Returns nodes which can't be reached because of a flow command inside.
# For example:
#
# ```
# def foobar
# a = 1
# return 42
#
# a + 2 # => unreachable assign node
# end
# ```
def unreachable_nodes
unreachable_nodes = [] of Crystal::ASTNode
case current_node = node
when Crystal::Expressions
control_flow_found = false
current_node.expressions.each do |exp|
if control_flow_found
unreachable_nodes << exp
end
control_flow_found ||= !loop?(exp) && flow_expression?(exp, in_loop?)
end
when Crystal::BinaryOp
if flow_expression?(current_node.left, in_loop?)
unreachable_nodes << current_node.right
end
end
unreachable_nodes
end
end
end
================================================
FILE: src/ameba/ast/liveness_analyzer.cr
================================================
module Ameba::AST
# Performs backward dataflow liveness analysis on a scope's AST to detect
# dead stores (assignments whose values are never read before being
# overwritten or the scope ends).
#
# The algorithm walks the AST in reverse execution order, maintaining a
# set of variable names that are currently "live" (will be read in the
# future). When an assignment is encountered and its target variable is
# not in the live set, the assignment is marked as a dead store.
class LivenessAnalyzer
alias LiveSet = Set(String)
# Maximum iterations for fixed-point convergence in loops.
# In practice, convergence happens in 2-3 iterations since the live set
# can only grow monotonically and is bounded by the number of variables.
MAX_FIXED_POINT_ITERATIONS = 100
BRANCH_NODES = %w[If Unless]
LOOP_NODES = %w[While Until]
CASE_NODES = %w[Case Select]
INNER_SCOPE_NODES = %w[
Block Def ProcLiteral ClassDef ModuleDef EnumDef
LibDef FunDef TypeDef CStructOrUnionDef TypeOf
Macro MacroIf MacroFor
]
@dead_stores = [] of Assignment
@var_names : Set(String)
@assignment_map : Hash(Tuple(String, UInt64), Assignment)
@inner_scope_nodes : Set(UInt64)
# Live sets for loop flow control: `break` exits to post-loop,
# `next` jumps to loop condition. Without these, assignments
# before break/next would be incorrectly marked as dead.
@break_live : LiveSet?
@next_live : LiveSet?
def initialize(@scope : Scope)
@var_names = @scope.variables.map(&.name).to_set
@assignment_map = build_assignment_map
@inner_scope_nodes = @scope.inner_scopes.map(&.node.object_id).to_set
end
# Returns assignments where the value is never read before being
# overwritten or the scope ends.
def dead_stores : Array(Assignment)
analyze.dead_stores
end
# Returns the set of variable names that are live at scope entry.
# A variable live at entry means its value (e.g. from a method argument)
# will be read before being overwritten.
def entry_live_set : LiveSet
analyze.entry_live_set
end
# Performs liveness analysis in a single pass, returning both the dead
# stores and the entry live set.
def analyze : Result
@dead_stores.clear
body = scope_body(@scope.node)
entry_live = body ? propagate_through(body, LiveSet.new) : LiveSet.new
Result.new(@dead_stores, entry_live)
end
record Result, dead_stores : Array(Assignment), entry_live_set : LiveSet
private def build_assignment_map
map = Hash(Tuple(String, UInt64), Assignment).new
@scope.variables.each do |var|
var.assignments.each do |assign|
key = {var.name, assign.node.object_id}
map[key] ||= assign
end
end
map
end
# ameba:disable Metrics/CyclomaticComplexity
private def scope_body(node)
case node
when Crystal::Def then node.body
when Crystal::FunDef then node.body
when Crystal::Block then node.body
when Crystal::ClassDef then node.body
when Crystal::ModuleDef then node.body
when Crystal::LibDef then node.body
when Crystal::CStructOrUnionDef then node.body
when Crystal::Assign then node.value
when Crystal::OpAssign then node.value
when Crystal::ProcLiteral then node.def.body
when Crystal::EnumDef then Crystal::Expressions.from(node.members)
when Crystal::TypeOf then Crystal::Expressions.from(node.expressions)
when Crystal::Expressions then node
else node
end
end
private def inner_scope_node?(node)
@inner_scope_nodes.includes?(node.object_id)
end
private def find_assignment(node, var_name) : Assignment?
@assignment_map[{var_name, node.object_id}]?
end
# Records a dead store if the variable is not in the live set.
private def mark_dead_store(assign_node, var_name, live : LiveSet) : Nil
return if live.includes?(var_name)
if assign = find_assignment(assign_node, var_name)
@dead_stores << assign
end
end
# Removes `var_name` from the live set. When `mark` is true,
# records a dead store if the variable was not live at this point.
private def remove_from_live_set(assign_node, var_name, live : LiveSet, mark) : LiveSet
mark_dead_store(assign_node, var_name, live) if mark
if live.includes?(var_name)
live = live.dup
live.delete(var_name)
end
live
end
# Type-specific overloads. Crystal dispatches to the most specific matching
# overload at runtime when the argument is a virtual type (Crystal::ASTNode+).
private def propagate_through(node : Crystal::Nop, live : LiveSet, mark = true) : LiveSet
live
end
private def propagate_through(node : Crystal::Expressions, live : LiveSet, mark = true) : LiveSet
node.expressions.reverse_each do |exp|
live = propagate_through(exp, live, mark)
end
live
end
private def propagate_through(node : Crystal::Assign, live : LiveSet, mark = true) : LiveSet
return live if inner_scope_node?(node)
target = node.target
unless target.is_a?(Crystal::Var) && @var_names.includes?(target.name)
live = propagate_through(node.value, live, mark)
return propagate_through(target, live, mark)
end
# Only remove from live set if this assignment is tracked in the scope.
# Untracked assignments (e.g. inside record/accessor macro args) are transparent.
if find_assignment(node, target.name)
live = remove_from_live_set(node, target.name, live, mark)
end
propagate_through(node.value, live, mark)
end
private def propagate_through(node : Crystal::OpAssign, live : LiveSet, mark = true) : LiveSet
return live if inner_scope_node?(node)
target = node.target
unless target.is_a?(Crystal::Var) && @var_names.includes?(target.name)
live = propagate_through(node.value, live, mark)
return propagate_through(target, live, mark)
end
# OpAssign both writes and reads the variable (x += 1 means x = x + 1).
# Mark the dead store if the result is never read, then ensure the
# variable is live (since the op-assign reads the current value).
mark_dead_store(node, target.name, live) if mark
unless live.includes?(target.name)
live = live.dup
live.add(target.name)
end
propagate_through(node.value, live, mark)
end
private def propagate_through(node : Crystal::MultiAssign, live : LiveSet, mark = true) : LiveSet
node.targets.reverse_each do |target|
if target.is_a?(Crystal::Var) && @var_names.includes?(target.name)
live = remove_from_live_set(node, target.name, live, mark)
end
end
node.values.reverse_each do |value|
live = propagate_through(value, live, mark)
end
live
end
private def propagate_through(node : Crystal::UninitializedVar, live : LiveSet, mark = true) : LiveSet
var = node.var
if var.is_a?(Crystal::Var) && @var_names.includes?(var.name)
live = remove_from_live_set(node, var.name, live, mark)
end
live
end
private def propagate_through(node : Crystal::TypeDeclaration, live : LiveSet, mark = true) : LiveSet
var = node.var
if var.is_a?(Crystal::Var) && @var_names.includes?(var.name)
if value = node.value
live = remove_from_live_set(node, var.name, live, mark)
live = propagate_through(value, live, mark)
else
# Type declarations without a value are type restrictions, not
# assignments — don't mark as dead. Since Crystal requires the
# variable to be previously undefined, kill it from the live set
# as no prior references can exist.
live = remove_from_live_set(node, var.name, live, mark: false)
end
end
live
end
private def propagate_through(node : Crystal::Var, live : LiveSet, mark = true) : LiveSet
if @var_names.includes?(node.name) && !live.includes?(node.name)
live = live.dup
live.add(node.name)
end
live
end
{% for type in BRANCH_NODES %}
private def propagate_through(node : Crystal::{{ type.id }}, live : LiveSet, mark = true) : LiveSet
then_live = propagate_through(node.then, live, mark)
else_live = propagate_through(node.else, live, mark)
merged = then_live | else_live
propagate_through(node.cond, merged, mark)
end
{% end %}
{% for type in LOOP_NODES %}
private def propagate_through(node : Crystal::{{ type.id }}, live : LiveSet, mark = true) : LiveSet
propagate_through_loop(node.cond, node.body, live, mark)
end
{% end %}
{% for type in CASE_NODES %}
private def propagate_through(node : Crystal::{{ type.id }}, live : LiveSet, mark = true) : LiveSet
propagate_through_case(node, live, mark)
end
{% end %}
private def propagate_through(node : Crystal::ExceptionHandler, live : LiveSet, mark = true) : LiveSet
post_ensure = (body = node.ensure) ? propagate_through(body, live, mark) : live
after_body = (body = node.else) ? propagate_through(body, post_ensure, mark) : post_ensure
# Rescue branches handle exceptions thrown at any point in the body,
# so collect all variables they need.
rescue_live = LiveSet.new
node.rescues.try &.each do |rescue_node|
rescue_live.concat(propagate_through(rescue_node.body, post_ensure, mark))
end
# Body can throw at any point, so variables live in any rescue
# branch must also be considered live throughout the body.
# Union rescue_live because rescue-needed variables are live before the entire handler.
body_live = propagate_through(node.body, after_body | rescue_live, mark)
body_live | rescue_live
end
private def propagate_through(node : Crystal::BinaryOp, live : LiveSet, mark = true) : LiveSet
# Right side is conditional, so union with entry state
right_live = propagate_through(node.right, live, mark)
merged = right_live | live
propagate_through(node.left, merged, mark)
end
private def propagate_through(node : Crystal::Call, live : LiveSet, mark = true) : LiveSet
# Bare `super` and `previous_def` (without parentheses) implicitly
# forward all method arguments, making each argument live.
if node.name.in?("super", "previous_def") && !node.has_parentheses? && node.args.empty?
@scope.arguments.each do |arg|
name = arg.name
if @var_names.includes?(name) && !live.includes?(name)
live = live.dup
live.add(name)
end
end
return live
end
node.block_arg.try { |arg| live = propagate_through(arg, live, mark) }
node.named_args.try &.reverse_each do |named_arg|
live = propagate_through(named_arg.value, live, mark)
end
node.args.reverse_each do |arg|
live = propagate_through(arg, live, mark)
end
node.obj.try { |obj| live = propagate_through(obj, live, mark) }
live
end
private def propagate_through(node : Crystal::Return, live : LiveSet, mark = true) : LiveSet
target_live = LiveSet.new
node.exp.try { |exp| target_live = propagate_through(exp, target_live, mark) }
target_live
end
private def propagate_through(node : Crystal::Break, live : LiveSet, mark = true) : LiveSet
target_live = @break_live || LiveSet.new
node.exp.try { |exp| target_live = propagate_through(exp, target_live, mark) }
target_live
end
private def propagate_through(node : Crystal::Next, live : LiveSet, mark = true) : LiveSet
target_live = @next_live || LiveSet.new
node.exp.try { |exp| target_live = propagate_through(exp, target_live, mark) }
target_live
end
# Inner scope nodes: don't descend into nested scopes
{% for type in INNER_SCOPE_NODES %}
private def propagate_through(node : Crystal::{{ type.id }}, live : LiveSet, mark = true) : LiveSet
live
end
{% end %}
private def propagate_through(node, live : LiveSet, mark = true) : LiveSet
children = [] of Crystal::ASTNode
node.accept_children(ChildCollector.new(children))
children.reverse_each do |child|
live = propagate_through(child, live, mark)
end
live
end
private def propagate_through_loop(cond, body, live : LiveSet, mark) : LiveSet
# Save outer loop context before overwriting
outer_break = @break_live
outer_next = @next_live
# `break` exits to post-loop code, `next` jumps to loop condition.
@break_live = live
entry_live = live.dup
converged_cond_live = entry_live
MAX_FIXED_POINT_ITERATIONS.times do
@next_live = entry_live
converged_cond_live = propagate_through(cond, entry_live, false)
body_live = propagate_through(body, converged_cond_live, false)
new_entry = body_live | live
break if new_entry == entry_live
entry_live = new_entry
end
# Final pass with marking enabled using the converged live set
@next_live = entry_live
cond_live = propagate_through(cond, entry_live, mark)
propagate_through(body, cond_live, mark)
@break_live = outer_break
@next_live = outer_next
converged_cond_live
end
private def propagate_through_case(node : Crystal::Case | Crystal::Select, live : LiveSet, mark) : LiveSet
branch_lives = LiveSet.new
node.whens.each do |when_node|
when_live = propagate_through(when_node.body, live, mark)
when_node.conds.reverse_each do |cond|
when_live = propagate_through(cond, when_live, mark)
end
branch_lives.concat(when_live)
end
else_live = (body = node.else) ? propagate_through(body, live, mark) : live
branch_lives.concat(else_live)
if node.is_a?(Crystal::Case) && (cond = node.cond)
branch_lives = propagate_through(cond, branch_lives, mark)
end
branch_lives
end
private class ChildCollector < Crystal::Visitor
def initialize(@children : Array(Crystal::ASTNode))
end
def visit(node : Crystal::ASTNode)
@children << node
false
end
end
end
end
================================================
FILE: src/ameba/ast/scope.cr
================================================
require "./variabling/*"
module Ameba::AST
# Represents a context of the local variable visibility.
# This is where the local variables belong to.
class Scope
# Whether the scope yields.
setter yields = false
# Scope visibility level
setter visibility : Crystal::Visibility?
# Link to local variables
getter variables = [] of Variable
# Link to all variable references in currency scope
getter references = [] of Reference
# Link to the arguments in current scope
getter arguments = [] of Argument
# Link to the instance variables used in current scope
getter ivariables = [] of InstanceVariable
# Link to the type declaration variables used in current scope
getter type_dec_variables = [] of TypeDecVariable
# Link to the outer scope
getter outer_scope : Scope?
# List of inner scopes
getter inner_scopes = [] of Scope
# The actual AST node that represents a current scope.
getter node : Crystal::ASTNode
delegate location, end_location, to_s,
to: @node
def_equals_and_hash node, location
# Creates a new scope. Accepts the AST node and the outer scope.
#
# ```
# scope = Scope.new(class_node, nil)
# ```
def initialize(@node, @outer_scope = nil)
@outer_scope.try &.inner_scopes.<< self
end
# Creates a new variable in the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_variable(var_node)
# ```
def add_variable(node)
variables << Variable.new(node, self)
end
# Creates a new argument in the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_argument(arg_node)
# ```
def add_argument(node)
add_variable Crystal::Var.new(node.name).at(node)
arguments << Argument.new(node, variables.last)
end
# Adds a new instance variable to the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_ivariable(ivar_node)
# ```
def add_ivariable(node)
ivariables << InstanceVariable.new(node)
end
# Adds a new type declaration variable to the current scope.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.add_type_dec_variable(node)
# ```
def add_type_dec_variable(node)
type_dec_variables << TypeDecVariable.new(node)
end
# Returns variable by its name or `nil` if it does not exist.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.find_variable("foo")
# ```
def find_variable(name : String)
variables.find(&.name.==(name)) ||
inherited? { outer_scope.try &.find_variable(name) }
end
# Creates a new assignment for the variable.
#
# ```
# scope = Scope.new(class_node, nil)
# scope.assign_variable(var_name, assign_node)
# ```
def assign_variable(name, node)
find_variable(name).try &.assign(node, self)
end
# Returns `true` if current scope represents a block (or proc),
# `false` otherwise.
def block?
node.is_a?(Crystal::Block) ||
node.is_a?(Crystal::ProcLiteral)
end
# Returns `true` if current scope represents a spawn block, e. g.
#
# ```
# spawn do
# # ...
# end
# ```
def spawn_block?
node.as?(Crystal::Block).try(&.call).try(&.name) == "spawn"
end
# Returns `true` if current scope sits inside a macro.
def in_macro?
(node.is_a?(Crystal::Macro) ||
node.is_a?(Crystal::MacroIf) ||
node.is_a?(Crystal::MacroFor)) ||
!!outer_scope.try(&.in_macro?)
end
# Returns `true` if instance variable is assigned in this scope.
def assigns_ivar?(name)
arguments.any?(&.name.== name) &&
ivariables.any?(&.name.== "@#{name}")
end
# Returns `true` if type declaration variable is assigned in this scope.
def assigns_type_dec?(name)
type_dec_variables.any?(&.name.== name) ||
!!inherited? { outer_scope.try(&.assigns_type_dec?(name)) }
end
# Returns `true` if and only if current scope represents some
# type definition, for example a class.
def type_definition?
node.is_a?(Crystal::ClassDef) ||
node.is_a?(Crystal::ModuleDef) ||
node.is_a?(Crystal::EnumDef) ||
node.is_a?(Crystal::LibDef) ||
node.is_a?(Crystal::FunDef) ||
node.is_a?(Crystal::TypeDef) ||
node.is_a?(Crystal::CStructOrUnionDef)
end
# Returns `true` if current scope (or any of inner scopes) references variable,
# `false` otherwise.
def references?(variable : Variable, check_inner_scopes = true)
variable.references.any? do |reference|
(reference.scope == self) ||
(check_inner_scopes && inner_scopes.any?(&.references?(variable)))
end || variable.used_in_macro?
end
# Returns `true` if current scope (or any of inner scopes) yields,
# `false` otherwise.
def yields?(check_inner_scopes = true)
@yields || (check_inner_scopes && inner_scopes.any?(&.yields?))
end
# Returns visibility of the current scope (could be inherited from the outer scope).
def visibility
@visibility || inherited? { outer_scope.try(&.visibility) }
end
{% for type in %w[Def ClassDef ModuleDef EnumDef LibDef FunDef].map(&.id) %}
{% method_name = type.underscore %}
# Returns `true` if current scope is a {{ method_name[0..-5] }} def, `false` otherwise.
def {{ method_name }}?(*, check_outer_scopes = false)
node.is_a?(Crystal::{{ type }}) ||
!!(check_outer_scopes &&
outer_scope.try(&.{{ method_name }}?(check_outer_scopes: true)))
end
{% end %}
# Returns `true` if this scope is a top level scope, `false` otherwise.
def top_level?
outer_scope.nil?
end
def inherited?
!(node.is_a?(Crystal::Def) || node.is_a?(Crystal::FunDef) || node.is_a?(Crystal::Assign) || node.is_a?(Crystal::OpAssign))
end
def inherited?(&)
yield if inherited?
end
# Returns `true` if var is an argument in current scope, `false` otherwise.
def arg?(var)
case current_node = node
when Crystal::Def
var.is_a?(Crystal::Arg) && any_arg?(current_node.args, var)
when Crystal::Block
var.is_a?(Crystal::Var) && any_arg?(current_node.args, var)
when Crystal::ProcLiteral
var.is_a?(Crystal::Var) && any_arg?(current_node.def.args, var)
else
false
end
end
private def any_arg?(args, var)
args.any? { |arg| arg.name == var.name && arg.location == var.location }
end
# Returns `true` if the *node* represents exactly
# the same Crystal node as `@node`.
def eql?(node)
node == @node &&
node.location == @node.location
end
end
end
================================================
FILE: src/ameba/ast/util.cr
================================================
# Utility module for Ameba's rules.
module Ameba::AST::Util
extend self
# Returns tuple with two bool flags:
#
# 1. is *node* a literal?
# 2. can *node* be proven static?
protected def literal_kind?(node) : {Bool, Bool}
case node
when Crystal::NilLiteral,
Crystal::BoolLiteral,
Crystal::NumberLiteral,
Crystal::CharLiteral,
Crystal::StringLiteral,
Crystal::SymbolLiteral,
Crystal::ProcLiteral,
Crystal::MacroLiteral
{true, true}
when Crystal::StringInterpolation
{true, node.expressions.all? do |exp|
static_literal?(exp)
end}
when Crystal::RegexLiteral
{true, static_literal?(node.value)}
when Crystal::RangeLiteral
{true, static_literal?(node.from) &&
static_literal?(node.to)}
when Crystal::ArrayLiteral,
Crystal::TupleLiteral
{true, node.elements.all? do |element|
static_literal?(element)
end}
when Crystal::HashLiteral
{true, node.entries.all? do |entry|
static_literal?(entry.key) &&
static_literal?(entry.value)
end}
when Crystal::NamedTupleLiteral
{true, node.entries.all? do |entry|
static_literal?(entry.value)
end}
else
{false, false}
end
end
# Returns `true` if current `node` is a static literal, `false` otherwise.
def static_literal?(node) : Bool
is_literal, is_static = literal_kind?(node)
is_literal && is_static
end
# Returns `true` if current `node` is a dynamic literal, `false` otherwise.
def dynamic_literal?(node) : Bool
is_literal, is_static = literal_kind?(node)
is_literal && !is_static
end
# Returns `true` if current `node` is a literal, `false` otherwise.
def literal?(node) : Bool
is_literal, _ = literal_kind?(node)
is_literal
end
# Returns `true` if current `node` is a `Crystal::Path`
# matching given *name*, `false` otherwise.
def path_named?(node, *names : String) : Bool
node.is_a?(Crystal::Path) &&
node.names.join("::").in?(names)
end
# Returns `true` if the *node* is a `Crystal::Call`
# with either `node.block` or `node.block_arg` set, `false` otherwise.
def has_block?(node) : Bool
node.is_a?(Crystal::Call) &&
!!(node.block || node.block_arg)
end
# Returns `true` if the *node* is a `Crystal::Call`
# with either `node.block` or `node.block_arg` set, `false` otherwise.
def has_arguments?(node) : Bool
node.is_a?(Crystal::Call) &&
!!(node.args.present? || node.named_args.try(&.present?))
end
# Returns `true` if the *node* is a `Crystal::Def`
# with either `args`, `splat_index`, or `double_splat` set,
# `false` otherwise.
def takes_arguments?(node) : Bool
node.is_a?(Crystal::Def) &&
!!(node.args.present? || node.splat_index || node.double_splat)
end
# Returns a source code for the current node.
# This method uses `node.location` and `node.end_location`
# to determine and cut a piece of source of the node.
def node_source(node, code_lines)
loc, end_loc = node.location, node.end_location
return unless loc && end_loc
source_between(loc, end_loc, code_lines)
end
# Returns the source code from *loc* to *end_loc* (inclusive).
def source_between(loc, end_loc, code_lines) : String?
line, column = loc.line_number - 1, loc.column_number - 1
end_line, end_column = end_loc.line_number - 1, end_loc.column_number - 1
node_lines = code_lines[line..end_line]
first_line, last_line = node_lines[0]?, node_lines[-1]?
return if first_line.nil? || last_line.nil?
return if first_line.size < column # compiler reports incorrect location
node_lines[0] = first_line.sub(0...column, "")
if line == end_line # one line
end_column = end_column - column
last_line = node_lines[0]
end
return if last_line.size < end_column + 1
node_lines[-1] = last_line.sub(end_column + 1...last_line.size, "")
node_lines.join('\n')
end
# Returns `true` if node is a flow command, `false` otherwise.
# Node represents a flow command if it is a control expression,
# or special call node that interrupts execution (i.e. raise, exit, abort).
def flow_command?(node, in_loop)
case node
when Crystal::Return
true
when Crystal::Break, Crystal::Next
in_loop
when Crystal::Call
raise?(node) || exit?(node) || abort?(node)
else
false
end
end
# Returns `true` if node is a flow expression, `false` if not.
# Node represents a flow expression if it is full-filled by a flow command.
#
# For example, this node is a flow expression, because each branch contains
# a flow command `return`:
#
# ```
# if a > 0
# return :positive
# elsif a < 0
# return :negative
# else
# return :zero
# end
# ```
#
# This node is a not a flow expression:
#
# ```
# if a > 0
# return :positive
# end
# ```
#
# That's because not all branches return(i.e. `else` is missing).
def flow_expression?(node, in_loop = false)
return true if flow_command? node, in_loop
case node
when Crystal::If, Crystal::Unless
flow_expressions? [node.then, node.else], in_loop
when Crystal::BinaryOp
flow_expression? node.left, in_loop
when Crystal::Case, Crystal::Select
flow_expressions? [node.whens, node.else].flatten, in_loop
when Crystal::ExceptionHandler
flow_expressions? [node.else || node.body, node.rescues].flatten, in_loop
when Crystal::While, Crystal::Until, Crystal::Rescue, Crystal::When
flow_expression? node.body, in_loop
when Crystal::Expressions
node.expressions.any? { |exp| flow_expression? exp, in_loop }
else
false
end
end
private def flow_expressions?(nodes, in_loop)
nodes.all? { |exp| flow_expression? exp, in_loop }
end
# Returns `true` if node represents `raise` method call.
def raise?(node)
node.is_a?(Crystal::Call) &&
node.name == "raise" && node.args.size == 1 && node.obj.nil?
end
# Returns `true` if node represents `exit` method call.
def exit?(node)
node.is_a?(Crystal::Call) &&
node.name == "exit" && node.args.size <= 1 && node.obj.nil?
end
# Returns `true` if node represents `abort` method call.
def abort?(node)
node.is_a?(Crystal::Call) &&
node.name == "abort" && node.args.size <= 2 && node.obj.nil?
end
# Returns `true` if node represents a loop.
def loop?(node)
case node
when Crystal::While, Crystal::Until
true
when Crystal::Call
node.name == "loop" && node.args.empty? && node.obj.nil?
else
false
end
end
# Returns `true` if *name* represents operator method.
def operator_method_name?(name : String)
name != "->" &&
name.each_char.none?(&.alphanumeric?)
end
# Returns `true` if *node* represents operator method.
def operator_method?(node)
return false unless node.responds_to?(:name)
return false unless name = node.name.try(&.to_s.presence)
operator_method_name?(name)
end
# Returns `true` if *name* represents setter method.
def setter_method_name?(name : String)
name == "[]=" ||
!name.empty? && name[0].letter? && name.ends_with?('=')
end
# Returns `true` if *node* represents setter method.
def setter_method?(node)
return false unless node.responds_to?(:name)
return false unless name = node.name.try(&.to_s.presence)
setter_method_name?(name)
end
# Returns `true` if *node* is a suffix node (`if` / `unless` / `rescue` / `ensure`).
def suffix?(node)
case node
when Crystal::If, Crystal::Unless
node.location == node.then.location
when Crystal::ExceptionHandler
node.suffix
else
false
end
end
# Returns `true` if *node* represents a short block version (`&.foo?`).
def short_block?(node, code_lines)
return false unless node.is_a?(Crystal::Block)
return false unless location = node.location
return false unless end_location = node.body.end_location
!!source_between(location, end_location, code_lines)
.try(&.starts_with?("&."))
end
# Returns `true` if *node* is a call with a short block version (`&.foo?`).
def has_short_block?(node, code_lines)
node.is_a?(Crystal::Call) &&
short_block?(node.block, code_lines)
end
# Returns `true` if node has a `:nodoc:` annotation as the first line.
def nodoc?(node)
return false unless node.responds_to?(:doc)
return false unless doc = node.doc.presence
doc.lines.first?.try(&.strip) == ":nodoc:"
end
# Returns `true` if node is a _heredoc_, `false` otherwise.
def heredoc?(node, source : Source)
return false unless node.is_a?(Crystal::StringInterpolation) ||
node.is_a?(Crystal::StringLiteral)
return false unless location = node.location
return false unless location_pos = source.pos(location)
source.code[location_pos..(location_pos + 2)]? == "<<-"
end
# Returns the exp code of a control expression.
# Wraps implicit tuple literal with curly brackets (e.g. multi-return).
def control_exp_code(node : Crystal::ControlExpression, code_lines)
return unless exp = node.exp
return unless exp_code = node_source(exp, code_lines)
return exp_code unless exp.is_a?(Crystal::TupleLiteral) && exp_code[0] != '{'
return unless exp_start = exp.elements.first.location
return unless exp_end = exp.end_location
"{#{source_between(exp_start, exp_end, code_lines)}}"
end
def name_location_or(node : Crystal::ASTNode, *, adjust_location_column_number = nil)
name = node.name if node.responds_to?(:name)
return node unless name = name.try(&.to_s.presence)
return node unless location = name_location(node) || node.location
location =
location.adjust(column_number: adjust_location_column_number || 0)
end_location =
location.adjust(column_number: name.size - 1)
{location, end_location}
end
def name_location_or(token : Crystal::Token, name, *, adjust_location_column_number = nil)
name = name.to_s.presence
location =
token.location.adjust(column_number: adjust_location_column_number || 0)
end_location =
location.adjust(column_number: name ? name.size - 1 : 0)
{location, end_location}
end
# Returns `nil` if *node* does not contain a name.
def name_location(node)
if loc = node.name_location
return loc
end
return node.var.location if node.is_a?(Crystal::TypeDeclaration) ||
node.is_a?(Crystal::UninitializedVar)
return unless node.responds_to?(:name) && (name = node.name)
return unless name.is_a?(Crystal::ASTNode)
name.location
end
# Returns zero if *node* does not contain a name.
def name_size(node)
unless (size = node.name_size).zero?
return size
end
return 0 unless node.responds_to?(:name) && (name = node.name)
case name
when Crystal::ASTNode then name.name_size
when Crystal::Token::Kind then name.to_s.size # Crystal::MagicConstant
else name.size
end
end
# Returns `nil` if *node* does not contain a name.
#
# NOTE: Use this instead of `Crystal::Call#name_end_location` to avoid an
# off-by-one error.
def name_end_location(node)
return unless loc = name_location(node)
return if (size = name_size(node)).zero?
loc.adjust(column_number: size - 1)
end
end
================================================
FILE: src/ameba/ast/variabling/argument.cr
================================================
module Ameba::AST
# Represents the argument of some node.
# Holds the reference to the variable, thus to scope.
#
# For example, all these vars are arguments:
#
# ```
# def method(a, b, c = 10, &block)
# 3.times do |i|
# end
#
# ->(x : Int32) { }
# end
# ```
class Argument
# The actual node.
getter node : Crystal::Var | Crystal::Arg
# Variable of this argument (may be the same node)
getter variable : Variable
delegate location, end_location, to_s,
to: @node
# Creates a new argument.
#
# ```
# Argument.new(node, variable)
# ```
def initialize(@node, @variable)
end
# Returns `true` if the `name` is empty, `false` otherwise.
def anonymous?
name.blank?
end
# Returns `true` if the `name` starts with '_', `false` otherwise.
def ignored?
name.starts_with? '_'
end
# Name of the argument.
def name
case current_node = node
when Crystal::Var, Crystal::Arg
current_node.name
else
raise ArgumentError.new "Invalid node"
end
end
end
end
================================================
FILE: src/ameba/ast/variabling/assignment.cr
================================================
require "./reference"
require "./variable"
module Ameba::AST
# Represents the assignment to the variable.
# Holds the assign node and the variable.
class Assignment
# The actual assignment node.
getter node : Crystal::ASTNode
# Variable of this assignment.
getter variable : Variable
# A scope assignment belongs to
getter scope : Scope
delegate location, end_location, to_s,
to: @node
# Creates a new assignment.
#
# ```
# Assignment.new(node, variable, scope)
# ```
def initialize(@node, @variable, @scope)
end
# Returns `true` if this assignment is an op assign, `false` if not.
# For example, this is an op assign:
#
# ```
# a ||= 1
# ```
def op_assign?
node.is_a?(Crystal::OpAssign)
end
# Returns the target node of the variable in this assignment.
def target_node
case assign = node
when Crystal::UninitializedVar then assign.var
when Crystal::Assign, Crystal::OpAssign then assign.target
when Crystal::MultiAssign
assign.targets.find(node) do |target|
target.is_a?(Crystal::Var) && target.name == variable.name
end
else
node
end
end
end
end
================================================
FILE: src/ameba/ast/variabling/ivariable.cr
================================================
module Ameba::AST
class InstanceVariable
getter node : Crystal::InstanceVar
delegate location, end_location, name, to_s,
to: @node
def initialize(@node)
end
end
end
================================================
FILE: src/ameba/ast/variabling/reference.cr
================================================
require "./variable"
module Ameba::AST
# Represents a reference to the variable.
# It behaves like a variable is used to distinguish a
# the variable from its reference.
class Reference < Variable
property? explicit = true
end
end
================================================
FILE: src/ameba/ast/variabling/type_dec_variable.cr
================================================
module Ameba::AST
class TypeDecVariable
getter node : Crystal::TypeDeclaration
delegate location, end_location, to_s,
to: @node
def initialize(@node)
end
def name
case var = @node.var
when Crystal::Var, Crystal::InstanceVar, Crystal::ClassVar, Crystal::Global
var.name
else
raise "Unsupported var node type: #{var.class}"
end
end
end
end
================================================
FILE: src/ameba/ast/variabling/variable.cr
================================================
module Ameba::AST
# Represents the existence of the local variable.
# Holds the var node and variable assignments.
class Variable
# List of the assignments of this variable.
getter assignments = [] of Assignment
# List of the references of this variable.
getter references = [] of Reference
# The actual var node.
getter node : Crystal::Var
# Scope of this variable.
getter scope : Scope
delegate location, end_location, name, to_s,
to: @node
# Creates a new variable(in the scope).
#
# ```
# Variable.new(node, scope)
# ```
def initialize(@node, @scope)
end
# Returns `true` if it is a special variable, i.e `$?`.
def special?
@node.special_var?
end
# Assigns the variable (creates a new assignment).
# Variable may have multiple assignments.
#
# ```
# variable = Variable.new(node, scope)
# variable.assign(node1)
# variable.assign(node2)
# variable.assignment.size # => 2
# ```
def assign(node, scope)
assignments << Assignment.new(node, self, scope)
end
# Returns `true` if variable has any reference.
#
# ```
# variable = Variable.new(node, scope)
# variable.reference(var_node, some_scope)
# variable.referenced? # => true
# ```
def referenced?
!references.empty?
end
# Creates a reference to this variable in some scope.
#
# ```
# variable = Variable.new(node, scope)
# variable.reference(var_node, some_scope)
# ```
def reference(node : Crystal::Var, scope : Scope)
Reference.new(node, scope).tap do |reference|
references << reference
scope.references << reference
end
end
# :ditto:
def reference(scope : Scope)
reference(node, scope)
end
# Returns `true` if the current var is referenced in
# in the block. For example this variable is captured
# by block:
#
# ```
# a = 1
# 3.times { |i| a = a + i }
# ```
#
# And this variable is not captured by block.
#
# ```
# i = 1
# 3.times { |i| i + 1 }
# ```
def captured_by_block?(scope = @scope)
scope.inner_scopes.each do |inner_scope|
return true if inner_scope.block? &&
inner_scope.references?(self, check_inner_scopes: false)
return true if captured_by_block?(inner_scope)
end
false
end
# Returns `true` if current variable potentially referenced in a macro,
# `false` if not.
def used_in_macro?(scope = @scope)
scope.inner_scopes.each do |inner_scope|
return true if MacroReferenceFinder.new(inner_scope.node, node.name).references?
end
return true if MacroReferenceFinder.new(scope.node, node.name).references?
return true if (outer_scope = scope.outer_scope) && used_in_macro?(outer_scope)
false
end
# Returns `true` if the variable is a target (on the left) of the assignment,
# `false` otherwise.
def target_of?(assign)
case assign
when Crystal::UninitializedVar then eql?(assign.var)
when Crystal::Assign, Crystal::OpAssign then eql?(assign.target)
when Crystal::MultiAssign
assign.targets.any? { |target| eql?(target) }
else
false
end
end
# Returns `true` if the name starts with '_', `false` if not.
def ignored?
name.starts_with? '_'
end
# Returns `true` if the `node` represents exactly
# the same Crystal node as `@node`.
def eql?(node)
node.is_a?(Crystal::Var) &&
node.name == @node.name &&
node.location == @node.location
end
# Returns `true` if the variable is declared before the `node`.
def declared_before?(node)
var_location, node_location = location, node.location
return unless var_location && node_location
(var_location.line_number < node_location.line_number) ||
(var_location.line_number == node_location.line_number &&
var_location.column_number < node_location.column_number)
end
end
end
================================================
FILE: src/ameba/ast/visitors/base_visitor.cr
================================================
require "compiler/crystal/syntax/*"
# A module that helps to traverse Crystal AST using `Crystal::Visitor`.
module Ameba::AST
# An abstract base visitor that utilizes general logic for all visitors.
abstract class BaseVisitor < Crystal::Visitor
# A corresponding rule that uses this visitor.
@rule : Rule::Base
# A source that needs to be traversed.
@source : Source
# Creates instance of this visitor.
#
# ```
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
# ```
def initialize(@rule, @source)
@source.ast.accept self
end
# A main visit method that accepts `Crystal::ASTNode`.
# Returns `true`, meaning all child nodes will be traversed.
def visit(node : Crystal::ASTNode)
true
end
end
end
================================================
FILE: src/ameba/ast/visitors/counting_visitor.cr
================================================
module Ameba::AST
# AST Visitor that counts occurrences of certain keywords
class CountingVisitor < Crystal::Visitor
DEFAULT_COMPLEXITY = 1
# Returns the number of keywords that were found in the node
getter count = DEFAULT_COMPLEXITY
# Returns `true` if the node is within a macro condition
getter? macro_condition = false
# Creates a new counting visitor
def initialize(node : Crystal::ASTNode)
node.accept self
end
# :nodoc:
def visit(node : Crystal::ASTNode)
true
end
# Uses the same logic than rubocop. See
# https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21
# Except "for", because crystal doesn't have a "for" loop.
{% for node in %i[if unless while until rescue or and] %}
# :nodoc:
def visit(node : Crystal::{{ node.id.capitalize }})
unless macro_condition?
@count += 1
end
true
end
{% end %}
# :nodoc:
def visit(node : Crystal::Case)
unless macro_condition?
# Count the complexity of an exhaustive `Case` as 1
# Otherwise count the number of `When`s
@count += node.exhaustive? ? 1 : node.whens.size
end
true
end
# :nodoc:
def visit(node : Crystal::Select)
unless macro_condition?
@count += node.whens.size
end
true
end
def visit(node : Crystal::MacroIf | Crystal::MacroFor)
@macro_condition = true
@count = DEFAULT_COMPLEXITY
false
end
end
end
================================================
FILE: src/ameba/ast/visitors/elseif_aware_node_visitor.cr
================================================
require "./node_visitor"
module Ameba::AST
# A class that utilizes a logic inherited from `NodeVisitor` to traverse AST
# nodes and fire a source test callback with the `Crystal::If` node and an array
# containing all `elsif` branches (first branch is an `if` node) in case
# at least one `elsif` branch is reached, and `nil` otherwise.
#
# In Crystal, consecutive `elsif` branches are transformed into `if` branches
# attached to the `else` branch of an adjacent `if` branch.
#
# For example:
#
# ```
# if foo
# do_foo
# elsif bar
# do_bar
# elsif baz
# do_baz
# else
# do_something_else
# end
# ```
#
# is transformed into:
#
# ```
# if foo
# do_foo
# else
# if bar
# do_bar
# else
# if baz
# do_baz
# else
# do_something_else
# end
# end
# end
# ```
class ElseIfAwareNodeVisitor < NodeVisitor
include Util
getter? exclude_ternary : Bool
getter? exclude_suffix : Bool
def initialize(rule, source, *, skip : Array | Category? = nil,
@exclude_ternary = true,
@exclude_suffix = true)
super rule, source,
skip: if skip.is_a?(Category)
NodeVisitor.category_to_node_classes(skip)
else
skip
end
end
def visit(node : Crystal::If)
if_node = node
ifs = [] of Crystal::If
loop do
break if exclude_ternary? && if_node.ternary?
break if exclude_suffix? && suffix?(if_node)
ifs << if_node
if_node.cond.accept self
if_node.then.accept self
unless (if_node = if_node.else).is_a?(Crystal::If)
if_node.accept self
break
end
end
@rule.test @source, node, (ifs if ifs.size > 1)
false
end
end
end
================================================
FILE: src/ameba/ast/visitors/flow_expression_visitor.cr
================================================
require "../util"
require "./base_visitor"
module Ameba::AST
# AST Visitor that traverses all the flow expressions.
class FlowExpressionVisitor < BaseVisitor
include Util
@loop_stack = [] of Crystal::ASTNode
# :nodoc:
def visit(node)
if flow_expression?(node, in_loop?)
@rule.test @source, node, FlowExpression.new(node, in_loop?)
end
true
end
# :nodoc:
def visit(node : Crystal::While | Crystal::Until)
on_loop_started(node)
true
end
# :nodoc:
def visit(node : Crystal::Call)
on_loop_started(node) if loop?(node)
true
end
# :nodoc:
def end_visit(node : Crystal::While | Crystal::Until)
on_loop_ended(node)
end
# :nodoc:
def end_visit(node : Crystal::Call)
on_loop_ended(node) if loop?(node)
end
private def on_loop_started(node)
@loop_stack.push(node)
end
private def on_loop_ended(node)
@loop_stack.pop?
end
private def in_loop?
!@loop_stack.empty?
end
end
end
================================================
FILE: src/ameba/ast/visitors/implicit_return_visitor.cr
================================================
module Ameba::AST
# AST visitor that finds nodes that are not used by their surrounding scope.
#
# A stack is used to keep track of when a node is used, incrementing every time
# something will capture its implicit (or explicit) return value, such as the
# path in a class name or the value in an assign.
#
# This also allows for passing through whether a node is captured from a nodes
# parent to its children, such as from an `if` statements parent to it's body,
# as the body is not used by the `if` itself, but by its parent scope.
class ImplicitReturnVisitor < BaseVisitor
# When greater than zero, indicates the current node's return value is used
@stack : Int32 = 0
@in_macro : Bool = false
# The stack is swapped out here as `Crystal::Expressions` are isolated from
# their parents scope. Only the last line in an expressions node can be
# captured by their parent node.
def visit(node : Crystal::Expressions) : Bool
report_implicit_return(node)
last_idx = node.expressions.size - 1
swap_stack do |old_stack|
node.expressions.each_with_index do |exp, idx|
case
when exp.is_a?(Crystal::ControlExpression)
incr_stack { exp.accept(self) }
break
when idx == last_idx && old_stack.positive?
incr_stack { exp.accept(self) }
else
exp.accept(self)
end
end
end
false
end
def visit(node : Crystal::BinaryOp) : Bool
report_implicit_return(node)
case node.right
when Crystal::Call, Crystal::Expressions, Crystal::ControlExpression
incr_stack { node.left.accept(self) }
else
node.left.accept(self)
end
node.right.accept(self)
false
end
def visit(node : Crystal::Call) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::Arg) : Bool
report_implicit_return(node)
incr_stack { node.default_value.try &.accept(self) }
false
end
def visit(node : Crystal::EnumDef) : Bool
report_implicit_return(node)
incr_stack { node.members.each &.accept(self) }
false
end
def visit(node : Crystal::Assign | Crystal::OpAssign) : Bool
report_implicit_return(node)
incr_stack { node.value.accept(self) }
false
end
def visit(node : Crystal::MultiAssign) : Bool
report_implicit_return(node)
incr_stack { node.values.each &.accept(self) }
false
end
def visit(node : Crystal::If | Crystal::Unless) : Bool
report_implicit_return(node)
incr_stack { node.cond.accept(self) }
node.then.accept(self)
node.else.accept(self)
false
end
def visit(node : Crystal::While | Crystal::Until) : Bool
report_implicit_return(node)
incr_stack { node.cond.accept(self) }
node.body.accept(self)
false
end
def visit(node : Crystal::Def) : Bool
report_implicit_return(node)
incr_stack do
node.args.each &.accept(self)
node.double_splat.try &.accept(self)
node.block_arg.try &.accept(self)
end
if node.name == "initialize" || Util.path_named?(node.return_type, "Nil")
# Special case of the return type being nil, meaning the last
# line of the method body is ignored
# Last line of initialize methods are also ignored
swap_stack { node.body.accept(self) }
else
incr_stack { node.body.accept(self) }
end
false
end
def visit(node : Crystal::Macro) : Bool
report_implicit_return(node)
incr_stack do
node.args.each &.accept(self)
node.double_splat.try &.accept(self)
node.block_arg.try &.accept(self)
end
swap_stack do
node.body.accept(self)
end
false
end
def visit(node : Crystal::ClassDef | Crystal::ModuleDef) : Bool
report_implicit_return(node)
node.body.accept(self)
false
end
def visit(node : Crystal::FunDef) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::Cast | Crystal::NilableCast | Crystal::IsA | Crystal::RespondsTo) : Bool
report_implicit_return(node)
incr_stack { node.obj.accept(self) }
false
end
def visit(node : Crystal::UnaryExpression) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::TypeOf) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::Annotation) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::TypeDeclaration) : Bool
report_implicit_return(node)
incr_stack { node.value.try &.accept(self) }
false
end
def visit(node : Crystal::ArrayLiteral | Crystal::TupleLiteral) : Bool
report_implicit_return(node)
incr_stack { node.elements.each &.accept(self) }
false
end
def visit(node : Crystal::StringInterpolation) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::HashLiteral | Crystal::NamedTupleLiteral) : Bool
report_implicit_return(node)
incr_stack { node.entries.each &.value.accept(self) }
false
end
def visit(node : Crystal::Case) : Bool
report_implicit_return(node)
incr_stack { node.cond.try &.accept(self) }
node.whens.each &.accept(self)
node.else.try &.accept(self)
false
end
def visit(node : Crystal::Select) : Bool
report_implicit_return(node)
node.accept_children(self)
false
end
def visit(node : Crystal::When) : Bool
report_implicit_return(node)
incr_stack { node.conds.each &.accept(self) }
node.body.accept(self)
false
end
def visit(node : Crystal::Rescue) : Bool
report_implicit_return(node)
node.body.accept(self)
false
end
def visit(node : Crystal::ExceptionHandler) : Bool
report_implicit_return(node)
if node.else
# Last line of body isn't implicitly returned if there's an else
swap_stack { node.body.try &.accept(self) }
else
node.body.accept(self)
end
node.rescues.try &.each &.accept(self)
node.else.try &.accept(self)
# Last line of ensure isn't implicitly returned
swap_stack { node.ensure.try &.accept(self) }
false
end
def visit(node : Crystal::Block) : Bool
report_implicit_return(node)
node.body.accept(self)
false
end
def visit(node : Crystal::ControlExpression) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::RangeLiteral) : Bool
report_implicit_return(node)
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::RegexLiteral) : Bool
report_implicit_return(node)
# Regex literals either contain string literals or string interpolations,
# both of which are "captured" by the parent regex literal
incr_stack { node.accept_children(self) }
false
end
def visit(node : Crystal::Yield) : Bool
report_implicit_return(node)
incr_stack { node.exps.each &.accept(self) }
false
end
def visit(node : Crystal::MacroExpression) : Bool
report_implicit_return(node)
in_macro do
if node.output?
incr_stack { node.exp.accept(self) }
else
swap_stack { node.exp.accept(self) }
end
end
false
end
def visit(node : Crystal::MacroIf) : Bool
report_implicit_return(node)
in_macro do
swap_stack do
incr_stack { node.cond.accept(self) }
node.then.accept(self)
node.else.accept(self)
end
end
false
end
def visit(node : Crystal::MacroFor) : Bool
report_implicit_return(node)
in_macro do
swap_stack { node.body.accept(self) }
end
false
end
def visit(node : Crystal::Alias | Crystal::TypeDef | Crystal::MacroVar)
false
end
def visit(node : Crystal::Generic | Crystal::Path | Crystal::Union | Crystal::UninitializedVar | Crystal::OffsetOf | Crystal::LibDef | Crystal::Include | Crystal::Extend) : Bool
report_implicit_return(node)
false
end
def visit(node)
report_implicit_return(node)
true
end
private def report_implicit_return(node) : Nil
@rule.test(@source, node, @in_macro) unless @stack.positive?
end
# Indicates that any nodes visited within the block are captured / used.
private def incr_stack(&) : Nil
@stack += 1
yield
ensure
@stack -= 1
end
private def swap_stack(& : Int32 -> Nil) : Nil
old_stack = @stack
@stack = 0
begin
yield old_stack
ensure
@stack = old_stack
end
end
private def in_macro(&) : Nil
old_value = @in_macro
@in_macro = true
begin
yield
ensure
@in_macro = old_value
end
end
end
end
================================================
FILE: src/ameba/ast/visitors/macro_reference_finder.cr
================================================
module Ameba::AST
class MacroReferenceFinder < Crystal::Visitor
property? references = false
def initialize(node, @reference : String)
node.accept self
end
@[AlwaysInline]
private def includes_reference?(val)
val.to_s.includes?(@reference)
end
def visit(node : Crystal::MacroLiteral)
!(@references ||= includes_reference?(node.value))
end
def visit(node : Crystal::MacroExpression)
!(@references ||= includes_reference?(node.exp))
end
def visit(node : Crystal::MacroFor)
!(@references ||= includes_reference?(node.exp) ||
includes_reference?(node.body))
end
def visit(node : Crystal::MacroIf)
!(@references ||= includes_reference?(node.cond) ||
includes_reference?(node.then) ||
includes_reference?(node.else))
end
def visit(node : Crystal::ASTNode)
true
end
end
end
================================================
FILE: src/ameba/ast/visitors/node_visitor.cr
================================================
require "./base_visitor"
module Ameba::AST
# An AST Visitor that traverses the source and allows all nodes
# to be inspected by rules.
#
# ```
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
# ```
class NodeVisitor < BaseVisitor
@[Flags]
enum Category
Macro
end
# List of nodes to be visited by Ameba's rules.
NODES = {
Alias,
ArrayLiteral,
Assign,
Block,
Call,
Case,
ClassDef,
ClassVar,
ControlExpression,
Def,
EnumDef,
ExceptionHandler,
Expressions,
HashLiteral,
If,
InstanceVar,
IsA,
LibDef,
MacroExpression,
ModuleDef,
MultiAssign,
NilLiteral,
ProcLiteral,
Rescue,
Select,
StringInterpolation,
StringLiteral,
Union,
Unless,
Until,
Var,
When,
While,
}
@skip : Array(Crystal::ASTNode.class)?
def self.category_to_node_classes(category : Category)
([] of Crystal::ASTNode.class).tap do |classes|
classes.push(
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
) if category.macro?
end
end
def initialize(@rule, @source, *, skip : Category)
initialize @rule, @source,
skip: NodeVisitor.category_to_node_classes(skip)
end
def initialize(@rule, @source, *, skip : Array? = nil)
@skip = skip.try &.map(&.as(Crystal::ASTNode.class))
super @rule, @source
end
def visit(node : Crystal::VisibilityModifier)
node.exp.visibility = node.modifier
true
end
{% for name in NODES %}
# A visit callback for `Crystal::{{ name }}` node.
#
# Returns `true` if the child nodes should be traversed as well,
# `false` otherwise.
def visit(node : Crystal::{{ name }})
return false if skip?(node)
@rule.test @source, node
true
end
{% end %}
def visit(node)
!skip?(node)
end
private def skip?(node)
!!@skip.try(&.includes?(node.class))
end
end
end
================================================
FILE: src/ameba/ast/visitors/redundant_control_expression_visitor.cr
================================================
module Ameba::AST
# A class that utilizes a logic to traverse AST nodes and
# fire a source test callback if a redundant `Crystal::ControlExpression`
# is reached.
class RedundantControlExpressionVisitor
# A corresponding rule that uses this visitor.
getter rule : Rule::Base
# A source that needs to be traversed.
getter source : Source
# A node to run traversal on.
getter node : Crystal::ASTNode
def initialize(@rule, @source, @node)
traverse_node node
end
private def traverse_control_expression(node)
@rule.test(@source, node, self)
end
private def traverse_node(node)
case node
when Crystal::ControlExpression then traverse_control_expression node
when Crystal::Expressions then traverse_expressions node
when Crystal::If, Crystal::Unless then traverse_condition node
when Crystal::Case, Crystal::Select then traverse_case node
when Crystal::BinaryOp then traverse_binary_op node
when Crystal::ExceptionHandler then traverse_exception_handler node
end
end
private def traverse_expressions(node)
traverse_node node.expressions.last?
end
private def traverse_condition(node)
return if node.else.nil? || node.else.nop?
traverse_node(node.then)
traverse_node(node.else)
end
private def traverse_case(node)
node.whens.each do |when_node|
traverse_node when_node.body
end
traverse_node(node.else)
end
private def traverse_binary_op(node)
traverse_node(node.right)
end
private def traverse_exception_handler(node)
traverse_node node.body
traverse_node node.else
node.rescues.try &.each do |rescue_node|
traverse_node rescue_node.body
end
end
end
end
================================================
FILE: src/ameba/ast/visitors/scope_calls_with_self_receiver_visitor.cr
================================================
module Ameba::AST
class ScopeCallsWithSelfReceiverVisitor < Crystal::Visitor
@current_scope : AST::Scope
@current_assign : Crystal::ASTNode?
getter scope_call_queue = {} of AST::Scope => Array(Crystal::Call)
def initialize(rule, source)
@current_scope = AST::Scope.new(source.ast) # top level scope
source.ast.accept self
scope_call_queue.each do |scope, calls|
calls.each do |call|
rule.test source, call, scope
end
end
end
private def on_scope_enter(node)
scope = AST::Scope.new(node, @current_scope)
@current_scope = scope
true
end
private def on_scope_end(node)
# go up if this is not a top level scope
if outer_scope = @current_scope.outer_scope
@current_scope = outer_scope
end
end
private def on_assign_end(target, node)
target.is_a?(Crystal::Var) &&
@current_scope.assign_variable(target.name, node)
end
# A main visit method that accepts `Crystal::ASTNode`.
# Returns `true`, meaning all child nodes will be traversed.
def visit(node : Crystal::ASTNode)
true
end
def end_visit(node : Crystal::ASTNode)
on_scope_end(node) if @current_scope.eql?(node)
end
def visit(node : Crystal::Def)
node.name == "->" || on_scope_enter(node)
end
def visit(node : Crystal::Block | Crystal::ProcLiteral)
on_scope_enter(node)
end
def visit(node : Crystal::ClassDef | Crystal::ModuleDef)
on_scope_enter(node)
end
def visit(node : Crystal::Assign | Crystal::OpAssign | Crystal::MultiAssign | Crystal::UninitializedVar)
@current_assign = node
true
end
def end_visit(node : Crystal::Assign | Crystal::OpAssign)
on_assign_end(node.target, node)
@current_assign = nil
end
def end_visit(node : Crystal::MultiAssign)
node.targets.each { |target| on_assign_end(target, node) }
@current_assign = nil
end
def end_visit(node : Crystal::UninitializedVar)
on_assign_end(node.var, node)
@current_assign = nil
end
def visit(node : Crystal::TypeDeclaration)
return unless (var = node.var).is_a?(Crystal::Var)
@current_scope.add_variable(var)
@current_scope.add_type_dec_variable(node)
@current_assign = node.value if node.value
true
end
def end_visit(node : Crystal::TypeDeclaration)
return unless (var = node.var).is_a?(Crystal::Var)
on_assign_end(var, node)
@current_assign = nil
end
def visit(node : Crystal::Arg)
@current_scope.add_argument(node)
true
end
def visit(node : Crystal::InstanceVar)
@current_scope.add_ivariable(node)
true
end
def visit(node : Crystal::Var)
scope = @current_scope
variable = scope.find_variable(node.name)
case
when scope.arg?(node) # node is an argument
scope.add_argument(node)
when variable.nil? && @current_assign # node is a variable
scope.add_variable(node)
when variable # node is a reference
variable.reference(node, scope)
end
true
end
def visit(node : Crystal::Call)
if (obj = node.obj).is_a?(Crystal::Var) && obj.name == "self"
calls = @scope_call_queue[@current_scope] ||= [] of Crystal::Call
calls << node
end
true
end
end
end
================================================
FILE: src/ameba/ast/visitors/scope_visitor.cr
================================================
require "./base_visitor"
module Ameba::AST
# AST Visitor that traverses the source and constructs scopes.
class ScopeVisitor < BaseVisitor
# Non-exhaustive list of nodes to be visited by Ameba's rules.
NODES = {
ClassDef,
ModuleDef,
EnumDef,
LibDef,
FunDef,
TypeDef,
TypeOf,
CStructOrUnionDef,
ProcLiteral,
Block,
Macro,
MacroIf,
MacroFor,
}
SPECIAL_NODE_NAMES = %w[super previous_def]
@scope_queue = [] of Scope
@current_scope : Scope
@current_assign : Crystal::ASTNode?
@current_visibility : Crystal::Visibility?
@skip : Array(Crystal::ASTNode.class)?
def initialize(@rule, @source, skip = nil)
@current_scope = Scope.new(@source.ast) # top level scope
@skip = skip.try &.map(&.as(Crystal::ASTNode.class))
super @rule, @source
if @scope_queue.empty?
@rule.test @source, @current_scope.node, @current_scope
else
@scope_queue.each do |scope|
@rule.test @source, scope.node, scope
end
end
end
private def on_scope_enter(node)
return if skip?(node)
scope = Scope.new(node, @current_scope)
scope.visibility = @current_visibility
@current_scope = scope
true
end
private def on_scope_end(node)
@scope_queue << @current_scope
@current_visibility = nil
# go up if this is not a top level scope
if outer_scope = @current_scope.outer_scope
@current_scope = outer_scope
end
end
private def on_assign_end(target, node)
target.is_a?(Crystal::Var) &&
@current_scope.assign_variable(target.name, node)
end
# :nodoc:
def end_visit(node : Crystal::ASTNode)
on_scope_end(node) if @current_scope.eql?(node)
end
{% for name in NODES %}
# :nodoc:
def visit(node : Crystal::{{ name }})
on_scope_enter(node)
end
{% end %}
# :nodoc:
def visit(node : Crystal::VisibilityModifier)
@current_visibility = node.exp.visibility = node.modifier
true
end
# :nodoc:
def visit(node : Crystal::Yield)
@current_scope.yields = true
true
end
# :nodoc:
def visit(node : Crystal::Def)
node.name == "->" || on_scope_enter(node)
end
# :nodoc:
def visit(node : Crystal::Assign | Crystal::OpAssign)
target = node.target
if isolated_assign_target?(target)
@current_assign = node
target.accept(self)
on_scope_enter(node)
node.value.accept(self)
return false
end
@current_assign = node
true
end
# :nodoc:
def visit(node : Crystal::MultiAssign | Crystal::UninitializedVar)
@current_assign = node
true
end
# :nodoc:
def end_visit(node : Crystal::Assign | Crystal::OpAssign)
on_scope_end(node) if @current_scope.eql?(node)
on_assign_end(node.target, node)
@current_assign = nil
end
# :nodoc:
def end_visit(node : Crystal::MultiAssign)
node.targets.each { |target| on_assign_end(target, node) }
@current_assign = nil
end
# :nodoc:
def end_visit(node : Crystal::UninitializedVar)
on_assign_end(node.var, node)
@current_assign = nil
end
# :nodoc:
def visit(node : Crystal::TypeDeclaration)
return unless (var = node.var).is_a?(Crystal::Var)
@current_scope.add_variable(var)
@current_scope.add_type_dec_variable(node)
@current_assign = node.value if node.value
true
end
# :nodoc:
def end_visit(node : Crystal::TypeDeclaration)
return unless (var = node.var).is_a?(Crystal::Var)
on_assign_end(var, node)
@current_assign = nil
end
# :nodoc:
def visit(node : Crystal::Arg)
@current_scope.add_argument(node)
true
end
# :nodoc:
def visit(node : Crystal::InstanceVar)
@current_scope.add_ivariable(node)
true
end
# :nodoc:
def visit(node : Crystal::Var)
variable = @current_scope.find_variable(node.name)
case
when @current_scope.arg?(node) # node is an argument
@current_scope.add_argument(node)
when variable.nil? && @current_assign # node is a variable
@current_scope.add_variable(node)
when variable # node is a reference
variable.reference(node, @current_scope)
end
true
end
# :nodoc:
def visit(node : Crystal::Call)
scope = @current_scope
case
when (scope.top_level? || scope.type_definition?) && record_macro?(node)
return false
when scope.type_definition? && accessor_macro?(node)
return false
when scope.def? && special_node?(node)
scope.arguments.each do |arg|
ref = arg.variable.reference(scope)
ref.explicit = false
end
end
true
end
private def special_node?(node)
node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
end
private def accessor_macro?(node)
node.name.matches? /^(class_)?(getter[?!]?|setter|property[?!]?)$/
end
private def record_macro?(node)
return false unless node.name == "record"
case node.args.first?
when Crystal::Path, Crystal::Generic
true
else
false
end
end
private def isolated_assign_target?(target)
case target
when Crystal::Path
true
when Crystal::InstanceVar, Crystal::ClassVar
@current_scope.type_definition?
else
false
end
end
private def skip?(node)
!!@skip.try(&.includes?(node.class))
end
end
end
================================================
FILE: src/ameba/ast/visitors/top_level_nodes_visitor.cr
================================================
module Ameba::AST
# AST Visitor that visits certain nodes at a top level, which
# can characterize the source (i.e. require statements, modules etc.)
class TopLevelNodesVisitor < Crystal::Visitor
getter require_nodes = [] of Crystal::Require
# Creates a new instance of visitor
def initialize(node : Crystal::ASTNode)
node.accept self
end
# :nodoc:
def visit(node : Crystal::Require)
require_nodes << node
true
end
# If a top level node is `Crystal::Expressions`,
# then always traverse the children.
def visit(node : Crystal::Expressions)
true
end
# A general visit method for rest of the nodes.
# Returns `false`, meaning all child nodes will not be traversed.
def visit(node : Crystal::ASTNode)
false
end
end
end
================================================
FILE: src/ameba/cli/cmd.cr
================================================
require "option_parser"
require "../../ameba"
# :nodoc:
module Ameba::CLI
extend self
private class Opts
property config : Path?
property version : String?
property formatter : Symbol | String?
property root = Path[Dir.current]
property globs : Set(String)?
property excluded : Set(String)?
property only : Set(String)?
property except : Set(String)?
property describe_rule : String?
property location_to_explain : Crystal::Location?
property severity : Severity?
property stdin_filename : String?
property? skip_reading_config = false
property? rules = false
property? rule_versions = false
property? all = false
property? colors = true
property? without_affected_code = false
property? autocorrect = false
end
private class ExitException < Exception
getter code : Int32
def initialize(@code = 0)
super("Exit with code #{code}")
end
end
def run(args = ARGV, output : IO = STDOUT) : Bool
safe_colorize_toggle do
opts = parse_args(args, output: output)
Colorize.enabled = opts.colors?
if (location_to_explain = opts.location_to_explain) && opts.autocorrect?
raise "Invalid usage: Cannot explain an issue and autocorrect at the same time."
end
if opts.stdin_filename && opts.autocorrect?
raise "Invalid usage: Cannot autocorrect from stdin."
end
config = config_from_opts(opts)
if opts.rules?
print_rules(config.rules, output)
return true
end
if opts.rule_versions?
print_rule_versions(config.rules, output)
return true
end
if describe_rule_name = opts.describe_rule
unless rule = config.rules.find(&.name.== describe_rule_name)
raise "Rule `#{describe_rule_name}` does not exist"
end
describe_rule(rule, output)
return true
end
runner = Ameba.run(config)
if location_to_explain
runner.explain(location_to_explain, output)
return true
end
runner.success?
end
rescue ex : ExitException
ex.code.zero?
end
private def safe_colorize_toggle(&)
prev_colorize_enabled = Colorize.enabled?
begin
yield
ensure
Colorize.enabled = prev_colorize_enabled
end
end
# ameba:disable Metrics/CyclomaticComplexity
def parse_args(args, opts = Opts.new, output : IO = STDOUT)
OptionParser.parse(args) do |parser|
parser.banner = "Usage: ameba [options] [file1 file2 ...]"
parser.unknown_args do |arr|
case
when arr.size == 1 && arr.first == "-"
opts.stdin_filename = arr.first
when arr.size == 1 && arr.first.matches?(/.+:\d+:\d+/)
configure_explain_opts(arr.first, opts)
else
configure_globs(arr, opts) if arr.present?
end
end
parser.on("-v", "--version", "Print version") do
print_version(output)
raise ExitException.new
end
parser.on("-h", "--help", "Show this help") do
print_help(parser, output)
raise ExitException.new
end
parser.on("-r", "--rules", "Show all available rules") do
opts.rules = true
end
parser.on("-R", "--rule-versions", "Show all available rule versions") do
opts.rule_versions = true
end
parser.on("-s", "--silent", "Disable output") do
opts.formatter = :silent
end
parser.on("-c", "--config PATH", "Specify a configuration file") do |path|
opts.config = Path[path] if path.presence
end
parser.on("-u", "--up-to-version VERSION", "Choose a version") do |version|
opts.version = version if version.presence
end
parser.on("-f", "--format FORMATTER",
"Choose an output formatter: #{Config.formatter_names}") do |formatter|
opts.formatter = formatter if formatter.presence
end
parser.on("--only RULE1,RULE2,...",
"Run only given rules (or groups)") do |rules|
opts.only = rules.split(',').to_set if rules.presence
end
parser.on("--except RULE1,RULE2,...",
"Disable the given rules (or groups)") do |rules|
opts.except = rules.split(',').to_set if rules.presence
end
parser.on("--all", "Enable all available rules") do
opts.all = true
end
parser.on("--fix", "Autocorrect issues") do
opts.autocorrect = true
end
parser.on("--gen-config",
"Generate a configuration file acting as a TODO list") do
opts.formatter = :todo
opts.skip_reading_config = true
end
parser.on("--min-severity SEVERITY",
"Minimum severity of issues to report (default: #{Rule::Base.default_severity})") do |level|
opts.severity = Severity.parse(level) if level.presence
end
parser.on("-e", "--explain PATH:line:column",
"Explain an issue at a specified location") do |loc|
configure_explain_opts(loc, opts)
end
parser.on("-d", "--describe Category/Rule",
"Describe a rule with specified name") do |rule_name|
configure_describe_opts(rule_name, opts)
end
parser.on("--without-affected-code",
"Stop showing affected code while using a default formatter") do
opts.without_affected_code = true
end
parser.on("--no-color", "Disable colors") do
opts.colors = false
end
parser.on("--stdin-filename FILENAME", "Read source from STDIN") do |filename|
opts.stdin_filename = filename if filename.presence
end
end
opts
end
private def config_from_opts(opts)
config = Config.load(
root: opts.root,
path: opts.config,
skip_reading_config: opts.skip_reading_config?,
)
config.autocorrect = opts.autocorrect?
config.stdin_filename = opts.stdin_filename
if version = opts.version
config.version = version
end
if globs = opts.globs
config.globs = globs
end
if excluded = opts.excluded
config.excluded += excluded
end
if severity = opts.severity
config.severity = severity
end
configure_formatter(config, opts)
configure_rules(config, opts)
config
end
private def configure_globs(args, opts) : Nil
excluded, globs =
args.partition(&.starts_with?('!'))
if root = root_path_from_globs(globs)
opts.root = root
end
if globs.present?
opts.globs = globs
.map! { |path| path_to_glob(path) }
.to_set
end
if excluded.present?
opts.excluded = excluded
.map! { |path| path_to_glob(path.lchop) }
.to_set
end
end
private def path_to_glob(path : String) : String
Path[path]
.expand(home: true)
.to_posix
.to_s
end
private def root_path_from_globs(globs) : Path?
dynasty =
case
when path = find_as_path(globs, &->File.directory?(String))
path.parents + [path]
when path = find_as_path(globs, &->File.file?(String))
path.parents
end
dynasty
.try &.reverse!
.find(&->root_path?(Path))
.try(&.expand(home: true))
end
private def find_as_path(globs, &) : Path?
globs
.find { |glob| yield glob }
.try(&->Path.new(String))
end
private def root_path?(path : Path) : Bool
File.exists?(path / Config::Loader::FILENAME) ||
File.exists?(path / "shard.yml")
end
private def configure_rules(config, opts) : Nil
case
when only = opts.only
config.rules.each(&.enabled = false)
config.update_rules(only, enabled: true)
# We need to clear the version to ensure that the selected rules
# are not affected by the version constraint
config.version = nil
when opts.all?
config.rules.each(&.enabled = true)
end
if except = opts.except
config.update_rules(except, enabled: false)
end
end
private def configure_formatter(config, opts) : Nil
if name = opts.formatter
config.formatter = name
end
config.formatter.config[:autocorrect] = opts.autocorrect?
config.formatter.config[:without_affected_code] =
opts.without_affected_code?
end
private def configure_describe_opts(rule_name, opts) : Nil
opts.describe_rule = rule_name.presence
opts.formatter = :silent
end
private def configure_explain_opts(loc, opts) : Nil
location_to_explain = parse_explain_location(loc)
filename = location_to_explain.original_filename
return unless filename
opts.location_to_explain = location_to_explain
opts.globs = Set{path_to_glob(filename)}
opts.formatter = :silent
end
private def parse_explain_location(arg)
Crystal::Location.parse(arg)
rescue
raise "location should have PATH:line:column format"
end
private def print_version(output)
output.puts Ameba.version
end
private def print_help(parser, output)
output.puts parser
end
private def describe_rule(rule, output)
Presenter::RulePresenter.new(output).run(rule)
end
private def print_rules(rules, output)
Presenter::RuleCollectionPresenter.new(output).run(rules)
end
private def print_rule_versions(rules, output)
Presenter::RuleVersionsPresenter.new(output).run(rules)
end
end
================================================
FILE: src/ameba/config/loader.cr
================================================
class Ameba::Config
# By default config loads `.ameba.yml` file located in a current
# working directory.
#
# If it cannot be found until reaching the root directory, then it will be
# searched for in the user’s global config locations, which consists of a
# dotfile or a config file inside the XDG Base Directory specification.
#
# - `~/.ameba.yml`
# - `$XDG_CONFIG_HOME/ameba/config.yml` (expands to `~/.config/ameba/config.yml`
# if `$XDG_CONFIG_HOME` is not set)
#
# If both files exist, the dotfile will be selected.
#
# As an example, if Ameba is invoked from inside `/path/to/project/lib/utils`,
# then it will use the config as specified inside the first of the following files:
#
# - `/path/to/project/lib/utils/.ameba.yml`
# - `/path/to/project/lib/.ameba.yml`
# - `/path/to/project/.ameba.yml`
# - `/path/to/.ameba.yml`
# - `/path/.ameba.yml`
# - `/.ameba.yml`
# - `~/.ameba.yml`
# - `~/.config/ameba/config.yml`
module Loader
extend self
XDG_CONFIG_HOME = ENV.fetch("XDG_CONFIG_HOME", "~/.config")
FILENAME = ".ameba.yml"
DEFAULT_PATH = Path[Dir.current] / FILENAME
DEFAULT_PATHS = {
Path["~"] / FILENAME,
Path[XDG_CONFIG_HOME] / "ameba" / "config.yml",
}
# Creates a new instance of `Ameba::Config` based on YAML parameters.
#
# `Config.load` uses this constructor to instantiate new config by YAML file.
protected def from_yaml(config : YAML::Any, root = nil)
config = YAML.parse("{}") if config.raw.nil?
config.raw.is_a?(Hash) ||
raise "Invalid config file format"
rules = Rule.rules.map &.new(config).as(Rule::Base)
new(
rules: rules,
root: root,
excluded: load_array_section(config, "Excluded", DEFAULT_EXCLUDED.dup).to_set,
globs: load_array_section(config, "Globs", DEFAULT_GLOBS.dup).to_set,
version: load_string_key(config, "Version"),
formatter: load_string_key(config, "Formatter", "Name"),
)
end
# Loads YAML configuration file by `path`.
#
# ```
# config = Ameba::Config.load
# ```
def load(path : Path | String? = nil, root : Path? = nil, skip_reading_config : Bool = false)
unless skip_reading_config
content = begin
if path
read_config(path: path)
else
read_config(root: root)
end
end
end
content ||= "{}"
from_yaml YAML.parse(content), root
rescue ex
raise "Unable to load config file: #{ex.message}"
end
protected def read_config(*, path : Path | String)
unless File.exists?(path)
raise "Config file #{path.to_s.inspect} does not exist"
end
File.read(path)
end
protected def read_config(*, root : Path?)
path = root ? root / FILENAME : DEFAULT_PATH
if config_path = find_config_path(path)
return File.read(config_path)
end
end
protected def find_config_path(path : Path)
path.parents.reverse_each do |search_path|
config_path =
search_path / FILENAME
return config_path if File.exists?(config_path)
end
DEFAULT_PATHS.each do |default_path|
return default_path if File.exists?(default_path)
end
end
private def load_string_key(config, *path)
config.dig?(*path).try(&.as_s).presence
end
private def load_array_section(config, section_name, default = [] of String)
case value = config[section_name]?
when .nil? then default
when .as_s? then [value.as_s]
when .as_a? then value.as_a.map(&.as_s)
else
raise "Incorrect `#{section_name}` section in a config files"
end
end
end
end
================================================
FILE: src/ameba/config/rule_config.cr
================================================
class Ameba::Config
# :nodoc:
module RuleConfig
# Define rule properties
macro properties(&block)
{% definitions = [] of NamedTuple %}
{% if (prop = block.body).is_a? Call %}
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% else %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% end %}
{% elsif block.body.is_a? Expressions %}
{% for prop in block.body.expressions %}
{% if prop.is_a? Call %}
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% else %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% end %}
{% end %}
{% end %}
{% end %}
{% properties = {} of MacroId => NamedTuple %}
{% for df in definitions %}
{% name = df[:var].id %}
{% key = name.camelcase.stringify %}
{% value = df[:value] %}
{% type = df[:type] %}
{% converter = nil %}
{% if key == "Severity" %}
{% type = Severity %}
{% converter = SeverityYamlConverter %}
{% end %}
{% unless type %}
{% if value.is_a?(BoolLiteral) %}
{% type = Bool %}
{% elsif value.is_a?(StringLiteral) || value.is_a?(StringInterpolation) %}
{% type = String %}
{% elsif value.is_a?(NumberLiteral) %}
{% if value.kind == :i32 %}
{% type = Int32 %}
{% elsif value.kind == :i64 %}
{% type = Int64 %}
{% elsif value.kind == :i128 %}
{% type = Int128 %}
{% elsif value.kind == :f32 %}
{% type = Float32 %}
{% elsif value.kind == :f64 %}
{% type = Float64 %}
{% end %}
{% end %}
{% end %}
{% properties[name] = {key: key, default: value, type: type, converter: converter} %}
@[YAML::Field(key: {{ key }}, converter: {{ converter }})]
{% if type == Bool %}
property? {{ name }}{{ " : #{type}".id if type }} = {{ value }}
{% else %}
property {{ name }}{{ " : #{type}".id if type }} = {{ value }}
{% end %}
{% end %}
{% unless properties["enabled".id] %}
@[YAML::Field(key: "Enabled")]
property? enabled = true
{% end %}
{% unless properties["severity".id] %}
@[YAML::Field(key: "Severity", converter: Ameba::SeverityYamlConverter)]
property severity = {{ @type }}.default_severity
{% end %}
{% unless properties["excluded".id] %}
@[YAML::Field(key: "Excluded")]
property excluded : Set(String)?
{% end %}
{% unless properties["since_version".id] %}
@[YAML::Field(key: "SinceVersion")]
property since_version : String?
{% end %}
def since_version : SemanticVersion?
if version = @since_version
SemanticVersion.parse(version)
end
end
def self.to_json_schema(builder : JSON::Builder) : Nil
builder.string(rule_name)
builder.object do
builder.field("$ref", "#/$defs/BaseRule")
builder.field("$comment", documentation_url)
builder.field("title", rule_name)
{% if description = properties["description".id] %}
builder.field("description", {{ description[:default] }})
{% end %}
{%
serializable_props =
properties.to_a.reject { |(key, _)| key == "description" }
%}
builder.string("properties")
builder.object do
{% for prop in serializable_props %}
{% default_set = false %}
{% prop_name, prop = prop %}
{% prop_stringified = prop[:type].stringify %}
builder.string({{ prop[:key] }})
builder.object do
{% if prop[:type] == Bool %}
builder.field("type", "boolean")
{% elsif prop[:type] == String %}
builder.field("type", "string")
{% elsif prop_stringified == "::Union(String, ::Nil)" %}
builder.string("type")
builder.array do
builder.string("string")
builder.string("null")
end
{% elsif prop_stringified =~ /^(Int|Float)\d+$/ %}
builder.field("type", "number")
{% elsif prop_stringified =~ /^::Union\((Int|Float)\d+, ::Nil\)$/ %}
builder.string("type")
builder.array do
builder.string("number")
builder.string("null")
end
{% elsif prop[:default].is_a?(ArrayLiteral) %}
builder.field("type", "array")
builder.string("items")
builder.object do
# TODO: Implement type validation for array items
builder.field("type", "string")
end
{% elsif prop[:default].is_a?(HashLiteral) %}
builder.field("type", "object")
builder.string("properties")
builder.object do
{% for pr in prop[:default] %}
builder.string({{ pr }})
builder.object do
# TODO: Implement type validation for object properties
builder.field("type", "string")
builder.field("default", {{ prop[:default][pr] }})
end
{% end %}
end
{% default_set = true %}
{% elsif prop[:type] == Severity %}
builder.field("$ref", "#/$defs/Severity")
builder.field("default", {{ prop[:default].capitalize }})
{% default_set = true %}
{% else %}
{% raise "Unhandled schema type for #{prop}" %}
{% end %}
{% unless default_set %}
builder.field("default", {{ prop[:default] }})
{% end %}
end
{% end %}
{% unless properties["severity".id] %}
unless default_severity == Rule::Base.default_severity
builder.string("Severity")
builder.object do
builder.field("$ref", "#/$defs/Severity")
builder.field("default", default_severity.to_s)
end
end
{% end %}
end
end
end
end
macro included
GROUP_SEVERITY = {
Lint: Ameba::Severity::Warning,
Metrics: Ameba::Severity::Warning,
Performance: Ameba::Severity::Warning,
}
class_getter default_severity : Ameba::Severity do
GROUP_SEVERITY[group_name]? || Ameba::Severity::Convention
end
macro inherited
include YAML::Serializable
include YAML::Serializable::Strict
def self.new(config = nil)
if (raw = config.try &.raw).is_a?(Hash)
yaml = raw[rule_name]?.try &.to_yaml
end
from_yaml yaml || "{}"
end
end
end
end
end
================================================
FILE: src/ameba/config.cr
================================================
require "semantic_version"
require "yaml"
require "ecr/processor"
require "./glob_utils"
require "./config/*"
# A configuration entry for `Ameba::Runner`.
#
# Config can be loaded from configuration YAML file and adjusted.
#
# ```
# config = Ameba::Config.load
# config.formatter = my_formatter
# ```
class Ameba::Config
extend Loader
include GlobUtils
DEFAULT_EXCLUDED = Set{"lib"}
DEFAULT_GLOBS = Set{"**/*.{cr,ecr}"}
AVAILABLE_FORMATTERS = {
progress: Formatter::DotFormatter,
todo: Formatter::TODOFormatter,
flycheck: Formatter::FlycheckFormatter,
silent: Formatter::BaseFormatter,
disabled: Formatter::DisabledFormatter,
json: Formatter::JSONFormatter,
"github-actions": Formatter::GitHubActionsFormatter,
}
# Returns available formatter names joined by *separator*.
def self.formatter_names(separator = '|')
AVAILABLE_FORMATTERS.keys.join(separator)
end
# Returns an array of configured rules.
getter rules : Array(Rule::Base)
# Returns minimum reported severity.
property severity : Severity = :convention
# Returns a root directory to be used by `Ameba::Runner`.
property root : Path { Path[Dir.current] }
# Returns an ameba version to be used by `Ameba::Runner`.
property version : SemanticVersion?
# Sets version from string.
#
# ```
# config = Ameba::Config.load
# config.version = "1.6.0"
# ```
def version=(version : String)
@version = SemanticVersion.parse(version)
end
# Returns a formatter to be used while inspecting files.
# If formatter is not set, it will return default formatter.
#
# ```
# config = Ameba::Config.load
# config.formatter = custom_formatter
# config.formatter
# ```
property formatter : Formatter::BaseFormatter do
Formatter::DotFormatter.new
end
# Sets formatter by name.
#
# ```
# config = Ameba::Config.load
# config.formatter = :progress
# ```
def formatter=(name : String | Symbol)
unless formatter = AVAILABLE_FORMATTERS[name]?
raise "Unknown formatter `#{name}`. Use one of #{Config.formatter_names}."
end
@formatter = formatter.new
end
# Returns a list of paths (with wildcards) to files.
# Represents a list of sources to be inspected.
# If globs are not set, it will return default list of files.
#
# ```
# config = Ameba::Config.load
# config.globs = Set{"**/*.cr"}
# config.globs
# ```
property globs : Set(String)
# Represents a list of paths to exclude from globs.
# Can have wildcards.
#
# ```
# config = Ameba::Config.load
# config.excluded = Set{"spec", "src/server/*.cr"}
# ```
property excluded : Set(String)
# Returns `true` if correctable issues should be autocorrected.
property? autocorrect = false
# Returns a filename if reading source file from STDIN.
property stdin_filename : String?
# Returns rules grouped by rule group.
protected getter rule_groups : Hash(String, Array(Rule::Base))
protected def initialize(
*,
@rules = [] of Rule::Base,
@severity : Severity = :convention,
@root = nil,
@globs = Set(String).new,
@excluded = Set(String).new,
@autocorrect = false,
@stdin_filename = nil,
version = nil,
formatter = nil,
)
@rule_groups = @rules.group_by &.group
if version
self.version = version
end
if formatter
self.formatter = formatter
end
end
# Returns a list of sources matching globs and excluded sections.
#
# ```
# config = Ameba::Config.load
# config.sources # => list of default sources
# config.globs = Set{"**/*.cr", "**/*.ecr"}
# config.excluded = Set{"spec"}
# config.sources # => list of sources pointing to files found by the wildcards
# ```
def sources
if file = stdin_filename
[Source.new(STDIN.gets_to_end, file)]
else
files.map do |path|
Source.new(File.read(path), path)
end
end
end
# Returns a list of files matching globs and excluded sections.
#
# ```
# config = Ameba::Config.load
# config.files # => list of default files
# config.globs = Set{"**/*.cr", "**/*.ecr"}
# config.excluded = Set{"spec"}
# config.files # => list of files found by the wildcards
# ```
def files
find_files_by_globs(globs, root) - find_files_by_globs(excluded, root)
end
# Updates rule properties.
#
# ```
# config = Ameba::Config.load
# config.update_rule "MyRuleName", enabled: false
# ```
def update_rule(name, enabled = true, excluded = nil)
rule = @rules.find(&.name.==(name))
raise ArgumentError.new("Rule `#{name}` does not exist") unless rule
rule
.tap(&.enabled = enabled)
.tap(&.excluded = excluded.try &.to_set)
end
# Updates rules properties.
#
# ```
# config = Ameba::Config.load
# config.update_rules %w[Rule1 Rule2], enabled: true
# ```
#
# also it allows to update groups of rules:
#
# ```
# config.update_rules %w[Group1 Group2], enabled: true
# ```
def update_rules(names : Enumerable(String), enabled = true, excluded = nil)
excluded = excluded.try &.to_set
names.each do |name|
if rules = @rule_groups[name]?
rules.each do |rule|
rule.enabled = enabled
rule.excluded = excluded
end
else
update_rule name, enabled, excluded
end
end
end
end
================================================
FILE: src/ameba/ext/location.cr
================================================
# Extensions to `Crystal::Location`
module Ameba::Ext::Location
# Returns `self` relative to the given *base* directory.
def relative(base = Dir.current) : self
return self unless path = original_filename
path =
Path[path].relative_to(base).to_s
self.class.new(path, @line_number, @column_number)
end
# Returns `true` if the line numbers of `self` and *other* are the same.
def same_line?(other : self?) : Bool
!!other &&
@line_number == other.line_number
end
# Returns the same location as this location but with the line and/or
# column number(s) changed to the given value(s).
def with(line_number = @line_number, column_number = @column_number) : self
self.class.new(@filename, line_number, column_number)
end
# Returns the same location as this location but with the line and/or
# column number(s) adjusted by the given amount(s).
def adjust(line_number = 0, column_number = 0) : self
self.class.new(@filename, @line_number + line_number, @column_number + column_number)
end
# Seeks to a given *offset* relative to `self`.
def seek(offset : self) : self
if offset.filename.as?(String).presence && @filename != offset.filename
raise ArgumentError.new <<-MSG
Mismatching filenames:
#{@filename}
#{offset.filename}
MSG
end
if offset.line_number == 1
self.class.new(@filename, @line_number, @column_number + offset.column_number - 1)
else
self.class.new(@filename, @line_number + offset.line_number - 1, offset.column_number)
end
end
end
class Crystal::Location
include Ameba::Ext::Location
end
================================================
FILE: src/ameba/formatter/base_formatter.cr
================================================
require "./util"
# A module that utilizes Ameba's formatters.
module Ameba::Formatter
# A base formatter for all formatters. It uses `output` IO
# to report results and also implements stub methods for
# callbacks in `Ameba::Runner#run` method.
class BaseFormatter
# TODO: allow other IOs
getter output : IO::FileDescriptor | IO::Memory
getter config = {} of Symbol => String | Bool
def initialize(@output = STDOUT)
end
# Callback that indicates when inspecting is started.
# A list of sources to inspect is passed as an argument.
def started(sources) : Nil; end
# Callback that indicates when source inspection is started.
# A corresponding source is passed as an argument.
#
# WARNING: This method needs to be MT safe
def source_started(source : Source) : Nil; end
# Callback that indicates when source inspection is finished.
# A corresponding source is passed as an argument.
#
# WARNING: This method needs to be MT safe
def source_finished(source : Source) : Nil; end
# Callback that indicates when inspection is finished.
# A list of inspected sources is passed as an argument.
def finished(sources) : Nil; end
end
end
================================================
FILE: src/ameba/formatter/disabled_formatter.cr
================================================
module Ameba::Formatter
# A formatter that shows all disabled lines by inline directives.
class DisabledFormatter < BaseFormatter
def finished(sources) : Nil
output << "Disabled rules using inline directives:\n\n"
sources.each do |source|
source.issues.each do |issue|
next unless issue.disabled?
next unless loc = issue.location
output << "#{source.path}:#{loc.line_number}".colorize(:cyan)
output << " #{issue.rule.name}\n"
end
end
end
end
end
================================================
FILE: src/ameba/formatter/dot_formatter.cr
================================================
require "./util"
module Ameba::Formatter
# A formatter that shows a progress of inspection in a terminal using dots.
# It is similar to Crystal's dot formatter for specs.
class DotFormatter < BaseFormatter
include Util
@started_at : Time::Instant?
@mutex = Mutex.new
# Reports a message when inspection is started.
def started(sources) : Nil
@started_at = Time.instant
output.puts started_message(sources.size)
output.puts
end
# Reports a result of the inspection of a corresponding source.
def source_finished(source : Source) : Nil
sym = source.valid? ? ".".colorize(:green) : "F".colorize(:red)
@mutex.synchronize { output << sym }
end
# Reports a message when inspection is finished.
def finished(sources) : Nil
output.flush
output << "\n\n"
show_affected_code = !config[:without_affected_code]?
failed_sources = sources.reject &.valid?
failed_sources.each do |source|
source.issues.each do |issue|
next if issue.disabled?
next if (location = issue.location).nil?
output.print location.colorize(:cyan)
if issue.correctable?
if config[:autocorrect]?
output.print " [Corrected]".colorize(:green)
else
output.print " [Correctable]".colorize(:yellow)
end
end
output.puts
output.puts ("[%s] %s: %s" % {
issue.rule.severity.symbol,
issue.rule.name,
issue.message,
}).colorize(issue.rule.severity.color)
if show_affected_code && (code = affected_code(issue))
output << code.colorize(:default)
end
output.puts
end
end
output.puts finished_in_message(@started_at)
output.puts final_message(sources, failed_sources)
end
private def started_message(size)
"Inspecting #{size} #{pluralize(size, "file")}"
end
private def finished_in_message(started)
return unless started
"Finished in #{to_human(started.elapsed)}".colorize(:default)
end
private def final_message(sources, failed_sources)
total = sources.size
failures = failed_sources.sum(&.issues.count(&.enabled?))
color = failures == 0 ? :green : :red
"#{total} inspected, #{failures} #{pluralize(failures, "failure")}".colorize(color)
end
end
end
================================================
FILE: src/ameba/formatter/explain_formatter.cr
================================================
require "./util"
module Ameba::Formatter
# A formatter that shows the detailed explanation of the issue at
# a specific location.
class ExplainFormatter
include Util
getter output : IO::FileDescriptor | IO::Memory
getter location : Crystal::Location
# Creates a new instance of `ExplainFormatter`.
#
# Accepts *output* which indicates the io where the explanation will be written to.
# Second argument is *location* which indicates the location to explain.
#
# ```
# ExplainFormatter.new output, {
# file: path,
# line: line_number,
# column: column_number,
# }
# ```
def initialize(@output, @location)
end
# Reports the explanations at the *@location*.
def finished(sources) : Nil
source = sources.find(&.path.==(@location.filename))
return unless source
issue = source.issues.find(&.location.==(@location))
return unless issue
explain(source, issue)
end
private def explain(source, issue) : Nil
return unless location = issue.location
output << '\n'
output_title "Issue info"
output_paragraph [
issue.message.colorize(:red),
location.to_s.colorize(:cyan),
]
if affected_code = affected_code(issue, context_lines: 3)
output_title "Affected code"
output_paragraph affected_code
end
rule = issue.rule
output_title "Rule info"
output_paragraph "%s of a %s severity" % {
rule.name.colorize(:magenta),
rule.severity.to_s.colorize(rule.severity.color),
}
if rule_description = rule.description
output_paragraph colorize_markdown(rule_description)
end
if rule_doc = rule.class.parsed_doc
output_title "Detailed description"
output_paragraph colorize_markdown(rule_doc)
end
end
private def output_title(title)
output << "### ".colorize(:yellow)
output << title.upcase.colorize(:yellow)
output << "\n\n"
end
private def output_paragraph(paragraph : String)
output_paragraph(paragraph.lines)
end
private def output_paragraph(paragraph : Array)
paragraph.each do |line|
output << " " << line << '\n'
end
output << '\n'
end
end
end
================================================
FILE: src/ameba/formatter/flycheck_formatter.cr
================================================
module Ameba::Formatter
class FlycheckFormatter < BaseFormatter
@mutex = Mutex.new
def source_finished(source : Source) : Nil
source.issues.each do |issue|
next if issue.disabled?
next if issue.correctable? && config[:autocorrect]?
next unless loc = issue.location
@mutex.synchronize do
output.printf "%s:%d:%d: %s: [%s] %s\n",
source.path, loc.line_number, loc.column_number, issue.rule.severity.symbol,
issue.rule.name, issue.message.gsub('\n', " ")
end
end
end
end
end
================================================
FILE: src/ameba/formatter/github_actions_formatter.cr
================================================
require "./util"
module Ameba::Formatter
# A formatter that outputs issues in a GitHub Actions compatible format.
#
# See [GitHub Actions documentation](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions) for details.
class GitHubActionsFormatter < BaseFormatter
include Util
@started_at : Time::Instant?
@mutex = Mutex.new
# Reports a message when inspection is started.
def started(sources) : Nil
@started_at = Time.instant
end
# Reports a result of the inspection of a corresponding source.
def source_finished(source : Source) : Nil
source.issues.each do |issue|
next if issue.disabled?
@mutex.synchronize do
output << "::"
output << command_name(issue.rule.severity)
output << " "
output << "file="
output << escape_property(source.path)
if location = issue.location
output << ",line="
output << location.line_number
output << ",col="
output << location.column_number
end
if end_location = issue.end_location
output << ",endLine="
output << end_location.line_number
output << ",endColumn="
output << end_location.column_number
end
output << ",title="
output << escape_property(issue.rule.name)
output << "::"
output << escape_data(issue.message)
output << "\n"
end
end
end
# Reports a message when inspection is finished.
def finished(sources) : Nil
return unless step_summary_file = ENV["GITHUB_STEP_SUMMARY"]?
time_elapsed =
@started_at.try(&.elapsed)
File.write(step_summary_file, summary(sources, time_elapsed))
end
private def summary(sources, time_elapsed)
failed_sources = sources.reject(&.valid?)
total = sources.size
failures = failed_sources.sum(&.issues.count(&.enabled?))
String.build do |output|
output << "## Ameba Results %s\n\n" % {
failures == 0 ? ":green_heart:" : ":bug:",
}
if failures.positive?
output << "### Issues found\n\n"
failed_sources.each do |source|
issues = source.issues.select(&.enabled?)
if issues.present?
output << "#### `%s` (**%d** %s)\n\n" % {
source.path,
issues.size,
pluralize(issues.size, "issue"),
}
output.puts "| Line | Severity | Name | Message |"
output.puts "| ---- | -------- | ---- | ------- |"
issues.each do |issue|
output.puts "| %s | %s | %s | %s |" % {
issue_location_value(issue) || "-",
issue.rule.severity,
"[%s](%s)" % {
issue.rule.name,
issue.rule.class.documentation_url,
},
issue.message.gsub('|', "\\|"),
}
end
output << "\n"
end
end
output << "\n"
end
if time_elapsed
output.puts "Finished in %s." % to_human(time_elapsed)
end
output.puts "**%d** sources inspected, **%d** %s." % {
total,
failures,
pluralize(failures, "failure"),
}
output.puts
output.puts "> Ameba version: **%s**" % Ameba.version
output.puts "> Crystal version: **%s**" % Crystal::VERSION
end
end
private BLOB_URL = begin
repo = ENV["GITHUB_REPOSITORY"]?
sha = ENV["GITHUB_SHA"]?
if repo && sha
"https://github.com/#{repo}/blob/#{sha}"
end
end
private def issue_location_value(issue)
location, end_location =
issue.location, issue.end_location
return unless location
line_selector =
if end_location && !location.same_line?(end_location)
"#{location.line_number}-#{end_location.line_number}"
else
"#{location.line_number}"
end
if BLOB_URL
location_url = "[%s](%s/%s#%s)" % {
line_selector,
BLOB_URL,
location.filename,
line_selector
.split('-')
.join('-') { |i| "L#{i}" },
}
end
location_url || line_selector
end
private def command_name(severity : Severity) : String
case severity
in .error? then "error"
in .warning? then "warning"
in .convention? then "notice"
end
end
# See for details:
# - https://github.com/actions/toolkit/blob/74906bea83a0dbf6aaba2d00b732deb0c3aefd2d/packages/core/src/command.ts#L92-L97
# - https://github.com/actions/toolkit/issues/193
private def escape_data(string : String) : String
string
.gsub('%', "%25")
.gsub('\r', "%0D")
.gsub('\n', "%0A")
end
# See for details:
# - https://github.com/actions/toolkit/blob/74906bea83a0dbf6aaba2d00b732deb0c3aefd2d/packages/core/src/command.ts#L99-L106
private def escape_property(string : String) : String
string
.gsub('%', "%25")
.gsub('\r', "%0D")
.gsub('\n', "%0A")
.gsub(':', "%3A")
.gsub(',', "%2C")
end
end
end
================================================
FILE: src/ameba/formatter/json_formatter.cr
================================================
require "json"
module Ameba::Formatter
# A formatter that produces the result in a json format.
#
# Example:
#
# ```json
# {
# "metadata": {
# "ameba_version": "x.x.x",
# "crystal_version": "x.x.x",
# },
# "sources": [
# {
# "issues": [
# {
# "location": {
# "column": 7,
# "line": 17,
# },
# "end_location": {
# "column": 20,
# "line": 17,
# },
# "message": "Useless assignment to variable `a`",
# "rule_name": "UselessAssign",
# "severity": "Convention",
# },
# {
# "location": {
# "column": 7,
# "line": 18,
# },
# "end_location": {
# "column": 8,
# "line": 18,
# },
# "message": "Useless assignment to variable `a`",
# "rule_name": "UselessAssign",
# },
# {
# "location": {
# "column": 7,
# "line": 19,
# },
# "end_location": {
# "column": 9,
# "line": 19,
# },
# "message": "Useless assignment to variable `a`",
# "rule_name": "UselessAssign",
# "severity": "Convention",
# },
# ],
# "path": "src/ameba/formatter/json_formatter.cr",
# },
# ],
# "summary": {
# "issues_count": 3,
# "target_sources_count": 1,
# }
# }
# ```
class JSONFormatter < BaseFormatter
@result = AsJSON::Result.new
@mutex = Mutex.new
def started(sources) : Nil
@result.summary.target_sources_count = sources.size
end
def source_finished(source : Source) : Nil
json_source = AsJSON::Source.new(source.path)
source.issues.each do |issue|
next if issue.disabled?
next if issue.correctable? && config[:autocorrect]?
json_source.issues << AsJSON::Issue.new(
issue.rule.name,
issue.rule.severity.to_s,
issue.location,
issue.end_location,
issue.message
)
end
return if json_source.issues.empty?
@mutex.synchronize do
@result.summary.issues_count += json_source.issues.size
@result.sources << json_source
end
end
def finished(sources) : Nil
@result.to_pretty_json @output
end
end
private module AsJSON
record Result,
sources = [] of Source,
metadata = Metadata.new,
summary = Summary.new do
include JSON::Serializable
end
record Source,
path : String,
issues = [] of Issue do
include JSON::Serializable
end
record Issue,
rule_name : String,
severity : String,
location : Crystal::Location?,
end_location : Crystal::Location?,
message : String do
def to_json(json)
{
rule_name: rule_name,
severity: severity,
message: message,
location: {
line: location.try &.line_number,
column: location.try &.column_number,
},
end_location: {
line: end_location.try &.line_number,
column: end_location.try &.column_number,
},
}.to_json(json)
end
end
record Metadata,
ameba_version : String = Ameba.version.to_s,
crystal_version : String = Crystal::VERSION do
include JSON::Serializable
end
class Summary
include JSON::Serializable
property target_sources_count = 0
property issues_count = 0
def initialize(@target_sources_count = 0, @issues_count = 0)
end
end
end
end
================================================
FILE: src/ameba/formatter/todo_formatter.cr
================================================
module Ameba::Formatter
# A formatter that creates a TODO config.
#
# Basically, it takes all issues reported and disables corresponding rules
# or excludes failed sources from these rules.
class TODOFormatter < DotFormatter
getter config_path : Path
def initialize(@output = STDOUT, @config_path = Config::Loader::DEFAULT_PATH)
end
def finished(sources) : Nil
super
issues = sources.flat_map(&.issues)
if issues.none?(&.enabled?)
output.puts "No issues found. File is not generated."
return
end
if issues.any?(&.syntax?)
output.puts "Unable to generate TODO file. Please fix syntax issues."
return
end
issues.sort_by! do |issue|
location = issue.location
{
issue.rule.name,
location.try(&.filename).to_s,
location.try(&.line_number) || 0,
location.try(&.column_number) || 0,
}
end
generate_todo_config(issues)
output.puts "Created #{config_path}"
end
private def generate_todo_config(issues) : Nil
File.open(config_path, mode: "w") do |file|
file.puts header
YAML::Builder.build(file) do |builder|
builder.stream do
builder.document(implicit_start_indicator: true) do
build_yaml(file, builder, issues)
end
end
end
end
end
private def build_yaml(io, builder, issues)
builder.mapping do
rule_issues_map(issues).each do |rule, rule_issues|
builder.flush
io.puts
rule_to_yaml(builder, rule, rule_issues)
end
end
end
private def rule_issues_map(issues)
Hash(Rule::Base, Array(Issue)).new.tap do |hash|
issues.each do |issue|
next if issue.disabled? || issue.syntax?
next if issue.correctable? && config[:autocorrect]?
(hash[issue.rule] ||= Array(Issue).new) << issue
end
end
end
private def header
<<-YAML
# This configuration file was generated by `ameba --gen-config`
# on #{Time.utc} using Ameba version #{Ameba.version}.
#
# The point is for the user to remove these configuration records
# one by one as the reported problems are removed from the code base.
#
# For more details on any individual rule, run `ameba --only RuleName`.
Version: "#{Ameba.version.for_production}"
YAML
end
private def exclude_paths(issues)
issues
.compact_map(&.location.try &.filename.try &.to_s)
.to_set
.map { |path| Path[path].to_posix.to_s }
end
private def rule_to_yaml(yaml, rule, issues)
yaml.scalar rule.name
yaml.mapping do
yaml.scalar "Excluded"
yaml.sequence do
exclude_paths(issues).each do |path|
yaml.scalar path
end
end
end
end
end
end
================================================
FILE: src/ameba/formatter/util.cr
================================================
module Ameba::Formatter
module Util
extend self
def pluralize(count : Int, singular : String, plural = "#{singular}s")
count == 1 ? singular : plural
end
def to_human(span : Time::Span)
total_milliseconds = span.total_milliseconds
if total_milliseconds < 1
return "#{(span.total_milliseconds * 1_000).round.to_i} microseconds"
end
total_seconds = span.total_seconds
if total_seconds < 1
return "#{span.total_milliseconds.round(2)} milliseconds"
end
if total_seconds < 60
return "#{total_seconds.round(2)} seconds"
end
minutes = span.minutes
seconds = span.seconds
"#{minutes}:#{seconds < 10 ? "0" : ""}#{seconds} minutes"
end
def colorize_markdown(string : String, code_color : Colorize::Color = :dark_gray)
string = colorize_code_fences(string, code_color)
string = colorize_text_styles(string)
string
end
def colorize_code_fences(string : String, color : Colorize::Color = :dark_gray)
string
.gsub(/```(.+?)```/m, &.colorize(color))
.gsub(/`(?!`)(.+?)`/, &.colorize(color))
end
def colorize_text_styles(string : String)
{% begin %}
{%
modes = {
underline: {/^([#]{1,6})(?=\s+)(.+?)$/m, "%1$s%2$s"},
strikethrough: {/([~]{2})(.+?)\1/, "%1$s%2$s%1$s"},
bold: {/([*_]{2})(.+?)\1/, "%1$s%2$s%1$s"},
italic: {/([*_])(.+?)\1/, "%1$s%2$s%1$s"},
}
%}
string
{% for mode, pattern in modes %}
.gsub({{ pattern[0] }}, %(<{{ mode.id }} fence="\\1">\\2{{ mode.id }}>))
{% end %}
{% for mode, pattern in modes %}
.gsub(/<{{ mode.id }} fence="(.+?)">(.+?)<\/{{ mode.id }}>/) do |_, match|
string = {{ pattern[1] }} % {match[1], match[2]}
string.colorize.{{ mode.id }}
end
{% end %}
{% end %}
end
def deansify(message : String?) : String?
message.try &.gsub(/\x1b[^m]*m/, "").presence
end
def trim(str, max_length = 120, ellipsis = " ...")
if (str.size - ellipsis.size) > max_length
str = str[0, max_length]
if str.size > ellipsis.size
str = str[0...-ellipsis.size] + ellipsis
end
end
str
end
def context(lines, lineno, context_lines = 3, remove_empty = true)
pre_context, post_context = %w[], %w[]
lines.each_with_index do |line, i|
case i + 1
when lineno - context_lines...lineno
pre_context << line
when lineno + 1..lineno + context_lines
post_context << line
end
end
if remove_empty
# remove empty lines at the beginning ...
while pre_context.first?.try(&.blank?)
pre_context.shift
end
# ... and the end
while post_context.last?.try(&.blank?)
post_context.pop
end
end
{pre_context, post_context}
end
def affected_code(issue : Issue, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ")
return unless location = issue.location
affected_code(issue.code, location, issue.end_location, context_lines, max_length, ellipsis, prompt)
end
def affected_code(code, location, end_location = nil, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ")
lines = code.split('\n') # must preserve trailing newline
lineno, column =
location.line_number, location.column_number
return unless affected_line = lines[lineno - 1]?.presence
if column < max_length
affected_line = trim(affected_line, max_length, ellipsis)
end
position = prompt.size + column
position -= 1
show_context = context_lines > 0
if show_context
pre_context, post_context =
context(lines, lineno, context_lines)
else
affected_line_size, affected_line =
affected_line.size, affected_line.lstrip
indent_size_diff = affected_line_size - affected_line.size
if column > indent_size_diff
position -= indent_size_diff
end
end
String.build do |str|
if show_context
pre_context.try &.each do |line|
line = trim(line, max_length, ellipsis)
str << prompt
str.puts(line.colorize(:dark_gray))
end
end
str << prompt
str.puts(affected_line.colorize(:white))
str << (" " * position)
str << "^".colorize(:yellow)
if end_location
end_lineno = end_location.line_number
end_column = end_location.column_number
if end_lineno == lineno && end_column > column
if column < max_length
end_column = {end_column, max_length}.min
end
length = end_column - column
length -= 1
str << ("-" * length).colorize(:dark_gray)
str << "^".colorize(:yellow)
end
end
str.puts
if show_context
post_context.try &.each do |line|
line = trim(line, max_length, ellipsis)
str << prompt
str.puts(line.colorize(:dark_gray))
end
end
end
end
end
end
================================================
FILE: src/ameba/glob_utils.cr
================================================
module Ameba
# Helper module that is utilizes helpers for working with globs.
module GlobUtils
extend self
# Returns all files that match specified globs.
# Globs can have wildcards or be rejected:
#
# ```
# find_files_by_globs(["**/*.cr", "!lib"])
# ```
def find_files_by_globs(globs, root = Dir.current)
rejected = rejected_globs(globs, root)
selected = globs - rejected
expand(selected, root) - expand(rejected.map!(&.lchop), root)
end
# Expands globs. Globs can point to files or even directories.
#
# ```
# expand(["spec/*.cr", "src"]) # => all files in src folder + first level specs
# ```
def expand(globs, root = Dir.current)
globs
.flat_map do |glob|
glob = Path[glob].expand(root).relative_to(Dir.current).to_posix
if File.directory?(glob)
glob = glob / "**" / "*.{cr,ecr}"
end
glob = glob.to_s.lchop("./")
Dir[glob]
end
.uniq!
.select! { |path| File.file?(path) }
end
private def rejected_globs(globs, root = Dir.current)
globs.select do |glob|
glob.starts_with?('!') && !File.exists?(Path[glob].expand(root))
end
end
end
end
================================================
FILE: src/ameba/inline_comments.cr
================================================
module Ameba
# A module that utilizes inline comments parsing and processing logic.
module InlineComments
COMMENT_DIRECTIVE_REGEX =
/# ameba:(?\w+) (?\w+(?:\/\w+)?(?:,? \w+(?:\/\w+)?)*)/
# Available actions in the inline comments
enum Action
Disable
Enable
end
# Returns `true` if current location is disabled for a particular rule,
# `false` otherwise.
#
# Location is disabled in two cases:
# 1. The line of the location ends with a comment directive.
# 2. The line above the location is a comment directive.
#
# For example, here are two examples of disabled location:
#
# ```
# # ameba:disable Style/LargeNumbers
# Time.epoch(1483859302)
#
# Time.epoch(1483859302) # ameba:disable Style/LargeNumbers
# ```
#
# But here are examples which are not considered as disabled location:
#
# ```
# # ameba:disable Style/LargeNumbers
# #
# Time.epoch(1483859302)
#
# if use_epoch? # ameba:disable Style/LargeNumbers
# Time.epoch(1483859302)
# end
# ```
def location_disabled?(location : Crystal::Location?, rule)
return false if rule.name.in?(Rule::SPECIAL)
return false unless line_number = location.try &.line_number.try &.- 1
return false unless line = lines[line_number]?
line_disabled?(line, rule) ||
(line_number > 0 &&
(prev_line = lines[line_number - 1]) &&
comment?(prev_line) &&
line_disabled?(prev_line, rule))
end
# Parses inline comment directive. Returns a tuple that consists of
# an action and parsed rules if directive found, nil otherwise.
#
# ```
# line = "# ameba:disable Rule1, Rule2"
# directive = parse_inline_directive(line)
# directive[:action] # => "disable"
# directive[:rules] # => ["Rule1", "Rule2"]
# ```
#
# It ignores the directive if it is commented out.
#
# ```
# line = "# # ameba:disable Rule1, Rule2"
# parse_inline_directive(line) # => nil
# ```
def parse_inline_directive(line)
return unless directive = COMMENT_DIRECTIVE_REGEX.match(line)
return if commented_out?(line.gsub(directive[0], ""))
{
action: directive["action"],
rules: directive["rules"].split(/[\s,]/, remove_empty: true),
}
end
# Returns `true` if the line at the given `line_number` is a comment.
def comment?(line_number : Int32)
return unless line = lines[line_number]?
comment?(line)
end
private def comment?(line : String)
line.lstrip.starts_with? '#'
end
private def line_disabled?(line, rule)
return false unless directive = parse_inline_directive(line)
return false unless Action.parse?(directive[:action]).try(&.disable?)
rules = directive[:rules]
rules.includes?(rule.name) || rules.includes?(rule.group)
end
private def commented_out?(line)
commented = false
lexer = Crystal::Lexer.new(line).tap(&.comments_enabled = true)
Tokenizer.new(lexer).run do |token|
commented = true if token.type.comment?
end
commented
end
end
end
================================================
FILE: src/ameba/issue.cr
================================================
module Ameba
# Represents an issue reported by Ameba.
struct Issue
enum Status
Enabled
Disabled
end
# The source code that triggered this issue.
getter code : String
# A rule that triggers this issue.
getter rule : Rule::Base
# Location of the issue.
getter location : Crystal::Location?
# End location of the issue.
getter end_location : Crystal::Location?
# Issue message.
getter message : String
# Issue status.
getter status : Status
delegate :enabled?, :disabled?,
to: status
def initialize(@code, @rule, @location, @end_location, @message, status : Status? = nil, @block : (Source::Corrector ->)? = nil)
@status = status || Status::Enabled
end
def syntax?
rule.is_a?(Rule::Lint::Syntax)
end
def correctable?
!@block.nil?
end
def correct(corrector)
@block.try &.call(corrector)
end
end
end
================================================
FILE: src/ameba/json_schema/builder.cr
================================================
module Ameba::JSONSchema::Builder
extend self
def build(indent = 2) : String
String.build do |str|
build(str, indent)
end
end
def build(path : Path, indent = 2) : Nil
File.open(path, "w") do |file|
build(file, indent)
end
end
def build(io : IO, indent = 2) : Nil
JSON.build(io, indent: indent) do |builder|
builder.object do
builder.field("$schema", "https://json-schema.org/draft/2020-12/schema")
builder.field("$id", "https://crystal-ameba.github.io/.ameba.yml.schema.json")
builder.field("title", ".ameba.yml")
builder.field("description", "Configuration rules for the Crystal language Ameba linter")
builder.field("type", "object")
builder.field("additionalProperties", false)
builder.string("$defs")
builder.object do
builder.string("Severity")
builder.object do
builder.field("type", "string")
builder.string("enum")
builder.array do
Severity.values.each do |value|
builder.string(value.to_s)
end
end
end
builder.string("Globs")
builder.object do
builder.field("type", "array")
builder.field("title", "Globbed files and paths")
builder.field("description",
"An array of wildcards (or paths) to include to the inspection")
builder.string("items")
builder.object do
builder.field("type", "string")
builder.string("examples")
builder.array do
builder.string("src/**/*.{cr,ecr}")
builder.string("!lib")
end
end
end
builder.string("Excluded")
builder.object do
builder.field("type", "array")
builder.field("title", "Excluded files and paths")
builder.field("description",
"An array of wildcards (or paths) to exclude from the source list")
builder.string("items")
builder.object do
builder.field("type", "string")
builder.string("examples")
builder.array do
builder.string("spec/fixtures/**")
builder.string("spec/**/*.manual_spec.cr")
end
end
end
builder.string("BaseRule")
builder.object do
builder.field("type", "object")
builder.string("properties")
builder.object do
builder.string("SinceVersion")
builder.object do
builder.field("type", "string")
end
builder.string("Enabled")
builder.object do
builder.field("type", "boolean")
builder.field("default", true)
end
builder.string("Severity")
builder.object do
builder.field("$ref", "#/$defs/Severity")
builder.field("default", Rule::Base.default_severity.to_s)
end
builder.string("Excluded")
builder.object do
builder.field("$ref", "#/$defs/Excluded")
end
end
end
end
builder.string("properties")
builder.object do
builder.string("Version")
builder.object do
builder.field("type", "string")
builder.field("description", "The version of Ameba to limit rules to")
builder.string("examples")
builder.array do
builder.string("1.7.0")
builder.string("1.6.4")
end
end
builder.string("Formatter")
builder.object do
builder.field("type", "object")
builder.field("description", "The formatter to use for Ameba")
builder.string("properties")
builder.object do
builder.string("Name")
builder.object do
builder.field("type", "string")
builder.string("enum")
builder.array do
Config::AVAILABLE_FORMATTERS.each_key do |key|
builder.string(key)
end
end
end
end
end
builder.string("Globs")
builder.object do
builder.field("$ref", "#/$defs/Globs")
end
builder.string("Excluded")
builder.object do
builder.field("$ref", "#/$defs/Excluded")
end
Rule.rules.each do |rule|
rule.to_json_schema(builder)
end
end
end
end
end
end
================================================
FILE: src/ameba/presenter/base_presenter.cr
================================================
module Ameba::Presenter
private ENABLED_MARK = "✓".colorize(:green)
private DISABLED_MARK = "x".colorize(:red)
class BasePresenter
# TODO: allow other IOs
getter output : IO::FileDescriptor | IO::Memory
def initialize(@output = STDOUT)
end
end
end
================================================
FILE: src/ameba/presenter/rule_collection_presenter.cr
================================================
module Ameba::Presenter
class RuleCollectionPresenter < BasePresenter
def run(rules) : Nil
rules = rules.to_h do |rule|
name = rule.name.split('/')
name = "%s/%s" % {
name[0...-1].join('/').colorize(:light_gray),
name.last.colorize(:white),
}
{name, rule}
end
longest_name = rules.max_of(&.first.size)
rules.group_by(&.last.group).each do |group, group_rules|
output.puts "— %s" % group.colorize(:light_blue).underline
output.puts
group_rules.each do |name, rule|
output.puts " %s [%s] %s %s" % {
rule.enabled? ? ENABLED_MARK : DISABLED_MARK,
rule.severity.symbol.to_s.colorize(:green),
name.ljust(longest_name),
rule.description.colorize(:dark_gray),
}
end
output.puts
end
output.puts "Total rules: %s / %s enabled" % {
rules.size.to_s.colorize(:light_blue),
rules.count(&.last.enabled?).to_s.colorize(:light_blue),
}
end
end
end
================================================
FILE: src/ameba/presenter/rule_presenter.cr
================================================
module Ameba::Presenter
class RulePresenter < BasePresenter
include Formatter::Util
def run(rule) : Nil
output_title "Rule info"
info = <<-INFO
Name: %s
Severity: %s
Enabled: %s
Since version: %s
INFO
output_paragraph info % {
rule.name.colorize(:magenta),
rule.severity.to_s.colorize(rule.severity.color),
rule.enabled? ? ENABLED_MARK : DISABLED_MARK,
(rule.since_version.try(&.to_s) || "N/A").colorize(:white),
}
if rule_description = rule.description
output_title "Description"
output_paragraph colorize_markdown(rule_description)
end
if rule_doc = rule.class.parsed_doc
output_title "Detailed description"
output_paragraph colorize_markdown(rule_doc)
end
end
private def output_title(title)
output.print "### %s\n\n" % title.upcase.colorize(:yellow)
end
private def output_paragraph(paragraph : String)
output_paragraph(paragraph.lines)
end
private def output_paragraph(paragraph : Array)
paragraph.each do |line|
output.puts " #{line}"
end
output.puts
end
end
end
================================================
FILE: src/ameba/presenter/rule_versions_presenter.cr
================================================
module Ameba::Presenter
class RuleVersionsPresenter < BasePresenter
def run(rules, verbose = true)
missing_version = SemanticVersion.new(0, 0, 0)
versions = rules
.sort_by { |rule| rule.since_version || missing_version }
.group_by(&.since_version)
if verbose
versions.each_with_index do |(version, version_rules), idx|
output.puts if idx.positive?
if version
output.puts "- %s" % version.to_s.colorize(:green)
else
output.puts "- %s" % "N/A".colorize(:dark_gray)
end
version_rules.map(&.name).sort!.each do |name|
output.puts " - %s" % name.colorize(:dark_gray)
end
end
else
versions.each_key do |version|
if version
output.puts "- %s" % version.to_s.colorize(:green)
end
end
end
end
end
end
================================================
FILE: src/ameba/reportable.cr
================================================
require "./ast/util"
module Ameba
# Represents a module used to report issues.
module Reportable
include AST::Util
# List of reported issues.
getter issues = [] of Issue
# Adds a new issue to the list of issues.
def add_issue(rule,
location : Crystal::Location?,
end_location : Crystal::Location?,
message : String,
status : Issue::Status? = nil,
block : (Source::Corrector ->)? = nil) : Issue
status ||=
Issue::Status::Disabled if location_disabled?(location, rule)
Issue.new(code, rule, location, end_location, message, status, block).tap do |issue|
issues << issue
end
end
# :ditto:
def add_issue(rule,
location : Crystal::Location?,
end_location : Crystal::Location?,
message : String,
status : Issue::Status? = nil,
&block : Source::Corrector ->) : Issue
add_issue rule, location, end_location, message, status, block
end
# Adds a new issue for *location* defined by line and column numbers.
def add_issue(rule,
location : {Crystal::Location, Crystal::Location},
message : String,
status : Issue::Status? = nil,
block : (Source::Corrector ->)? = nil) : Issue
add_issue rule, *location, message, status, block
end
# Adds a new issue for Crystal AST *node*.
def add_issue(rule,
node : Crystal::ASTNode,
message : String,
status : Issue::Status? = nil,
block : (Source::Corrector ->)? = nil,
*,
prefer_name_location = false) : Issue
if prefer_name_location
case node_location = name_location_or(node)
when Tuple(Crystal::Location, Crystal::Location)
location, end_location = node_location
else
location, end_location =
name_location(node), name_end_location(node)
end
end
location ||= node.location
end_location ||= node.end_location
add_issue rule, location, end_location, message, status, block
end
# :ditto:
def add_issue(rule,
node : Crystal::ASTNode,
message : String,
status : Issue::Status? = nil,
*,
prefer_name_location = false,
&block : Source::Corrector ->) : Issue
add_issue rule, node, message, status, block, prefer_name_location: prefer_name_location
end
# Adds a new issue for Crystal *token*.
def add_issue(rule,
token : Crystal::Token,
message : String,
status : Issue::Status? = nil,
block : (Source::Corrector ->)? = nil) : Issue
add_issue rule, token.location, nil, message, status, block
end
# :ditto:
def add_issue(rule,
token : Crystal::Token,
message : String,
status : Issue::Status? = nil,
&block : Source::Corrector ->) : Issue
add_issue rule, token, message, status, block
end
# Adds a new issue for *location* defined by line and column numbers.
def add_issue(rule,
location : {Int32, Int32},
message : String,
status : Issue::Status? = nil,
block : (Source::Corrector ->)? = nil) : Issue
location =
Crystal::Location.new(path, *location)
add_issue rule, location, nil, message, status, block
end
# :ditto:
def add_issue(rule,
location : {Int32, Int32},
message : String,
status : Issue::Status? = nil,
&block : Source::Corrector ->) : Issue
add_issue rule, location, message, status, block
end
# Adds a new issue for *location* and *end_location* defined by line and column numbers.
def add_issue(rule,
location : {Int32, Int32},
end_location : {Int32, Int32},
message : String,
status : Issue::Status? = nil,
block : (Source::Corrector ->)? = nil) : Issue
location =
Crystal::Location.new(path, *location)
end_location =
Crystal::Location.new(path, *end_location)
add_issue rule, location, end_location, message, status, block
end
# :ditto:
def add_issue(rule,
location : {Int32, Int32},
end_location : {Int32, Int32},
message : String,
status : Issue::Status? = nil,
&block : Source::Corrector ->) : Issue
add_issue rule, location, end_location, message, status, block
end
# Returns `true` if the list of not disabled issues is empty, `false` otherwise.
def valid?
issues.none?(&.enabled?)
end
end
end
================================================
FILE: src/ameba/rule/base.cr
================================================
module Ameba::Rule
# List of names of the special rules, which
# behave differently than usual rules.
SPECIAL = {
Lint::Syntax.rule_name,
Lint::UnneededDisableDirective.rule_name,
}
# Represents a base of all rules. In other words, all rules
# inherits from this class:
#
# ```
# class Ameba::Rule::MyRule < Ameba::Rule::Base
# def test(source)
# if invalid?(source)
# issue_for line, column, "Something wrong"
# end
# end
#
# private def invalid?(source)
# # ...
# end
# end
# ```
#
# Enforces rules to implement an abstract `#test` method which
# is designed to test the source passed in. If source has issues
# that are tested by this rule, it should add an issue.
abstract class Base
include Config::RuleConfig
# This method is designed to test the source passed in. If source has issues
# that are tested by this rule, it should add an issue.
#
# By default it uses a node visitor to traverse all the nodes in the source.
#
# NOTE: Must be overridden for other type of rules.
def test(source : Source)
AST::NodeVisitor.new self, source
end
# NOTE: Can't be abstract
def test(source : Source, node : Crystal::ASTNode, *opts)
end
# A convenient addition to `#test` method that does the same
# but returns a passed in `source` as an addition.
#
# ```
# source = MyRule.new.catch(source)
# source.valid?
# ```
def catch(source : Source)
source.tap { test source }
end
# Returns a name of this rule, which is basically a class name.
#
# ```
# module Ameba
# class Rule::MyRule < Rule::Base
# def test(source)
# end
# end
# end
#
# Ameba::Rule::MyRule.new.name # => "MyRule"
# ```
def name
{{ @type }}.rule_name
end
# Returns a group this rule belong to.
#
# ```
# module Ameba
# class Rule::MyGroup::MyRule < Rule::Base
# # ...
# end
# end
#
# Ameba::Rule::MyGroup::MyRule.new.group # => "MyGroup"
# ```
def group
{{ @type }}.group_name
end
# Checks whether the source is excluded from this rule.
# It searches for a path in `excluded` property which matches
# the one of the given source.
#
# ```
# my_rule.excluded?(source) # => true or false
# ```
def excluded?(source, root = Dir.current)
!!excluded.try &.any? do |path|
path = Path.posix(path).to_native.expand(root)
source.fullpath == path.to_s ||
Dir.glob(path.to_posix).includes?(source.fullpath)
end
end
# Returns `true` if this rule is special and behaves differently than
# usual rules.
#
# ```
# my_rule.special? # => true or false
# ```
def special?
name.in?(SPECIAL)
end
def_equals_and_hash name
# Adds an issue to the *source*
macro issue_for(*args, **kwargs, &block)
source.add_issue(self, {{ args.splat(", ") }}{{ kwargs.double_splat }}) {{ block }}
end
# Returns the name for this rule.
#
# ```
# Ameba::Rule::Lint::Syntax.rule_name # => "Lint/Syntax"
# ```
def self.rule_name
name.lchop("Ameba::Rule::").gsub("::", '/')
end
# Returns the group name for this rule.
#
# ```
# Ameba::Rule::Lint::Syntax.group_name # => "Lint"
# ```
def self.group_name
rule_name.split('/')[0...-1].join('/')
end
protected def self.subclasses
{{ @type.subclasses }}
end
protected def self.abstract?
{{ @type.abstract? }}
end
protected def self.inherited_rules
subclasses.each_with_object([] of Base.class) do |klass, obj|
klass.abstract? ? obj.concat(klass.inherited_rules) : (obj << klass)
end
end
private macro read_type_doc(filepath = __FILE__)
{{ run("../../contrib/read_type_doc",
@type.name.split("::").last,
filepath
).chomp.stringify }}.presence
end
macro inherited
# Returns the documentation URL for this rule.
#
# ```
# Ameba::Rule::Lint::Syntax.documentation_url
# # => "https://crystal-ameba.github.io/ameba/master/Ameba/Rule/Lint/Syntax.html"
# ```
class_getter documentation_url : String do
"https://crystal-ameba.github.io/ameba/%s/Ameba/Rule/%s.html" % {
Ameba.version.for_docs, rule_name,
}
end
# Returns documentation for this rule, if there is any.
#
# ```
# module Ameba
# # This is a test rule.
# # Does nothing.
# class Rule::MyRule < Rule::Base
# def test(source)
# end
# end
# end
#
# Ameba::Rule::MyRule.parsed_doc # => "This is a test rule.\nDoes nothing."
# ```
class_getter parsed_doc : String? = read_type_doc
end
end
# Returns a list of all available rules.
#
# ```
# Ameba::Rule.rules # => [Rule1, Rule2, ....]
# ```
def self.rules
Base.inherited_rules
end
end
================================================
FILE: src/ameba/rule/documentation/admonition.cr
================================================
module Ameba::Rule::Documentation
# A rule that reports documentation admonitions.
#
# Optionally, these can fail at an appropriate time.
#
# ```
# def get_user(id)
# # TODO(2024-04-24) Fix this hack when the database migration is complete
# if id < 1_000_000
# v1_api_call(id)
# else
# v2_api_call(id)
# end
# end
# ```
#
# `TODO` comments are used to remind yourself of source code related things.
#
# The premise here is that `TODO` should be dealt with in the near future
# and are therefore reported by Ameba.
#
# `FIXME` comments are used to indicate places where source code needs fixing.
#
# The premise here is that `FIXME` should indeed be fixed as soon as possible
# and are therefore reported by Ameba.
#
# YAML configuration example:
#
# ```
# Documentation/Admonition:
# Enabled: true
# Admonitions: [TODO, FIXME, BUG]
# Timezone: UTC
# ```
class Admonition < Base
include AST::Util
properties do
since_version "1.6.0"
enabled false
description "Reports documentation admonitions"
severity :warning
admonitions %w[TODO FIXME BUG]
timezone "UTC"
end
MSG = "Found a %s admonition in a comment"
MSG_LATE = "Found a %s admonition in a comment (%s)"
MSG_ERR = "%s admonition error: %s"
@[YAML::Field(ignore: true)]
private getter location : Time::Location do
Time::Location.load(timezone)
end
def test(source)
Tokenizer.new(source).run do |token|
next unless token.type.comment?
next unless doc = token.value.to_s
pattern =
/^#\s*(?#{Regex.union(admonitions)})(?:\((?.+?)\))?(?:\W+|$)/m
matches = doc.scan(pattern)
matches.each do |match|
admonition = match["admonition"]
token_location = name_location_or token, admonition,
adjust_location_column_number: {{ "# ".size }}
begin
case expr = match["context"]?.presence
when /\A\d{4}-\d{2}-\d{2}\Z/ # date
date = Time.parse($0, "%F", location)
issue_for_date source, token_location, admonition, date
when /\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?\Z/ # date + time (no tz)
date = Time.parse($0, "%F #{$1?.presence ? "%T" : "%R"}", location)
issue_for_date source, token_location, admonition, date
else
issue_for *token_location, MSG % admonition
end
rescue ex
issue_for *token_location, MSG_ERR % {admonition, "#{ex}: #{expr.inspect}"}
end
end
end
end
private def issue_for_date(source, token_location, admonition, date)
diff = Time.utc - date.to_utc
return if diff.negative?
past = case diff
when 0.seconds..1.day then "today is the day!"
when 1.day..2.days then "1 day past"
else "#{diff.total_days.to_i} days past"
end
issue_for *token_location, MSG_LATE % {admonition, past}
end
end
end
================================================
FILE: src/ameba/rule/documentation/documentation.cr
================================================
module Ameba::Rule::Documentation
# A rule that enforces documentation for public types:
# modules, classes, enums, methods and macros.
#
# YAML configuration example:
#
# ```
# Documentation/Documentation:
# Enabled: true
# IgnoreClasses: false
# IgnoreModules: true
# IgnoreEnums: false
# IgnoreDefs: true
# IgnoreMacros: false
# IgnoreMacroHooks: true
# RequireExample: false
# ```
class Documentation < Base
properties do
since_version "1.5.0"
enabled false
description "Enforces public types to be documented"
ignore_classes false
ignore_modules true
ignore_enums false
ignore_defs true
ignore_macros false
ignore_macro_hooks true
require_example false
end
MSG = "Missing documentation"
MSG_EXAMPLE = "Missing documentation example"
MACRO_HOOK_NAMES = %w[
inherited
included extended
method_missing method_added
finished
]
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node : Crystal::ClassDef, scope : AST::Scope)
ignore_classes? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::ModuleDef, scope : AST::Scope)
ignore_modules? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::EnumDef, scope : AST::Scope)
ignore_enums? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::Def, scope : AST::Scope)
ignore_defs? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::Macro, scope : AST::Scope)
return if ignore_macro_hooks? && node.name.in?(MACRO_HOOK_NAMES)
ignore_macros? || check_missing_doc(source, node, scope)
end
private def check_missing_doc(source, node, scope)
# bail out if the node is not public,
# i.e. `private def foo`
return if !node.visibility.public?
# bail out if the scope is not public,
# i.e. `def bar` inside `private class Foo`
return if (visibility = scope.visibility) && !visibility.public?
if doc = node.doc.presence
issue_for node, MSG_EXAMPLE unless valid_example?(doc)
else
issue_for node, MSG
end
end
private def valid_example?(doc : String)
!require_example? || doc.matches?(/^\s*```\n/m)
end
end
end
================================================
FILE: src/ameba/rule/layout/line_length.cr
================================================
module Ameba::Rule::Layout
# A rule that disallows lines longer than `max_length` number of symbols.
#
# YAML configuration example:
#
# ```
# Layout/LineLength:
# Enabled: true
# MaxLength: 100
# ```
class LineLength < Base
properties do
since_version "0.1.0"
enabled false
description "Disallows lines longer than `MaxLength` number of symbols"
max_length 140
end
MSG = "Line too long"
def test(source)
source.lines.each_with_index do |line, index|
issue_for({index + 1, max_length + 1}, MSG) if line.size > max_length
end
end
end
end
================================================
FILE: src/ameba/rule/layout/trailing_blank_lines.cr
================================================
module Ameba::Rule::Layout
# A rule that disallows trailing blank lines at the end of the source file.
#
# YAML configuration example:
#
# ```
# Layout/TrailingBlankLines:
# Enabled: true
# ```
class TrailingBlankLines < Base
properties do
since_version "0.1.0"
description "Disallows trailing blank lines"
end
MSG = "Excessive trailing newline detected"
MSG_FINAL_NEWLINE = "Trailing newline missing"
def test(source)
source_lines = source.lines
return if source_lines.empty?
last_source_line = source_lines.last
source_lines_size = source_lines.size
return if source_lines_size == 1 && last_source_line.empty?
last_line_empty = last_source_line.empty?
return if source_lines_size.zero? ||
(source_lines.last(2).join.presence && last_line_empty)
location = {source_lines_size, 1}
if last_line_empty
issue_for location, MSG
else
issue_for location, MSG_FINAL_NEWLINE do |corrector|
corrector.insert_before({source_lines_size + 1, 1}, '\n')
end
end
end
end
end
================================================
FILE: src/ameba/rule/layout/trailing_whitespace.cr
================================================
module Ameba::Rule::Layout
# A rule that disallows trailing whitespace.
#
# YAML configuration example:
#
# ```
# Layout/TrailingWhitespace:
# Enabled: true
# ```
class TrailingWhitespace < Base
properties do
since_version "0.1.0"
description "Disallows trailing whitespace"
end
MSG = "Trailing whitespace detected"
def test(source)
source.lines.each_with_index do |line, index|
next unless ws_index = line =~ /\s+$/
location = {index + 1, ws_index + 1}
end_location = {index + 1, line.size}
issue_for location, end_location, MSG do |corrector|
corrector.remove(location, end_location)
end
end
end
end
end
================================================
FILE: src/ameba/rule/lint/ambiguous_assignment.cr
================================================
module Ameba::Rule::Lint
# This rule checks for mistyped shorthand assignments.
#
# This is considered invalid:
#
# x =- y
# x =+ y
# x =! y
#
# And this is valid:
#
# x -= y # or x = -y
# x += y # or x = +y
# x != y # or x = !y
#
# YAML configuration example:
#
# ```
# Lint/AmbiguousAssignment:
# Enabled: true
# ```
class AmbiguousAssignment < Base
include AST::Util
properties do
since_version "1.0.0"
description "Disallows ambiguous `=-/=+/=!`"
end
MSG = "Suspicious assignment detected. Did you mean `%s`?"
MISTAKES = {
"=-": "-=",
"=+": "+=",
"=!": "!=",
}
def test(source, node : Crystal::Assign)
return unless op_end_location = node.value.location
op_location = op_end_location.adjust(column_number: -1)
op_text = source_between(op_location, op_end_location, source.lines)
return unless op_text
return unless suggestion = MISTAKES[op_text]?
issue_for op_location, op_end_location, MSG % suggestion
end
end
end
================================================
FILE: src/ameba/rule/lint/assignment_in_call_argument.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows assignments in call arguments.
#
# For example, this is considered invalid:
#
# ```
# foo a = 1
# ```
#
# And has to be written as the following:
#
# ```
# a = 1
#
# foo a
# ```
#
# YAML configuration example:
#
# ```
# Lint/AssignmentInCallArgument:
# Enabled: true
# ```
class AssignmentInCallArgument < Base
properties do
since_version "1.7.0"
description "Disallows variable assignment in call arguments"
end
MSG = "Assignment within a call argument detected"
def test(source)
AssignmentInCallArgumentVisitor.new(self, source) do |node|
issue_for node, MSG
end
end
end
private class AssignmentInCallArgumentVisitor < AST::ScopeVisitor
include AST::Util
getter? in_call_args = false
def initialize(rule, source, &@on_assign : Crystal::ASTNode ->)
super(rule, source)
end
private def in_call_args(value = true, &)
prev_value = @in_call_args
begin
@in_call_args = value
yield
ensure
@in_call_args = prev_value
end
end
def visit(node : Crystal::Def)
return super unless node.name == "->"
in_call_args(false) do
node.accept_children(self)
end
false
end
def visit(node : Crystal::Block)
super
in_call_args(false) do
node.accept_children(self)
end
false
end
def visit(node : Crystal::Call)
return false if setter_method?(node) || operator_method?(node)
return false unless super
node.obj.try &.accept(self)
in_call_args do
node.args.each &.accept(self)
node.named_args.try &.each &.accept(self)
end
node.block_arg.try &.accept(self)
node.block.try &.accept(self)
false
end
def visit(node : Crystal::Assign | Crystal::OpAssign | Crystal::MultiAssign)
super.tap do
@on_assign.call(node) if in_call_args?
end
end
end
end
================================================
FILE: src/ameba/rule/lint/bad_directive.cr
================================================
module Ameba::Rule::Lint
# A rule that reports incorrect comment directives for Ameba.
#
# ```
# # ameba:off Lint/NotNil
# def foo
# :bar
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/BadDirective:
# Enabled: true
# ```
class BadDirective < Base
include AST::Util
properties do
since_version "0.13.0"
description "Reports bad comment directives"
end
MSG = "Bad action in comment directive: `%s`. Possible values: %s"
AVAILABLE_ACTIONS = InlineComments::Action
.names
.map!(&.underscore.gsub('_', '-'))
def test(source)
Tokenizer.new(source).run do |token|
next unless token.type.comment?
next unless directive = source.parse_inline_directive(token.value.to_s)
check_action source, token, directive[:action]
end
end
private def check_action(source, token, action)
return if InlineComments::Action.parse?(action)
# See `InlineComments::COMMENT_DIRECTIVE_REGEX`
prefix_size = {{ "# ameba:".size }}
issue_for name_location_or(token, action, adjust_location_column_number: prefix_size),
MSG % {action, AVAILABLE_ACTIONS.map { |name| "`#{name}`" }.join(", ")}
end
end
end
================================================
FILE: src/ameba/rule/lint/comparison_to_boolean.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows comparison to booleans.
#
# For example, these are considered invalid:
#
# ```
# foo == true
# bar != false
# false === baz
# ```
#
# This is because these expressions evaluate to `true` or `false`, so you
# could get the same result by using either the variable directly,
# or negating the variable.
#
# YAML configuration example:
#
# ```
# Lint/ComparisonToBoolean:
# Enabled: true
# ```
class ComparisonToBoolean < Base
include AST::Util
properties do
since_version "0.1.0"
enabled false
description "Disallows comparison to booleans"
end
MSG = "Comparison to a boolean is pointless"
OP_NAMES = %w[== != ===]
def test(source, node : Crystal::Call)
return unless node.name.in?(OP_NAMES)
return unless node.args.size == 1
arg, obj = node.args.first, node.obj
case
when arg.is_a?(Crystal::BoolLiteral)
bool, exp = arg, obj
when obj.is_a?(Crystal::BoolLiteral)
bool, exp = obj, arg
end
return unless bool && exp
return unless exp_code = node_source(exp, source.lines)
not =
case node.name
when "==", "===" then !bool.value # foo == false
when "!=" then bool.value # foo != true
end
exp_code = "!#{exp_code}" if not
issue_for node, MSG do |corrector|
corrector.replace(node, exp_code)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/debug_calls.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows calls to debug-related methods.
#
# This is because we don't want debug calls accidentally being
# committed into our codebase.
#
# YAML configuration example:
#
# ```
# Lint/DebugCalls:
# Enabled: true
# MethodNames:
# - p
# - p!
# - pp
# - pp!
# ```
class DebugCalls < Base
properties do
since_version "1.0.0"
description "Disallows debug-related calls"
method_names %w[p p! pp pp!]
end
MSG = "Possibly forgotten debug-related `%s` call detected"
def test(source, node : Crystal::Call)
return unless node.name.in?(method_names) && node.obj.nil?
issue_for node, MSG % node.name
end
end
end
================================================
FILE: src/ameba/rule/lint/debugger_statement.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows calls to `debugger`.
#
# This is because we don't want debugger breakpoints accidentally being
# committed into our codebase.
#
# YAML configuration example:
#
# ```
# Lint/DebuggerStatement:
# Enabled: true
# ```
class DebuggerStatement < Base
include AST::Util
properties do
since_version "0.1.0"
description "Disallows calls to `debugger`"
end
MSG = "Possible forgotten `debugger` statement detected"
def test(source, node : Crystal::Call)
return unless node.name == "debugger" && node.obj.nil?
return if has_arguments?(node)
issue_for node, MSG do |corrector|
corrector.remove(node)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/duplicate_branch.cr
================================================
module Ameba::Rule::Lint
# Checks that there are no repeated bodies within `if/unless`,
# `case-when`, `case-in` and `rescue` constructs.
#
# This is considered invalid:
#
# ```
# if foo
# do_foo
# do_something_else
# elsif bar
# do_foo
# do_something_else
# end
# ```
#
# And this is valid:
#
# ```
# if foo || bar
# do_foo
# do_something_else
# end
# ```
#
# With `IgnoreLiteralBranches: true`, branches are not registered
# as offenses if they return a basic literal value (string, symbol,
# integer, float, `true`, `false`, or `nil`), or return an array,
# hash, regexp or range that only contains one of the above basic
# literal values.
#
# With `IgnoreConstantBranches: true`, branches are not registered
# as offenses if they return a constant value.
#
# With `IgnoreDuplicateElseBranch: true`, in conditionals with multiple branches,
# duplicate 'else' branches are not registered as offenses.
#
# YAML configuration example:
#
# ```
# Lint/DuplicateBranch:
# Enabled: true
# IgnoreLiteralBranches: false
# IgnoreConstantBranches: false
# IgnoreDuplicateElseBranch: false
# ```
class DuplicateBranch < Base
include AST::Util
properties do
since_version "1.7.0"
description "Reports duplicated branch bodies"
enabled false
ignore_literal_branches false
ignore_constant_branches false
ignore_duplicate_else_branch false
end
MSG = "Duplicate branch body detected"
def test(source)
AST::ElseIfAwareNodeVisitor.new self, source, skip: :macro
end
def test(
source,
node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::ExceptionHandler,
ifs : Enumerable(Crystal::If)? = nil,
)
found_bodies = Set(String).new
each_branch(ifs || node) do |body_node|
next if ignore_literal_branches? && static_literal?(body_node)
next if ignore_constant_branches? && body_node.is_a?(Crystal::Path)
next if found_bodies.add?(body_node.to_s)
issue_for body_node, MSG
end
end
private def each_branch(ifs : Enumerable(Crystal::If), &)
ifs.each do |if_node|
yield if_node.then
end
if !ignore_duplicate_else_branch? && (else_node = ifs.last.else)
yield else_node
end
end
private def each_branch(node : Crystal::If | Crystal::Unless, &)
if !ignore_duplicate_else_branch? && (else_node = node.else)
yield node.then
yield else_node
end
end
private def each_branch(node : Crystal::Case, &)
node.whens.each do |when_node|
yield when_node.body
end
if !ignore_duplicate_else_branch? && (else_node = node.else)
yield else_node
end
end
private def each_branch(node : Crystal::ExceptionHandler, &)
node.rescues.try &.each do |rescue_node|
yield rescue_node.body
end
if !ignore_duplicate_else_branch? && (else_node = node.else)
yield else_node
end
end
end
end
================================================
FILE: src/ameba/rule/lint/duplicate_enum_value.cr
================================================
module Ameba::Rule::Lint
# A rule that reports duplicated `enum` member values.
#
# ```
# enum Foo
# Foo = 1
# Bar = 2
# Baz = 2 # duplicate value
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/DuplicateEnumValue:
# Enabled: true
# ```
class DuplicateEnumValue < Base
properties do
since_version "1.7.0"
description "Reports duplicated `enum` member values"
end
MSG = "Duplicate enum member value detected"
def test(source, node : Crystal::EnumDef)
found_values = Set(String).new
node.members.each do |member|
next unless member.is_a?(Crystal::Arg)
next unless value = member.default_value
next if found_values.add?(value.to_s)
issue_for value, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/lint/duplicate_method_signature.cr
================================================
module Ameba::Rule::Lint
# Reports repeated class or module method signatures.
#
# Only methods of the same signature are considered duplicates,
# regardless of their bodies, except for ones including `previous_def`.
#
# ```
# class Foo
# def greet(name)
# puts "Hello #{name}!"
# end
#
# def greet(name) # duplicated method signature
# puts "¡Hola! #{name}"
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/DuplicateMethodSignature:
# Enabled: true
# ```
class DuplicateMethodSignature < Base
properties do
since_version "1.7.0"
description "Reports repeated method signatures"
end
MSG = "Duplicate method signature detected"
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef, scope : AST::Scope)
found_defs = Set(String).new
each_def_node(node) do |def_node|
def_node_to_s = def_node.to_s
next if def_node_to_s.matches?(/\Wprevious_def\W/)
next if found_defs.add?(def_node_to_s.lines.first)
issue_for def_node, MSG
end
end
private def each_def_node(node, &)
case body = node.body
when Crystal::Def
yield body
when Crystal::VisibilityModifier
yield body.exp
when Crystal::Expressions
body.expressions.each do |exp|
case exp
when Crystal::Def then yield exp
when Crystal::VisibilityModifier then yield exp.exp
end
end
end
end
end
end
================================================
FILE: src/ameba/rule/lint/duplicate_when_condition.cr
================================================
module Ameba::Rule::Lint
# Reports repeated conditions used in case `when` expressions.
#
# This is considered invalid:
#
# ```
# case x
# when .nil?
# do_something
# when .nil?
# do_something_else
# end
# ```
#
# And this is valid:
#
# ```
# case x
# when .nil?
# do_something
# when Symbol
# do_something_else
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/DuplicateWhenCondition:
# Enabled: true
# ```
class DuplicateWhenCondition < Base
properties do
since_version "1.7.0"
description "Reports repeated conditions used in case `when` expressions"
end
MSG = "Duplicate `when` condition detected"
def test(source, node : Crystal::Case | Crystal::Select)
found_conditions = Set(String).new
node.whens.each &.conds.each do |cond|
next if found_conditions.add?(cond.to_s)
issue_for cond, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/lint/duplicated_require.cr
================================================
module Ameba::Rule::Lint
# A rule that reports duplicated `require` statements.
#
# ```
# require "./thing"
# require "./stuff"
# require "./thing" # duplicated require
# ```
#
# YAML configuration example:
#
# ```
# Lint/DuplicatedRequire:
# Enabled: true
# ```
class DuplicatedRequire < Base
properties do
since_version "0.14.0"
description "Reports duplicated `require` statements"
end
MSG = "Duplicated require of `%s`"
def test(source)
found_requires = Set(String).new
nodes = AST::TopLevelNodesVisitor.new(source.ast).require_nodes
nodes.each do |node|
next if found_requires.add?(node.string)
issue_for node, MSG % node.string
end
end
end
end
================================================
FILE: src/ameba/rule/lint/else_nil.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows `else` blocks with `nil` as their body, as they
# have no effect and can be safely removed.
#
# This is considered invalid:
#
# ```
# if foo
# do_foo
# else
# nil
# end
# ```
#
# And this is valid:
#
# ```
# if foo
# do_foo
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/ElseNil:
# Enabled: true
# ```
class ElseNil < Base
properties do
since_version "1.7.0"
description "Disallows `else` blocks with `nil` as their body"
end
MSG = "Avoid `else` blocks with `nil` as their body"
def test(source, node : Crystal::Case)
check_issue(source, node) unless node.exhaustive?
end
def test(source, node : Crystal::If)
check_issue(source, node) unless node.ternary?
end
def test(source, node : Crystal::Unless)
check_issue(source, node)
end
private def check_issue(source, node)
return unless node_else = node.else
return unless node_else.is_a?(Crystal::NilLiteral)
if node.responds_to?(:else_location) &&
(else_location = node.else_location) &&
(end_location = node.end_location)
issue_for node_else, MSG do |corrector|
corrector.remove(
else_location,
end_location.adjust(column_number: -{{ "end".size }})
)
end
else
issue_for node_else, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/lint/empty_ensure.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows empty `ensure` statement.
#
# For example, this is considered invalid:
#
# ```
# def some_method
# do_some_stuff
# ensure
# end
#
# begin
# do_some_stuff
# ensure
# end
# ```
#
# And it should be written as this:
#
# ```
# def some_method
# do_some_stuff
# ensure
# do_something_else
# end
#
# begin
# do_some_stuff
# ensure
# do_something_else
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/EmptyEnsure:
# Enabled: true
# ```
class EmptyEnsure < Base
properties do
since_version "0.3.0"
description "Disallows empty `ensure` statement"
end
MSG = "Empty `ensure` block detected"
def test(source, node : Crystal::ExceptionHandler)
node_ensure = node.ensure
return if node_ensure.nil? || !node_ensure.nop?
ensure_location = node.ensure_location
end_location = node.end_location
return unless ensure_location && end_location
issue_for ensure_location, end_location, MSG do |corrector|
corrector.remove(
ensure_location,
end_location.adjust(column_number: -{{ "end".size }})
)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/empty_expression.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows empty expressions.
#
# This is considered invalid:
#
# ```
# foo = ()
#
# if ()
# bar
# end
# ```
#
# And this is valid:
#
# ```
# foo = (some_expression)
#
# if (some_expression)
# bar
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/EmptyExpression:
# Enabled: true
# ```
class EmptyExpression < Base
properties do
since_version "0.2.0"
description "Disallows empty expressions"
end
MSG = "Avoid empty expressions"
def test(source, node : Crystal::Expressions)
return unless node.expressions.size == 1 &&
node.expressions.first.nop?
issue_for node, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/empty_loop.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows empty loops.
#
# This is considered invalid:
#
# ```
# while false
# end
#
# until 10
# end
#
# loop do
# # nothing here
# end
# ```
#
# And this is valid:
#
# ```
# a = 1
# while a < 10
# a += 1
# end
#
# until socket_opened?
# end
#
# loop do
# do_something_here
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/EmptyLoop:
# Enabled: true
# ```
class EmptyLoop < Base
include AST::Util
properties do
since_version "0.12.0"
description "Disallows empty loops"
end
MSG = "Empty loop detected"
def test(source, node : Crystal::Call)
check_node(source, node, node.block) if loop?(node)
end
def test(source, node : Crystal::While | Crystal::Until)
check_node(source, node, node.body) if literal?(node.cond)
end
private def check_node(source, node, loop_body)
body =
loop_body.is_a?(Crystal::Block) ? loop_body.body : loop_body
return unless body.nil? || body.nop?
issue_for node, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/enum_member_name_conflict.cr
================================================
module Ameba::Rule::Lint
# A rule that reports conflicting enum member names.
#
# Since Crystal will parse enum member names using `String#camelcase` and
# `String#downcase`, it is important to ensure that each member has a name
# that stays unique after the transformation.
#
# ```
# enum Foo
# Bar
# BAR
# end
#
# Foo.parse("bar") # => Foo::Bar
# Foo.parse("Bar") # => Foo::Bar
# Foo.parse("BAR") # => Foo::Bar
# ```
#
# YAML configuration example:
#
# ```
# Lint/EnumMemberNameConflict:
# Enabled: true
# ```
class EnumMemberNameConflict < Base
properties do
since_version "1.7.0"
description "Reports conflicting enum member names"
end
MSG = "Enum member name conflict detected"
def test(source, node : Crystal::EnumDef)
found_names = Set(String).new
node.members.each do |member|
next unless member.is_a?(Crystal::Arg)
next if found_names.add?(member.name.camelcase.downcase)
issue_for member, MSG, prefer_name_location: true
end
end
end
end
================================================
FILE: src/ameba/rule/lint/formatting.cr
================================================
require "compiler/crystal/formatter"
module Ameba::Rule::Lint
# A rule that verifies syntax formatting according to the
# Crystal's built-in formatter.
#
# For example, this syntax is invalid:
#
# def foo(a,b,c=0)
# #foobar
# a+b+c
# end
#
# And should be properly written:
#
# def foo(a, b, c = 0)
# # foobar
# a + b + c
# end
#
# YAML configuration example:
#
# ```
# Lint/Formatting:
# Enabled: true
# FailOnError: false
# ```
class Formatting < Base
properties do
since_version "1.4.0"
description "Reports not formatted sources"
fail_on_error false
end
MSG = "Use built-in formatter to format this source"
MSG_ERROR = "Error while formatting: %s"
private LOCATION = {1, 1}
def test(source)
source_code = source.code
source_lines = source_code.lines
return if source_lines.empty?
result = Crystal.format(source_code, source.path)
return if result == source_code
end_location = {
source_lines.size,
source_lines.last.size + 1,
}
issue_for LOCATION, MSG do |corrector|
corrector.replace(LOCATION, end_location, result)
end
rescue ex : Crystal::SyntaxException
if fail_on_error?
issue_for({ex.line_number, ex.column_number}, MSG_ERROR % ex.message)
end
rescue ex
if fail_on_error?
issue_for(LOCATION, MSG_ERROR % ex.message)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/hash_duplicated_key.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows duplicated keys in hash literals.
#
# This is considered invalid:
#
# ```
# h = {"foo" => 1, "bar" => 2, "foo" => 3}
# ```
#
# And it has to written as this instead:
#
# ```
# h = {"foo" => 1, "bar" => 2}
# ```
#
# YAML configuration example:
#
# ```
# Lint/HashDuplicatedKey:
# Enabled: true
# ```
class HashDuplicatedKey < Base
properties do
since_version "0.3.0"
description "Disallows duplicated keys in hash literals"
end
MSG = "Duplicated keys in hash literal: %s"
def test(source, node : Crystal::HashLiteral)
return if (keys = duplicated_keys(node.entries)).empty?
issue_for node, MSG % keys.map { |key| "`#{key}`" }.join(", ")
end
private def duplicated_keys(entries)
entries.map(&.key)
.group_by(&.itself)
.select! { |_, v| v.size > 1 }
.keys
end
end
end
================================================
FILE: src/ameba/rule/lint/literal_assignments_in_expressions.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows assignments with literal values
# in control expressions.
#
# For example, this is considered invalid:
#
# ```
# if foo = 42
# do_something
# end
# ```
#
# And most likely should be replaced by the following:
#
# ```
# if foo == 42
# do_something
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/LiteralAssignmentsInExpressions:
# Enabled: true
# ```
class LiteralAssignmentsInExpressions < Base
include AST::Util
properties do
since_version "1.4.0"
description "Disallows assignments with literal values in control expressions"
end
MSG = "Detected assignment with a literal value in control expression"
def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until)
return unless (cond = node.cond).is_a?(Crystal::Assign)
return unless literal?(cond.value)
issue_for cond, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/literal_in_condition.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows useless conditional statements that contain a literal
# in place of a variable or predicate function.
#
# This is because a conditional construct with a literal predicate will
# always result in the same behavior at run time, meaning it can be
# replaced with either the body of the construct, or deleted entirely.
#
# This is considered invalid:
#
# ```
# if "something"
# :ok
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/LiteralInCondition:
# Enabled: true
# ```
class LiteralInCondition < Base
include AST::Util
properties do
since_version "0.1.0"
description "Disallows useless conditional statements that contain \
a literal in place of a variable or predicate function"
end
MSG = "Literal value found in conditional"
def test(source, node : Crystal::If | Crystal::Unless | Crystal::Until)
issue_for node.cond, MSG if literal?(node.cond)
end
def test(source, node : Crystal::Case)
return unless cond = node.cond
return unless static_literal?(cond)
issue_for cond, MSG
end
def test(source, node : Crystal::While)
return unless cond = node.cond
return unless literal?(cond)
# allow `while true`
return if cond.is_a?(Crystal::BoolLiteral) && cond.value
issue_for cond, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/literal_in_interpolation.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows useless string interpolations
# that contain a literal value instead of a variable or function.
#
# For example:
#
# ```
# "Hello, #{:Ary}"
# "There are #{4} cats"
# ```
#
# YAML configuration example:
#
# ```
# Lint/LiteralInInterpolation:
# Enabled: true
# ```
class LiteralInInterpolation < Base
include AST::Util
properties do
since_version "0.1.0"
description "Disallows useless string interpolations"
end
MSG = "Literal value found in interpolation"
MAGIC_CONSTANTS = %w[__LINE__ __FILE__ __DIR__]
def test(source, node : Crystal::StringInterpolation)
each_literal_node(source, node) do |exp|
issue_for exp, MSG
end
end
private def each_literal_node(source, node, &)
source_lines = source.lines
node.expressions.each do |exp|
next if exp.is_a?(Crystal::StringLiteral)
next unless static_literal?(exp)
next unless code = node_source(exp, source_lines)
next if code.in?(MAGIC_CONSTANTS)
yield exp
end
end
end
end
================================================
FILE: src/ameba/rule/lint/literals_comparison.cr
================================================
module Ameba::Rule::Lint
# This rule is used to identify comparisons between two literals.
#
# They usually have the same result - except for non-primitive
# types like containers, range or regex.
#
# For example, this will be always false:
#
# ```
# "foo" == 42
# ```
#
# YAML configuration example:
#
# ```
# Lint/LiteralsComparison:
# Enabled: true
# ```
class LiteralsComparison < Base
include AST::Util
properties do
since_version "1.3.0"
description "Identifies comparisons between literals"
end
OP_NAMES = %w[=== == != =~ !~ < <= > >= <=>]
MSG = "Comparison always evaluates to %s"
def test(source, node : Crystal::Call)
return unless node.name.in?(OP_NAMES)
return unless (obj = node.obj) && (arg = node.args.first?)
return unless static_literal?(obj)
return unless static_literal?(arg)
what =
case node.name
when "=="
"`#{obj.to_s == arg.to_s}`"
when "!="
"`#{obj.to_s != arg.to_s}`"
else
"the same"
end
issue_for node, MSG % what
end
end
end
================================================
FILE: src/ameba/rule/lint/missing_block_argument.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows yielding method definitions without block argument.
#
# For example, this is considered invalid:
#
# def foo
# yield 42
# end
#
# And has to be written as the following:
#
# def foo(&)
# yield 42
# end
#
# YAML configuration example:
#
# ```
# Lint/MissingBlockArgument:
# Enabled: true
# ```
class MissingBlockArgument < Base
properties do
since_version "1.4.0"
description "Disallows yielding method definitions without block argument"
end
MSG = "Missing anonymous block argument. Use `&` as an argument " \
"name to indicate yielding method."
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node : Crystal::Def, scope : AST::Scope)
return if !scope.yields? || node.block_arg
issue_for node, MSG, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/lint/non_existent_rule.cr
================================================
module Ameba::Rule::Lint
# A rule that reports non-existent rules in comment directives.
#
# For example, the user can mistakenly add a directive
# to disable a rule that even doesn't exist:
#
# ```
# # ameba:disable BadRuleName
# def foo
# :bar
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/NonExistentRule:
# Enabled: true
# ```
class NonExistentRule < Base
include AST::Util
properties do
since_version "0.13.0"
description "Reports non-existent rules in comment directives"
end
MSG = "Such rules do not exist: %s"
ALL_RULE_NAMES = Rule.rules.map(&.rule_name)
ALL_GROUP_NAMES = Rule.rules.map(&.group_name).uniq!
def test(source)
Tokenizer.new(source).run do |token|
next unless token.type.comment?
next unless directive = source.parse_inline_directive(token.value.to_s)
check_rules source, token, directive[:action], directive[:rules]
end
end
private def check_rules(source, token, action, rules)
bad_names = rules - ALL_RULE_NAMES - ALL_GROUP_NAMES
return if bad_names.empty?
# See `InlineComments::COMMENT_DIRECTIVE_REGEX`
prefix_size = "# ameba:#{action} ".size
token_value = token.value.to_s[prefix_size - 1...-1]?
issue_for name_location_or(token, token_value, adjust_location_column_number: prefix_size),
MSG % bad_names.map { |name| "`#{name}`" }.join(", ")
end
end
end
================================================
FILE: src/ameba/rule/lint/not_nil.cr
================================================
module Ameba::Rule::Lint
# This rule is used to identify usages of `not_nil!` calls.
#
# For example, this is considered a code smell:
#
# ```
# names = %w[Alice Bob]
# alice = names.find { |name| name == "Alice" }.not_nil!
# ```
#
# And can be written as this:
#
# ```
# names = %w[Alice Bob]
# alice = names.find { |name| name == "Alice" }
#
# if alice
# # ...
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/NotNil:
# Enabled: true
# ```
class NotNil < Base
include AST::Util
properties do
since_version "1.3.0"
description "Identifies usage of `not_nil!` calls"
end
MSG = "Avoid using `not_nil!`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "not_nil!" && node.obj
return if has_arguments?(node)
issue_for node, MSG, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/lint/not_nil_after_no_bang.cr
================================================
module Ameba::Rule::Lint
# This rule is used to identify usage of `index/rindex/find/match` calls
# followed by a call to `not_nil!`.
#
# For example, this is considered a code smell:
#
# ```
# %w[Alice Bob].find(&.chars.any?(&.in?('o', 'b'))).not_nil!
# ```
#
# And can be written as this:
#
# ```
# %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b')))
# ```
#
# YAML configuration example:
#
# ```
# Lint/NotNilAfterNoBang:
# Enabled: true
# ```
class NotNilAfterNoBang < Base
include AST::Util
properties do
since_version "1.3.0"
description "Identifies usage of `index/rindex/find/match` calls followed by `not_nil!`"
end
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
BLOCK_CALL_NAMES = %w[index rindex find]
CALL_NAMES = %w[index rindex match]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "not_nil!"
return if has_arguments?(node)
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless obj.name.in?(has_block?(obj) ? BLOCK_CALL_NAMES : CALL_NAMES)
return unless name_location = name_location(obj)
return unless name_location_end = name_end_location(obj)
return unless end_location = name_end_location(node)
msg = MSG % {obj.name, obj.name}
issue_for name_location, end_location, msg do |corrector|
corrector.insert_after(name_location_end, '!')
corrector.remove_trailing(node, {{ ".not_nil!".size }})
end
end
end
end
================================================
FILE: src/ameba/rule/lint/percent_arrays.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows some unwanted symbols in percent string and symbol array literals.
#
# For example, this is usually written by mistake:
#
# ```
# %w["one", "two"]
# %i[:one, :two]
# ```
#
# And the expected example is:
#
# ```
# %w[one two]
# %i[one two]
# ```
#
# YAML configuration example:
#
# ```
# Lint/PercentArrays:
# Enabled: true
# StringArrayUnwantedSymbols: ',"'
# SymbolArrayUnwantedSymbols: ',:'
# ```
class PercentArrays < Base
properties do
since_version "0.3.0"
description "Disallows some unwanted symbols in percent array literals"
string_array_unwanted_symbols %(,")
symbol_array_unwanted_symbols %(,:)
end
MSG = "Symbols `%s` may be unwanted in `%s` array literals"
def test(source)
issue = start_token = nil
Tokenizer.new(source).run do |token|
case token.type
when .string_array_start?, .symbol_array_start?
start_token = token.dup
when .string?
if (_start = start_token) && !issue
issue = array_entry_invalid?(token.value.to_s, _start.raw)
end
when .string_array_end?
if (_start = start_token) && (_issue = issue)
issue_for _start.location, _start.location.adjust(column_number: 1), _issue
end
issue = start_token = nil
end
end
end
private def array_entry_invalid?(entry, array_type)
case array_type
when .starts_with? "%w"
check_array_entry entry, string_array_unwanted_symbols, "%w"
when .starts_with? "%i"
check_array_entry entry, symbol_array_unwanted_symbols, "%i"
end
end
private def check_array_entry(entry, symbols, literal)
MSG % {symbols, literal} if entry.matches?(/[#{Regex.escape(symbols)}]/)
end
end
end
================================================
FILE: src/ameba/rule/lint/rand_zero.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows `rand(0)` and `rand(1)` calls.
# Such calls always return `0`.
#
# For example:
#
# ```
# rand(1)
# ```
#
# Should be written as:
#
# ```
# rand
# # or
# rand(2)
# ```
#
# YAML configuration example:
#
# ```
# Lint/RandZero:
# Enabled: true
# ```
class RandZero < Base
properties do
since_version "0.5.1"
description "Disallows `rand` zero calls"
end
MSG = "`%s` always returns `0`"
def test(source, node : Crystal::Call)
return unless node.name == "rand" &&
node.args.size == 1 &&
(arg = node.args.first).is_a?(Crystal::NumberLiteral) &&
arg.value.in?("0", "1")
issue_for node, MSG % node
end
end
end
================================================
FILE: src/ameba/rule/lint/redundant_string_coercion.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows string conversion in string interpolation,
# which is redundant.
#
# For example, this is considered invalid:
#
# ```
# "Hello, #{name.to_s}"
# ```
#
# And this is valid:
#
# ```
# "Hello, #{name}"
# ```
#
# YAML configuration example:
#
# ```
# Lint/RedundantStringCoercion:
# Enabled: true
# ```
class RedundantStringCoercion < Base
include AST::Util
properties do
since_version "0.12.0"
description "Disallows redundant string conversions in interpolation"
end
MSG = "Redundant use of `Object#to_s` in interpolation"
def test(source, node : Crystal::StringInterpolation)
each_string_coercion_node(node) do |expr|
issue_for name_location(expr), expr.end_location, MSG
end
end
private def each_string_coercion_node(node, &)
node.expressions.each do |exp|
yield exp if exp.is_a?(Crystal::Call) &&
exp.name == "to_s" &&
exp.obj &&
!has_arguments?(exp)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/redundant_with_index.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows redundant `with_index` calls.
#
# For example, this is considered invalid:
#
# ```
# collection.each.with_index do |e|
# # ...
# end
#
# collection.each_with_index do |e, _|
# # ...
# end
# ```
#
# and it should be written as follows:
#
# ```
# collection.each do |e|
# # ...
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/RedundantWithIndex:
# Enabled: true
# ```
class RedundantWithIndex < Base
properties do
since_version "0.11.0"
description "Disallows redundant `with_index` calls"
end
MSG_WITH_INDEX = "Remove redundant `with_index`"
MSG_EACH_WITH_INDEX = "Use `each` instead of `each_with_index`"
def test(source, node : Crystal::Call)
args, block = node.args, node.block
return if block.nil? || args.size > 1
return if with_index_arg?(block)
case node.name
when "with_index"
report source, node, MSG_WITH_INDEX
when "each_with_index"
report source, node, MSG_EACH_WITH_INDEX
end
end
private def with_index_arg?(block : Crystal::Block)
block.args.size >= 2 && block.args.last.name != "_"
end
private def report(source, node, msg)
issue_for node, msg, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/lint/redundant_with_object.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows redundant `each_with_object` calls.
#
# For example, this is considered invalid:
#
# ```
# collection.each_with_object(0) do |e|
# # ...
# end
#
# collection.each_with_object(0) do |e, _|
# # ...
# end
# ```
#
# and it should be written as follows:
#
# ```
# collection.each do |e|
# # ...
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/RedundantWithObject:
# Enabled: true
# ```
class RedundantWithObject < Base
properties do
since_version "0.11.0"
description "Disallows redundant `with_object` calls"
end
MSG = "Use `each` instead of `each_with_object`"
def test(source, node : Crystal::Call)
return if node.name != "each_with_object" ||
node.args.size != 1 ||
!(block = node.block) ||
with_index_arg?(block)
issue_for node, MSG, prefer_name_location: true
end
private def with_index_arg?(block : Crystal::Block)
block.args.size >= 2 && block.args.last.name != "_"
end
end
end
================================================
FILE: src/ameba/rule/lint/require_parentheses.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows method calls with at least one argument, where no
# parentheses are used around the argument list, and a logical operator
# (`&&` or `||`) is used within the argument list.
#
# For example, this is considered invalid:
#
# ```
# if foo.includes? "bar" || foo.includes? "baz"
# # ...
# end
# ```
#
# And need to be written as:
#
# ```
# if foo.includes?("bar") || foo.includes?("baz")
# # ...
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/RequireParentheses:
# Enabled: true
# ```
class RequireParentheses < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows method calls with no parentheses and a logical operator in the argument list"
end
MSG = "Use parentheses in the method call to avoid confusion about precedence"
ALLOWED_CALL_NAMES = %w[[]? []]
def test(source, node : Crystal::Call)
return if node.has_parentheses? ||
!has_arguments?(node) ||
setter_method?(node) ||
node.name.in?(ALLOWED_CALL_NAMES)
node.args.each do |arg|
next unless arg.is_a?(Crystal::BinaryOp)
next unless (right = arg.right).is_a?(Crystal::Call)
next unless has_arguments?(right)
issue_for node, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/lint/self_initialize_definition.cr
================================================
module Ameba::Rule::Lint
# A rule that reports usage of `initialize` method definition with a `self` receiver.
# Such definitions are almost always a typo.
#
# For example, this is considered invalid:
#
# ```
# class Foo
# def self.initialize
# end
# end
# ```
#
# And should be written as:
#
# ```
# class Foo
# def initialize
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/SelfInitializeDefinition:
# Enabled: true
# ```
class SelfInitializeDefinition < Base
properties do
since_version "1.7.0"
description "Reports `initialize` method definitions with a `self` receiver"
end
MSG = "`initialize` method definition should not have a receiver"
def test(source : Source)
AST::NodeVisitor.new self, source, skip: [
Crystal::EnumDef,
Crystal::ModuleDef,
]
end
def test(source, node : Crystal::Def)
return unless node.name == "initialize"
return unless (receiver = node.receiver).is_a?(Crystal::Var)
return unless receiver.name == "self"
if (location = receiver.location) && (end_location = receiver.end_location)
issue_for node, MSG do |corrector|
corrector.remove(location, end_location.adjust(column_number: 1))
end
else
issue_for node, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/lint/shadowed_argument.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows shadowed arguments.
#
# For example, this is considered invalid:
#
# ```
# do_something do |foo|
# foo = 1 # shadows block argument
# foo
# end
#
# def do_something(foo)
# foo = 1 # shadows method argument
# foo
# end
# ```
#
# and it should be written as follows:
#
# ```
# do_something do |foo|
# foo = foo + 42
# foo
# end
#
# def do_something(foo)
# foo = foo + 42
# foo
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/ShadowedArgument:
# Enabled: true
# ```
class ShadowedArgument < Base
properties do
since_version "0.7.0"
description "Disallows shadowed arguments"
end
MSG = "Argument `%s` is assigned before it is used"
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node, scope : AST::Scope)
return unless scope.def? || scope.block?
args = scope.arguments.reject(&.ignored?)
return if args.empty?
# Skip liveness analysis if no argument is ever reassigned
return unless args.any?(&.variable.assignments.present?)
result = AST::LivenessAnalyzer.new(scope).analyze
dead_store_ids = nil
args.each do |arg|
next if result.entry_live_set.includes?(arg.name)
next if arg.variable.captured_by_block?
next if arg.variable.used_in_macro?
next if scope.inner_scopes.any?(&.references?(arg.variable))
assigns = arg.variable.assignments
# Prefer the first non-dead-store assignment (the one whose value
# actually gets used), falling back to the first assignment.
dead_store_ids ||= result.dead_stores.map(&.node.object_id).to_set
target =
assigns.find { |a| !a.node.object_id.in?(dead_store_ids) } ||
assigns.first?
next unless target
issue_for target.node, MSG % arg.name
end
end
end
end
================================================
FILE: src/ameba/rule/lint/shadowed_exception.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows a rescued exception that get shadowed by a
# less specific exception being rescued before a more specific
# exception is rescued.
#
# For example, this is invalid:
#
# ```
# begin
# do_something
# rescue Exception
# handle_exception
# rescue ArgumentError
# handle_argument_error_exception
# end
# ```
#
# And it has to be written as follows:
#
# ```
# begin
# do_something
# rescue ArgumentError
# handle_argument_error_exception
# rescue Exception
# handle_exception
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/ShadowedException:
# Enabled: true
# ```
class ShadowedException < Base
properties do
since_version "0.3.0"
description "Disallows rescued exception that get shadowed"
end
MSG = "Shadowed exception found: `%s`"
def test(source, node : Crystal::ExceptionHandler)
return unless rescues = node.rescues
shadowed(rescues).each do |path|
issue_for path, MSG % path.names.join("::")
end
end
private def shadowed(rescues, catch_all = false)
traversed_types = Set(String).new
rescues = filter_rescues(rescues)
rescues.each_with_object([] of Crystal::Path) do |types, shadowed|
case
when catch_all
shadowed.concat(types)
when types.any?(&.single?("Exception"))
nodes = types.reject(&.single?("Exception"))
shadowed.concat(nodes) unless nodes.empty?
catch_all = true
else
nodes = types.select { |path| traverse(path.to_s, traversed_types) }
shadowed.concat(nodes) unless nodes.empty?
end
end
end
private def filter_rescues(rescues)
rescues.compact_map(&.types.try &.select(Crystal::Path))
end
private def traverse(path, traversed_types)
dup = traversed_types.includes?(path)
dup || (traversed_types << path)
dup
end
end
end
================================================
FILE: src/ameba/rule/lint/shadowing_outer_local_var.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows the usage of the same name as outer local variables
# for block or proc arguments.
#
# For example, this is considered incorrect:
#
# ```
# def some_method
# foo = 1
#
# 3.times do |foo| # shadowing outer `foo`
# end
# end
# ```
#
# and should be written as:
#
# ```
# def some_method
# foo = 1
#
# 3.times do |bar|
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/ShadowingOuterLocalVar:
# Enabled: true
# ```
class ShadowingOuterLocalVar < Base
properties do
since_version "0.7.0"
description "Disallows the usage of the same name as outer local variables " \
"for block or proc arguments"
end
MSG = "Shadowing outer local variable `%s`"
def test(source)
AST::ScopeVisitor.new self, source, skip: [
Crystal::Macro,
Crystal::MacroFor,
]
end
def test(source, node : Crystal::ProcLiteral | Crystal::Block, scope : AST::Scope)
find_shadowing source, scope
end
private def find_shadowing(source, scope)
return unless outer_scope = scope.outer_scope
each_argument_node(scope) do |arg|
# TODO: handle unpacked variables from `Block#unpacks`
next unless name = arg.name.presence
variable = outer_scope.find_variable(name)
next if variable.nil? || !variable.declared_before?(arg)
next if outer_scope.assigns_ivar?(name)
next if outer_scope.assigns_type_dec?(name)
issue_for arg.node, MSG % name, prefer_name_location: true
end
end
private def each_argument_node(scope, &)
scope.arguments.each do |arg|
yield arg unless arg.ignored?
end
end
end
end
================================================
FILE: src/ameba/rule/lint/shared_var_in_fiber.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows using shared variables in fibers,
# which are mutated during iterations.
#
# In most cases it leads to unexpected behaviour and is undesired.
#
# For example, having this example:
#
# ```
# n = 0
# channel = Channel(Int32).new
#
# while n < 3
# n = n + 1
# spawn { channel.send n }
# end
#
# 3.times { puts channel.receive } # => # 3, 3, 3
# ```
#
# The problem is there is only one shared between fibers variable `n`
# and when `channel.receive` is executed its value is `3`.
#
# To solve this, the code above needs to be rewritten to the following:
#
# ```
# n = 0
# channel = Channel(Int32).new
#
# while n < 3
# n = n + 1
# m = n
# spawn do { channel.send m }
# end
#
# 3.times { puts channel.receive } # => # 1, 2, 3
# ```
#
# This rule is able to find the shared variables between fibers, which are mutated
# during iterations. So it reports the issue on the first sample and passes on
# the second one.
#
# There are also other techniques to solve the problem above which are
# [officially documented](https://crystal-lang.org/reference/guides/concurrency.html)
#
# YAML configuration example:
#
# ```
# Lint/SharedVarInFiber:
# Enabled: true
# ```
class SharedVarInFiber < Base
include AST::Util
properties do
since_version "0.12.0"
description "Disallows shared variables in fibers"
end
MSG = "Shared variable `%s` is used in fiber"
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node, scope : AST::Scope)
return unless scope.spawn_block?
scope.references.each do |ref|
next if (variable = scope.find_variable(ref.name)).nil?
next if variable.scope == scope || !mutated_in_loop?(variable)
issue_for ref.node, MSG % variable.name
end
end
# Variable is mutated in loop if it was declared above the loop and assigned inside.
private def mutated_in_loop?(variable)
first_assign_node = variable.assignments.first?.try(&.node)
targets = Set(UInt64).new
variable.assignments.each do |assign|
next if assign.scope.spawn_block?
next if assign.node == first_assign_node
targets << assign.node.object_id
end
targets.present? &&
LoopAncestorVisitor.new(targets, variable.scope.node).any_in_loop?
end
# Checks whether any of the target nodes are inside a loop within the boundary.
# Single traversal for all targets instead of one traversal per target.
private class LoopAncestorVisitor < Crystal::Visitor
include AST::Util
getter? any_in_loop = false
def initialize(@targets : Set(UInt64), boundary : Crystal::ASTNode)
@inside_loop = false
boundary.accept(self)
end
def visit(node : Crystal::ASTNode)
return false if any_in_loop?
if @targets.includes?(node.object_id)
@any_in_loop = @inside_loop
return false
end
true
end
def visit(node : Crystal::While | Crystal::Until)
return false if any_in_loop?
prev = @inside_loop
@inside_loop = true
node.accept_children(self)
@inside_loop = prev unless any_in_loop?
false
end
def visit(node : Crystal::Call)
return false if any_in_loop?
if loop?(node) && (block = node.block)
prev = @inside_loop
@inside_loop = true
block.body.accept(self)
@inside_loop = prev unless any_in_loop?
else
node.accept_children(self)
end
false
end
end
end
end
================================================
FILE: src/ameba/rule/lint/signal_trap.cr
================================================
module Ameba::Rule::Lint
# A rule that reports when `Signal::INT/HUP/TERM.trap` is used,
# which should be replaced with `Process.on_terminate` instead -
# a more portable alternative.
#
# For example, this is considered invalid:
#
# ```
# Signal::INT.trap do
# shutdown
# end
# ```
#
# And it should be written as this:
#
# ```
# Process.on_terminate do
# shutdown
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/SignalTrap:
# Enabled: true
# ```
class SignalTrap < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows `Signal::INT/HUP/TERM.trap` in favor of `Process.on_terminate`"
end
MSG = "Use `Process.on_terminate` instead of `%s.trap`"
def test(source, node : Crystal::Call)
return unless (obj = node.obj).is_a?(Crystal::Path)
return unless path_named?(obj, "Signal::INT", "Signal::HUP", "Signal::TERM")
return unless node.name == "trap"
if (name_location = name_location(node)) && (name_end_location = name_end_location(node))
issue_for node.location, name_end_location, MSG % obj do |corrector|
corrector.replace obj, "Process"
corrector.replace name_location, name_end_location, "on_terminate"
end
else
issue_for node, MSG % obj
end
end
end
end
================================================
FILE: src/ameba/rule/lint/spec_eq_with_bool_or_nil_literal.cr
================================================
module Ameba::Rule::Lint
# Reports `eq(true|false|nil)` expectations in specs.
#
# This is considered bad:
#
# ```
# it "works" do
# foo.is_a?(String).should eq true
# foo.is_a?(Int32).should eq false
# foo.as?(Symbol).should eq nil
# end
# ```
#
# And it should be written as the following:
#
# ```
# it "works" do
# foo.is_a?(String).should be_true
# foo.is_a?(Int32).should be_false
# foo.as?(Symbol).should be_nil
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/SpecEqWithBoolOrNilLiteral:
# Enabled: true
# ```
class SpecEqWithBoolOrNilLiteral < Base
include AST::Util
properties do
since_version "1.7.0"
description "Reports `eq(true|false|nil)` expectations in specs"
end
MSG = "Use `%s` instead of `%s` expectation"
def test(source)
return super if source.spec?
end
def test(source, node : Crystal::Call)
return unless node.name.in?("should", "should_not") && node.args.size == 1
return if has_block?(node)
return unless (matcher = node.args.first).is_a?(Crystal::Call)
return unless matcher.name == "eq" && matcher.args.size == 1
return if has_block?(matcher)
replacement =
case arg = matcher.args.first
when Crystal::BoolLiteral then arg.value ? "be_true" : "be_false"
when Crystal::NilLiteral then "be_nil"
end
return unless replacement
issue_for matcher, MSG % {replacement, matcher.to_s} do |corrector|
corrector.replace(matcher, replacement)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/spec_filename.cr
================================================
require "file_utils"
module Ameba::Rule::Lint
# A rule that enforces spec filenames to have `_spec` suffix.
#
# YAML configuration example:
#
# ```
# Lint/SpecFilename:
# Enabled: true
# IgnoredDirs: [spec/support spec/fixtures spec/data]
# IgnoredFilenames: [spec_helper]
# ```
class SpecFilename < Base
properties do
since_version "1.6.0"
description "Enforces spec filenames to have `_spec` suffix"
ignored_dirs %w[spec/support spec/fixtures spec/data]
ignored_filenames %w[spec_helper]
end
MSG = "Spec filename should have `_spec` suffix: `%s.cr`, not `%s.cr`"
private LOCATION = {1, 1}
# TODO: fix the assumption that *source.path* contains relative path
def test(source : Source)
path_ = Path[source.path].to_posix
name = path_.stem
path = path_.to_s
# check only files within spec/ directory
return unless path.starts_with?("spec/")
# check only files with `.cr` extension
return unless path.ends_with?(".cr")
# ignore files having `_spec` suffix
return if name.ends_with?("_spec")
# ignore known false-positives
ignored_dirs.each do |substr|
return if path.starts_with?("#{substr}/")
end
return if name.in?(ignored_filenames)
expected = "#{name}_spec"
issue_for LOCATION, MSG % {expected, name} do
new_path =
path_.sibling(expected + path_.extension)
FileUtils.mv(path, new_path)
end
end
end
end
================================================
FILE: src/ameba/rule/lint/spec_focus.cr
================================================
module Ameba::Rule::Lint
# Checks if specs are focused.
#
# In specs `focus: true` is mainly used to focus on a spec
# item locally during development. However, if such change
# is committed, it silently runs only focused spec on all
# other environment, which is undesired.
#
# This is considered bad:
#
# ```
# describe MyClass, focus: true do
# end
#
# describe ".new", focus: true do
# end
#
# context "my context", focus: true do
# end
#
# it "works", focus: true do
# end
# ```
#
# And it should be written as the following:
#
# ```
# describe MyClass do
# end
#
# describe ".new" do
# end
#
# context "my context" do
# end
#
# it "works" do
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/SpecFocus:
# Enabled: true
# ```
class SpecFocus < Base
include AST::Util
properties do
since_version "0.14.0"
description "Reports focused spec items"
end
MSG = "Focused spec item detected"
SPEC_ITEM_NAMES = %w[describe context it pending]
def test(source)
return super if source.spec?
end
def test(source, node : Crystal::Call)
return unless node.name.in?(SPEC_ITEM_NAMES)
return unless has_block?(node)
arg = node.named_args.try &.find(&.name.== "focus")
return if arg.nil? ||
arg.value.is_a?(Crystal::Call) ||
arg.value.is_a?(Crystal::Var)
issue_for arg, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/syntax.cr
================================================
module Ameba::Rule::Lint
# A rule that reports invalid Crystal syntax.
#
# For example, this syntax is invalid:
#
# ```
# def hello
# do_something
# rescue Exception => e
# end
# ```
#
# And should be properly written:
#
# ```
# def hello
# do_something
# rescue ex : Exception
# end
# ```
class Syntax < Base
properties do
since_version "0.4.2"
description "Reports invalid Crystal syntax"
severity :error
end
def test(source)
source.ast
rescue ex : Crystal::SyntaxException
issue_for({ex.line_number, ex.column_number}, ex.message.to_s)
end
end
end
================================================
FILE: src/ameba/rule/lint/top_level_operator_definition.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows top level operator method definitions, since these cannot be called.
#
# For example, this is considered invalid:
#
# ```
# def +(other)
# end
# ```
#
# And has to be written within a class, struct, or module:
#
# ```
# class Foo
# def +(other)
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/TopLevelOperatorDefinition:
# Enabled: true
# ```
class TopLevelOperatorDefinition < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows top level operator method definitions"
end
MSG = "Top level operator method definitions cannot be called"
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::ClassDef,
Crystal::EnumDef,
Crystal::ModuleDef,
Crystal::Call,
]
end
def test(source, node : Crystal::Def)
return if node.receiver || !operator_method?(node)
issue_for node, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/trailing_rescue_exception.cr
================================================
module Ameba::Rule::Lint
# A rule that prohibits the misconception about how trailing `rescue` statements work,
# preventing Paths (exception class names or otherwise) from being used as the
# trailing value. The value after the trailing `rescue` statement is the value
# to use if an exception occurs, not the exception class to rescue from.
#
# For example, this is considered invalid - if an exception occurs,
# `response` will be assigned with the value of `IO::Error` instead of `nil`:
#
# ```
# response = HTTP::Client.get("http://www.example.com") rescue IO::Error
# ```
#
# And should instead be written as this in order to capture only `IO::Error` exceptions:
#
# ```
# response = begin
# HTTP::Client.get("http://www.example.com")
# rescue IO::Error
# "default value"
# end
# ```
#
# Or to rescue all exceptions (instead of just `IO::Error`):
#
# ```
# response = HTTP::Client.get("http://www.example.com") rescue "default value"
# ```
#
# YAML configuration example:
#
# ```
# Lint/TrailingRescueException:
# Enabled: true
# ```
class TrailingRescueException < Base
properties do
since_version "1.7.0"
description "Disallows trailing `rescue` with a path"
end
MSG = "Use a block variant of `rescue` to filter by the exception type"
def test(source, node : Crystal::ExceptionHandler)
return unless node.suffix &&
(rescues = node.rescues) &&
(resc = rescues.first?) &&
resc.body.is_a?(Crystal::Path)
issue_for resc.body, MSG, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/lint/typos.cr
================================================
module Ameba::Rule::Lint
# A rule that reports typos found in source files.
#
# NOTE: Needs [typos](https://github.com/crate-ci/typos) CLI tool.
# NOTE: See the chapter on [false positives](https://github.com/crate-ci/typos#false-positives).
#
# YAML configuration example:
#
# ```
# Lint/Typos:
# Enabled: true
# BinPath: ~
# FailOnMissingBin: false
# FailOnError: true
# ```
class Typos < Base
properties do
since_version "1.6.0"
description "Reports typos found in source files"
enabled false
bin_path nil, as: String?
fail_on_missing_bin false
fail_on_error true
end
MSG = "Typo found: `%s` -> %s"
BIN_PATH = Process.find_executable("typos") rescue nil
@@mutex = Mutex.new
private record Typo,
typo : String,
corrections : Array(String),
location : {Int32, Int32},
end_location : {Int32, Int32} do
def self.parse(str) : self?
issue =
JSON.parse(str)
return unless issue["type"] == "typo"
typo = issue["typo"].as_s
corrections = issue["corrections"].as_a.map(&.as_s)
return if typo.empty? || corrections.empty?
line_no = issue["line_num"].as_i
col_no = issue["byte_offset"].as_i + 1
end_col_no = col_no + (typo.size - 1)
new(typo, corrections,
{line_no, col_no},
{line_no, end_col_no})
end
end
protected def self.typos_from(bin_path : String, source : Source) : Array(Typo)?
input, output =
IO::Memory.new(source.code), IO::Memory.new
result = @@mutex.synchronize do
status = Process.run bin_path, args: %w[--format json -],
input: input,
output: output
output.to_s.presence unless status.success?
end
return unless result
([] of Typo).tap do |typos|
# NOTE: `--format json` is actually JSON Lines (`jsonl`)
result.each_line do |line|
Typo.parse(line).try { |typo| typos << typo }
end
end
end
def bin_path : String?
@bin_path || BIN_PATH
end
def test(source : Source)
typos = typos_from(source)
typos.try &.each do |typo|
corrections = typo.corrections
message = MSG % {
typo.typo, corrections.map { |correction| "`#{correction}`" }.join(" | "),
}
if corrections.size == 1
issue_for typo.location, typo.end_location, message do |corrector|
corrector.replace(typo.location, typo.end_location, corrections.first)
end
else
issue_for typo.location, typo.end_location, message
end
end
rescue ex
raise ex if fail_on_error?
end
protected def typos_from(source : Source) : Array(Typo)?
if bin_path = self.bin_path
return Typos.typos_from(bin_path, source)
end
if fail_on_missing_bin?
raise RuntimeError.new "Could not find `typos` executable"
end
end
end
end
================================================
FILE: src/ameba/rule/lint/unneeded_disable_directive.cr
================================================
module Ameba::Rule::Lint
# A rule that reports unneeded disable directives.
# For example, this is considered invalid:
#
# ```
# # ameba:disable Style/PredicateName
# def comment?
# do_something
# end
# ```
#
# As the predicate name is correct and the comment directive does not
# have any effect, the snippet should be written as the following:
#
# ```
# def comment?
# do_something
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnneededDisableDirective:
# Enabled: true
# ```
class UnneededDisableDirective < Base
include AST::Util
properties do
since_version "0.5.0"
description "Reports unneeded disable directives in comments"
end
MSG = "Unnecessary disabling of %s"
def test(source)
test(source, Set(String).new)
end
def test(source, excluded_rules : Set(String))
Tokenizer.new(source).run do |token|
next unless token.type.comment?
next unless directive = source.parse_inline_directive(token.value.to_s)
next unless names = unneeded_disables(source, directive, token.location, excluded_rules)
next if names.empty?
issue_for name_location_or(token, token.value),
MSG % names.map { |name| "`#{name}`" }.join(", ")
end
end
private def unneeded_disables(source, directive, location, excluded_rules)
return unless directive[:action] == "disable"
directive[:rules].reject do |rule_name|
next if rule_name == name
next true if rule_name.in?(excluded_rules)
# skip non-existent rules
next true unless Rule.rules.any?(&.rule_name.== rule_name)
source.issues.any? do |issue|
issue.rule.name == rule_name &&
issue.disabled? &&
issue_at_location?(source, issue, location)
end
end
end
private def issue_at_location?(source, issue, location)
return false unless issue_line_number = issue.location.try(&.line_number)
issue_line_number == location.line_number ||
((prev_line_number = issue_line_number - 1) &&
prev_line_number == location.line_number &&
source.comment?(prev_line_number - 1))
end
end
end
================================================
FILE: src/ameba/rule/lint/unreachable_code.cr
================================================
module Ameba::Rule::Lint
# A rule that reports unreachable code.
#
# For example, this is considered invalid:
#
# ```
# def method(a)
# return 42
# a + 1
# end
# ```
#
# ```
# a = 1
# loop do
# break
# a += 1
# end
# ```
#
# And has to be written as the following:
#
# ```
# def method(a)
# return 42 if a == 0
# a + 1
# end
# ```
#
# ```
# a = 1
# loop do
# break a > 3
# a += 1
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnreachableCode:
# Enabled: true
# ```
class UnreachableCode < Base
properties do
since_version "0.9.0"
description "Reports unreachable code"
end
MSG = "Unreachable code detected"
def test(source)
AST::FlowExpressionVisitor.new self, source
end
def test(source, node, flow_expression : AST::FlowExpression)
return unless unreachable_node = flow_expression.unreachable_nodes.first?
issue_for unreachable_node, MSG
end
end
end
================================================
FILE: src/ameba/rule/lint/unused_argument.cr
================================================
module Ameba::Rule::Lint
# A rule that reports unused arguments.
# For example, this is considered invalid:
#
# ```
# def method(a, b, c)
# a + b
# end
# ```
#
# and should be written as:
#
# ```
# def method(a, b)
# a + b
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnusedArgument:
# Enabled: true
# IgnoreDefs: true
# IgnoreBlocks: false
# IgnoreProcs: false
# ```
class UnusedArgument < Base
properties do
since_version "0.6.0"
description "Disallows unused arguments"
ignore_defs true
ignore_blocks false
ignore_procs false
end
MSG = "Unused argument `%s`. If it's necessary, use `%s` " \
"as an argument name to indicate that it won't be used."
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node : Crystal::ProcLiteral, scope : AST::Scope)
ignore_procs? || find_unused_arguments(source, scope)
end
def test(source, node : Crystal::Block, scope : AST::Scope)
ignore_blocks? || find_unused_arguments(source, scope)
end
def test(source, node : Crystal::Def, scope : AST::Scope)
arguments = scope.arguments.dup
# `Lint/UnusedBlockArgument` rule covers this case explicitly
if block_arg = node.block_arg
arguments.reject!(&.node.== block_arg)
end
ignore_defs? || find_unused_arguments(source, scope, arguments)
end
private def find_unused_arguments(source, scope, arguments = scope.arguments)
arguments.each do |argument|
next if argument.anonymous? || argument.ignored?
next if scope.references?(argument.variable)
name_suggestion = scope.node.is_a?(Crystal::Block) ? '_' : "_#{argument.name}"
message = MSG % {argument.name, name_suggestion}
node = argument.node
location = node.location
name_end_location = location.try &.adjust(column_number: argument.name.size - 1)
if node.responds_to?(:restriction)
end_location = node.restriction.try(&.end_location)
end
end_location ||= name_end_location
if location && name_end_location && end_location
issue_for location, end_location, message do |corrector|
corrector.replace(location, name_end_location, name_suggestion)
end
else
issue_for node, message
end
end
end
end
end
================================================
FILE: src/ameba/rule/lint/unused_block_argument.cr
================================================
module Ameba::Rule::Lint
# A rule that reports unused block arguments.
# For example, this is considered invalid:
#
# ```
# def foo(a, b, &block)
# a + b
# end
#
# def bar(&block)
# yield 42
# end
# ```
#
# and should be written as:
#
# ```
# def foo(a, b, &_block)
# a + b
# end
#
# def bar(&)
# yield 42
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnusedBlockArgument:
# Enabled: true
# ```
class UnusedBlockArgument < Base
include AST::Util
properties do
since_version "1.4.0"
description "Disallows unused block arguments"
end
MSG_UNUSED = "Unused block argument `%1$s`. If it's necessary, use `_%1$s` " \
"as an argument name to indicate that it won't be used."
MSG_YIELDED = "Use `&` as an argument name to indicate that it won't be referenced"
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node : Crystal::Def, scope : AST::Scope)
return if node.abstract?
return unless block_arg = node.block_arg
return unless block_arg = scope.arguments.find(&.node.== block_arg)
return if block_arg.anonymous?
return if scope.references?(block_arg.variable)
location = name_location_or(block_arg.node)
case
when scope.yields?
case location
when Tuple
issue_for *location, MSG_YIELDED do |corrector|
corrector.remove(*location)
end
else
issue_for location, MSG_YIELDED
end
when !block_arg.ignored?
case location
when Tuple
issue_for *location, MSG_UNUSED % block_arg.name do |corrector|
corrector.insert_before(location[0], '_')
end
else
issue_for location, MSG_UNUSED % block_arg.name
end
end
end
end
end
================================================
FILE: src/ameba/rule/lint/unused_expression.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows unused expressions.
#
# For example, this is considered invalid:
#
# ```
# a = obj.method do |x|
# x == 1 # => Comparison operation has no effect
# puts x
# end
#
# Float64 | StaticArray(Float64, 10)
#
# pointerof(foo)
# ```
#
# And these are considered valid:
#
# ```
# a = obj.method do |x|
# x == 1
# end
#
# foo : Float64 | StaticArray(Float64, 10) = 0.1
#
# bar = pointerof(foo)
# ```
#
# This rule currently supports checking for unused:
# - comparison operators: `<`, `>=`, etc.
# - generics and unions: `String?`, `Int32 | Float64`, etc.
# - literals: strings, bools, chars, hashes, arrays, range, etc.
# - pseudo-method calls: `sizeof`, `is_a?` etc.
# - variable access: local, `@ivar`, `@@cvar` and `self`
#
# YAML configuration example:
#
# ```
# Lint/UnusedExpression:
# Enabled: true
# ```
class UnusedExpression < Base
properties do
since_version "1.7.0"
description "Disallows unused expressions"
end
COMPARISON_OPERATORS = %w[== != < <= > >= <=>]
MSG_CLASS_VAR = "Class variable access is unused"
MSG_COMPARISON = "Comparison operation is unused"
MSG_GENERIC = "Generic type is unused"
MSG_UNION = "Union type is unused"
MSG_INSTANCE_VAR = "Instance variable access is unused"
MSG_LITERAL = "Literal value is unused"
MSG_LOCAL_VAR = "Local variable access is unused"
MSG_SELF = "`self` access is unused"
MSG_PSEUDO_METHOD = "Pseudo-method call is unused"
def test(source : Source)
AST::ImplicitReturnVisitor.new(self, source)
end
def test(source, node : Crystal::ClassVar, in_macro : Bool)
# Class variables aren't supported in macros
return if in_macro
issue_for node, MSG_CLASS_VAR
end
def test(source, node : Crystal::Call, in_macro : Bool)
if node.name.in?(COMPARISON_OPERATORS) && node.args.size == 1
issue_for node, MSG_COMPARISON
end
if path_or_generic_union?(node)
issue_for node, MSG_UNION
end
end
def test(source, node : Crystal::Generic, in_macro : Bool)
issue_for node, MSG_GENERIC
end
def test(source, node : Crystal::InstanceVar, in_macro : Bool)
# Handle special case when using `@type` within a method body has side-effects
return if in_macro && node.name == "@type"
issue_for node, MSG_INSTANCE_VAR
end
def test(source, node : Crystal::RegexLiteral, in_macro : Bool)
issue_for node, MSG_LITERAL
end
def test(
source,
node : Crystal::BoolLiteral | Crystal::CharLiteral | Crystal::HashLiteral |
Crystal::ProcLiteral | Crystal::ArrayLiteral | Crystal::RangeLiteral |
Crystal::TupleLiteral | Crystal::NumberLiteral |
Crystal::StringLiteral | Crystal::SymbolLiteral |
Crystal::NamedTupleLiteral | Crystal::StringInterpolation,
in_macro : Bool,
)
issue_for node, MSG_LITERAL
end
def test(source, node : Crystal::Var, in_macro : Bool)
if node.name == "self"
issue_for node, MSG_SELF
return
end
# Ignore `debug` and `skip_file` macro methods
return if in_macro && node.name.in?("debug", "skip_file")
issue_for node, MSG_LOCAL_VAR
end
def test(
source,
node : Crystal::PointerOf | Crystal::SizeOf | Crystal::InstanceSizeOf |
Crystal::AlignOf | Crystal::InstanceAlignOf | Crystal::OffsetOf |
Crystal::IsA | Crystal::NilableCast | Crystal::RespondsTo | Crystal::Not,
in_macro : Bool,
)
issue_for node, MSG_PSEUDO_METHOD
end
private def path_or_generic_union?(node : Crystal::Call) : Bool
node.name == "|" && node.args.size == 1 && !!(obj = node.obj) &&
valid_type_node?(obj) && valid_type_node?(node.args.first)
end
private def valid_type_node?(node : Crystal::ASTNode) : Bool
case node
when Crystal::Path, Crystal::Generic, Crystal::Self, Crystal::TypeOf, Crystal::Underscore
true
when Crystal::Var
node.name == "self"
when Crystal::Call
path_or_generic_union?(node)
else
false
end
end
end
end
================================================
FILE: src/ameba/rule/lint/unused_rescue_variable.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows unused `rescue` variables.
#
# For example, this is considered invalid:
#
# ```
# begin
# raise MyException.new("OH NO!")
# rescue ex : MyException
# puts "Rescued MyException"
# end
# ```
#
# and should be written as:
#
# ```
# begin
# raise MyException.new("OH NO!")
# rescue MyException
# puts "Rescued MyException"
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnusedRescueVariable:
# Enabled: true
# ```
class UnusedRescueVariable < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows unused `rescue` variables"
end
MSG = "Unused `rescue` variable `%s`"
def test(source, node : Crystal::Rescue)
return unless name = node.name
visitor = VariableReferenceVisitor.new(node.body, name)
return if visitor.referenced?
issue_for name_location_or(node, adjust_location_column_number: {{ "rescue ".size }}),
MSG % name
end
end
private class VariableReferenceVisitor < Crystal::Visitor
getter variable_name : String
getter? referenced = false
def initialize(node : Crystal::ASTNode, @variable_name)
node.accept(self)
end
def visit(node : Crystal::Var)
@referenced ||= true if node.name == variable_name
true
end
def visit(node : Crystal::MacroIf | Crystal::MacroFor)
if AST::MacroReferenceFinder.new(node, variable_name).references?
@referenced ||= true
end
false
end
# Shadowed variable usage check
def visit(node : Crystal::Block)
node.args.all? { |arg| should_visit?(arg) }
end
# Shadowed variable usage check
def visit(node : Crystal::ProcLiteral)
node.def.args.all? { |arg| should_visit?(arg) }
end
# Shadowed variable usage check
def visit(node : Crystal::UninitializedVar)
should_visit?(node.var)
end
# Shadowed variable usage check
def visit(node : Crystal::Assign | Crystal::OpAssign)
should_visit?(node.target)
end
# Shadowed variable usage check
def visit(node : Crystal::MultiAssign)
node.targets.all? { |target| should_visit?(target) }
end
def visit(node : Crystal::ASTNode)
true
end
private def should_visit?(node : Crystal::Var | Crystal::Arg)
node.name != variable_name
end
private def should_visit?(node : Crystal::ASTNode)
true
end
end
end
================================================
FILE: src/ameba/rule/lint/useless_assign.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows useless assignments.
#
# For example, this is considered invalid:
#
# ```
# def method
# var = 1
# do_something
# end
# ```
#
# And has to be written as the following:
#
# ```
# def method
# var = 1
# do_something(var)
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UselessAssign:
# Enabled: true
# ```
class UselessAssign < Base
properties do
since_version "0.6.0"
description "Disallows useless variable assignments"
end
MSG = "Useless assignment to variable `%s`"
def test(source)
UselessAssignScopeVisitor.new self, source
end
def test(source, node, scope : AST::Scope)
return if scope.lib_def?(check_outer_scopes: true)
analyzer = AST::LivenessAnalyzer.new(scope)
dead_stores = analyzer.dead_stores
dead_stores.each do |assign|
var = assign.variable
next if var.special? || var.ignored? || var.used_in_macro? || var.captured_by_block?
next if referenced_in_inner_scope?(scope, var)
report_issue(source, assign, var)
end
end
private def referenced_in_inner_scope?(scope, var)
scope.inner_scopes.any?(&.references?(var))
end
private def report_issue(source, assign, var)
case target_node = assign.target_node
when Crystal::TypeDeclaration
issue_for target_node.var, MSG % var.name
else
issue_for target_node, MSG % var.name
end
end
end
private class UselessAssignScopeVisitor < AST::ScopeVisitor
getter? in_call_args = false
private def in_call_args(value = true, &)
prev_value = @in_call_args
begin
@in_call_args = value
yield
ensure
@in_call_args = prev_value
end
end
def visit(node : Crystal::Def)
return super unless node.name == "->"
in_call_args(false) do
node.accept_children(self)
end
false
end
def visit(node : Crystal::Block)
super
in_call_args(false) do
node.accept_children(self)
end
false
end
def visit(node : Crystal::Call)
return false unless super
node.obj.try &.accept(self)
in_call_args do
node.args.each &.accept(self)
node.named_args.try &.each &.accept(self)
end
node.block_arg.try &.accept(self)
node.block.try &.accept(self)
false
end
def visit(node : Crystal::TypeDeclaration)
super unless in_call_args?
end
private def on_assign_end(target, node)
super unless in_call_args?
end
end
end
================================================
FILE: src/ameba/rule/lint/useless_condition_in_when.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows useless conditions in `when` clause
# where it is guaranteed to always return the same result.
#
# For example, this is considered invalid:
#
# ```
# case
# when utc?
# io << " UTC"
# when local?
# Format.new(" %:z").format(self, io) if local?
# end
# ```
#
# And has to be written as the following:
#
# ```
# case
# when utc?
# io << " UTC"
# when local?
# Format.new(" %:z").format(self, io)
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UselessConditionInWhen:
# Enabled: true
# ```
class UselessConditionInWhen < Base
properties do
since_version "0.3.0"
description "Disallows useless conditions in `when`"
end
MSG = "Useless condition in `when` detected"
# TODO: condition *cond* may be a complex ASTNode with
# useless inner conditions. We might need to improve this
# simple implementation in future.
protected def check_node(source, when_node, cond)
return unless cond_s = cond.to_s.presence
return if when_node.conds.none?(&.to_s.==(cond_s))
issue_for cond, MSG
end
def test(source, node : Crystal::When)
ConditionInWhenVisitor.new self, source, node
end
private class ConditionInWhenVisitor < Crystal::Visitor
@source : Source
@rule : UselessConditionInWhen
@parent : Crystal::When
def initialize(@rule, @source, @parent)
@parent.accept self
end
def visit(node : Crystal::If | Crystal::Unless)
@rule.check_node(@source, @parent, node.cond)
true
end
def visit(node : Crystal::ASTNode)
true
end
end
end
end
================================================
FILE: src/ameba/rule/lint/useless_visibility_modifier.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows top level `protected` method visibility modifier,
# since it has no effect.
#
# For example, this is considered invalid:
#
# ```
# protected def foo
# end
# ```
#
# And has to be written as follows:
#
# ```
# def foo
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UselessVisibilityModifier:
# Enabled: true
# ```
class UselessVisibilityModifier < Base
properties do
since_version "1.7.0"
description "Disallows top level `protected` method visibility modifier"
end
MSG = "Useless visibility modifier"
def test(source)
AST::ScopeVisitor.new self, source, skip: [
Crystal::ClassDef,
Crystal::EnumDef,
Crystal::ModuleDef,
]
end
def test(source, node : Crystal::Def, scope : AST::Scope)
return if node.receiver || node.name == "->"
return unless node.visibility.protected?
issue_for node, MSG do |corrector|
corrector.remove_preceding(node, {{ "protected ".size }})
end
end
end
end
================================================
FILE: src/ameba/rule/lint/void_outside_lib.cr
================================================
module Ameba::Rule::Lint
# A rule that disallows uses of `Void` outside C lib bindings.
# Usages of these outside of C lib bindings don't make sense,
# and can sometimes break the compiler. `Nil` should be used instead in these cases.
# `Pointer(Void)` is the only case that's allowed per this rule.
#
# These are considered invalid:
#
# ```
# def foo(bar : Void) : Slice(Void)?
# end
#
# alias Baz = Void
#
# struct Qux < Void
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/VoidOutsideLib:
# Enabled: true
# ```
class VoidOutsideLib < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows use of `Void` outside C lib bindings and `Pointer(Void)`"
end
MSG = "`Void` is not allowed in this context"
def test(source)
PathGenericUnionVisitor.new self, source, skip: [Crystal::LibDef]
end
def test(source, node : Crystal::Path)
return unless path_named?(node, "Void")
issue_for node, MSG
end
def test(source, node : Crystal::Generic)
# Specifically only allow `Pointer(Void)`
return if path_named?(node.name, "Pointer") &&
node.type_vars.size == 1 &&
path_named?(node.type_vars.first, "Void")
if path_named?(node.name, "Void")
issue_for node, MSG, prefer_name_location: true
end
node.type_vars.each do |type_var|
test(source, type_var)
end
end
def test(source, node : Crystal::Union)
node.types.each do |type|
test(source, type)
end
end
private class PathGenericUnionVisitor < AST::NodeVisitor
def visit(node : Crystal::Generic | Crystal::Path | Crystal::Union)
return false if skip?(node)
@rule.test @source, node
false
end
end
end
end
================================================
FILE: src/ameba/rule/lint/whitespace_around_macro_expression.cr
================================================
module Ameba::Rule::Lint
# A rule that checks for whitespace around macro expressions.
#
# This is considered invalid:
#
# ```
# {{foo}}
# ```
#
# And it has to written as this instead:
#
# ```
# {{ foo }}
# ```
#
# YAML configuration example:
#
# ```
# Lint/WhitespaceAroundMacroExpression:
# Enabled: true
# ```
class WhitespaceAroundMacroExpression < Base
include AST::Util
properties do
since_version "1.7.0"
description "Reports missing spaces around macro expressions"
end
MSG = "Missing spaces around macro expression"
def test(source, node : Crystal::MacroExpression)
return unless node.output?
return unless (location = node.location) && location.same_line?(node.end_location)
return unless code = node_source(node, source.lines)
return if code.starts_with?("{{ ") && code.ends_with?(" }}")
issue_for node, MSG do |corrector|
corrected_code =
"{{ #{code[2...-2].strip} }}"
corrector.replace(node, corrected_code)
end
end
end
end
================================================
FILE: src/ameba/rule/metrics/cyclomatic_complexity.cr
================================================
module Ameba::Rule::Metrics
# A rule that disallows methods with a cyclomatic complexity higher than `MaxComplexity`
#
# YAML configuration example:
#
# ```
# Metrics/CyclomaticComplexity:
# Enabled: true
# MaxComplexity: 12
# ```
class CyclomaticComplexity < Base
properties do
since_version "0.9.1"
description "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`"
max_complexity 12
end
MSG = "Cyclomatic complexity too high [%d/%d]"
def test(source, node : Crystal::Def)
complexity = AST::CountingVisitor.new(node).count
return unless complexity > max_complexity
issue_for node, MSG % {complexity, max_complexity}, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/naming/accessor_method_name.cr
================================================
module Ameba::Rule::Naming
# A rule that makes sure that accessor methods are named properly.
#
# Favour this:
#
# ```
# class Foo
# def user
# @user
# end
#
# def user=(value)
# @user = value
# end
# end
# ```
#
# Over this:
#
# ```
# class Foo
# def get_user
# @user
# end
#
# def set_user(value)
# @user = value
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/AccessorMethodName:
# Enabled: true
# ```
class AccessorMethodName < Base
include AST::Util
properties do
since_version "1.6.0"
description "Makes sure that accessor methods are named properly"
end
MSG = "Favour method name `%s` over `%s`"
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
each_def_node(node) do |def_node|
# skip defs with explicit receiver, as they'll be handled
# by the `test(source, node : Crystal::Def)` overload
check_issue(source, def_node) unless def_node.receiver
end
end
def test(source, node : Crystal::Def)
# check only defs with explicit receiver (`def self.foo`)
check_issue(source, node) if node.receiver
end
private def each_def_node(node, &)
case body = node.body
when Crystal::Def
yield body
when Crystal::Expressions
body.expressions.each do |exp|
yield exp if exp.is_a?(Crystal::Def)
end
end
end
private def check_issue(source, node : Crystal::Def)
case node.name
when /^get_([a-z]\w*)$/
return if node.block_arg || takes_arguments?(node)
issue_for node, MSG % {$1, node.name}, prefer_name_location: true
when /^set_([a-z]\w*)$/
return unless node.args.size == 1
issue_for node, MSG % {"#{$1}=", node.name}, prefer_name_location: true
end
end
end
end
================================================
FILE: src/ameba/rule/naming/ascii_identifiers.cr
================================================
module Ameba::Rule::Naming
# A rule that reports non-ascii characters in identifiers.
#
# Favour this:
#
# ```
# class BigAwesomeWolf
# end
# ```
#
# Over this:
#
# ```
# class BigAwesome🐺
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/AsciiIdentifiers:
# Enabled: true
# IgnoreSymbols: false
# ```
class AsciiIdentifiers < Base
properties do
since_version "1.6.0"
description "Disallows non-ascii characters in identifiers"
ignore_symbols false
end
MSG = "Identifier contains non-ascii characters"
def test(source, node : Crystal::Assign)
if (target = node.target).is_a?(Crystal::Path)
check_issue(source, target, target)
end
check_symbol_literal(source, node.value)
end
def test(source, node : Crystal::MultiAssign)
node.values.each do |value|
check_symbol_literal(source, value)
end
end
def test(source, node : Crystal::Call)
node.args.each do |arg|
check_symbol_literal(source, arg)
end
node.named_args.try &.each do |arg|
check_symbol_literal(source, arg.value)
end
end
def test(source, node : Crystal::Def)
check_issue(source, node, prefer_name_location: true)
node.args.each do |arg|
check_issue(source, arg, prefer_name_location: true)
check_symbol_literal(source, arg.default_value)
end
end
def test(source, node : Crystal::ClassVar | Crystal::InstanceVar | Crystal::Var | Crystal::Alias)
check_issue(source, node, prefer_name_location: true)
end
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef | Crystal::EnumDef | Crystal::LibDef)
check_issue(source, node.name, node.name)
end
private def check_symbol_literal(source, node)
return if ignore_symbols?
return unless node.is_a?(Crystal::SymbolLiteral)
check_issue(source, node, node.value)
end
private def check_issue(source, location, end_location, name)
issue_for location, end_location, MSG unless name.to_s.ascii_only?
end
private def check_issue(source, node, name = node.name, *, prefer_name_location = false)
issue_for node, MSG, prefer_name_location: prefer_name_location unless name.to_s.ascii_only?
end
end
end
================================================
FILE: src/ameba/rule/naming/binary_operator_parameter_name.cr
================================================
module Ameba::Rule::Naming
# A rule that enforces that certain binary operator methods have
# standardized parameter names - by default `other`.
#
# For example, this is considered valid:
#
# ```
# class Money
# def +(other)
# end
# end
# ```
#
# And this is invalid parameter name:
#
# ```
# class Money
# def +(amount)
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/BinaryOperatorParameterName:
# Enabled: true
# ExcludedOperators: ["[]", "[]?", "[]=", "<<", ">>", "=~", "!~"]
# AllowedNames: [other]
# ```
class BinaryOperatorParameterName < Base
include AST::Util
properties do
since_version "1.6.0"
description "Enforces that certain binary operator methods have " \
"their sole parameter name standardized"
excluded_operators %w[[] []? []= << >> ` =~ !~]
allowed_names %w[other]
end
MSG = "When defining the `%s` operator, name its argument %s"
def test(source, node : Crystal::Def)
name = node.name
return if !operator_method?(node) || name.in?(excluded_operators)
return unless node.args.size == 1
return if (arg = node.args.first).name.in?(allowed_names)
opts =
allowed_names.map { |val| "`#{val}`" }.join(" or ")
issue_for arg, MSG % {name, opts}, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/naming/block_parameter_name.cr
================================================
module Ameba::Rule::Naming
# A rule that reports non-descriptive block parameter names.
#
# Favour this:
#
# ```
# tokens.each { |token| token.last_accessed_at = Time.utc }
# ```
#
# Over this:
#
# ```
# tokens.each { |t| t.last_accessed_at = Time.utc }
# ```
#
# YAML configuration example:
#
# ```
# Naming/BlockParameterName:
# Enabled: true
# MinNameLength: 3
# AllowNamesEndingInNumbers: true
# AllowedNames: [a, b, e, i, j, k, v, x, y, k1, k2, v1, v2, db, ex, id, io, ip, op, tx, wg, ws]
# ForbiddenNames: []
# ```
class BlockParameterName < Base
properties do
since_version "1.6.0"
description "Disallows non-descriptive block parameter names"
min_name_length 3
allow_names_ending_in_numbers true
allowed_names %w[a b e i j k v x y k1 k2 v1 v2 db ex id io ip op tx wg ws]
forbidden_names %w[]
end
MSG = "Disallowed block parameter name found"
def test(source, node : Crystal::Call)
node.try(&.block).try(&.args).try &.each do |arg|
next if valid_name?(arg.name)
issue_for arg, MSG, prefer_name_location: true
end
end
private def valid_name?(name)
return true if name.blank? # TODO: handle unpacked variables
return true if name.starts_with?('_') || name.in?(allowed_names)
return false if name.in?(forbidden_names)
return false if name.size < min_name_length
return false if name[-1].ascii_number? && !allow_names_ending_in_numbers?
true
end
end
end
================================================
FILE: src/ameba/rule/naming/constant_names.cr
================================================
module Ameba::Rule::Naming
# A rule that enforces constant names to be in screaming case.
#
# For example, these constant names are considered valid:
#
# ```
# LUCKY_NUMBERS = [3, 7, 11]
# DOCUMENTATION_URL = "http://crystal-lang.org/docs"
# ```
#
# And these are invalid names:
#
# ```
# myBadConstant = 1
# Wrong_NAME = 2
# ```
#
# YAML configuration example:
#
# ```
# Naming/ConstantNames:
# Enabled: true
# ```
class ConstantNames < Base
properties do
since_version "0.2.0"
description "Enforces constant names to be in screaming case"
end
MSG = "Constant name should be screaming-cased: `%s`, not `%s`"
def test(source, node : Crystal::Assign)
return unless (target = node.target).is_a?(Crystal::Path)
name = target.names.last
expected = name.upcase
return if name.in?(expected, name.camelcase)
issue_for target, MSG % {expected, name}
end
end
end
================================================
FILE: src/ameba/rule/naming/filename.cr
================================================
module Ameba::Rule::Naming
# A rule that enforces file names to be in underscored case.
#
# YAML configuration example:
#
# ```
# Naming/Filename:
# Enabled: true
# ```
class Filename < Base
properties do
since_version "1.6.0"
description "Enforces file names to be in underscored case"
end
MSG = "Filename should be underscore-cased: `%s`, not `%s`"
private LOCATION = {1, 1}
def test(source : Source)
path = Path[source.path]
name = path.basename
return if (expected = name.underscore) == name
issue_for LOCATION, MSG % {expected, name}
end
end
end
================================================
FILE: src/ameba/rule/naming/method_names.cr
================================================
module Ameba::Rule::Naming
# A rule that enforces method names to be in underscored case.
#
# For example, these are considered valid:
#
# ```
# class Person
# def first_name
# end
#
# def date_of_birth
# end
#
# def homepage_url
# end
# end
# ```
#
# And these are invalid method names:
#
# ```
# class Person
# def firstName
# end
#
# def date_of_Birth
# end
#
# def homepageURL
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/MethodNames:
# Enabled: true
# ```
class MethodNames < Base
properties do
since_version "0.2.0"
description "Enforces method names to be in underscored case"
end
MSG = "Method name should be underscore-cased: `%s`, not `%s`"
def test(source, node : Crystal::Def)
name = node.name.to_s
return if (expected = name.underscore) == name
issue_for node, MSG % {expected, name}, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/naming/predicate_name.cr
================================================
module Ameba::Rule::Naming
# A rule that disallows tautological predicate names -
# meaning those that start with the prefix `is_`, except for
# the ones that are not valid Crystal code (e.g. `is_404?`).
#
# Favour this:
#
# ```
# def valid?(x)
# end
# ```
#
# Over this:
#
# ```
# def is_valid?(x)
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/PredicateName:
# Enabled: true
# ```
class PredicateName < Base
properties do
since_version "0.2.0"
description "Disallows tautological predicate names"
end
MSG = "Favour method name `%s?` over `%s`"
def test(source, node : Crystal::Def)
return unless node.name =~ /^is_([a-z]\w*)\??$/
alternative = $1
issue_for node, MSG % {alternative, node.name}, prefer_name_location: true
end
end
end
================================================
FILE: src/ameba/rule/naming/query_bool_methods.cr
================================================
module Ameba::Rule::Naming
# A rule that disallows boolean properties without the `?` suffix - defined
# using `Object#(class_)property` or `Object#(class_)getter` macros.
#
# Favour this:
#
# ```
# class Person
# property? deceased = false
# getter? witty = true
# end
# ```
#
# Over this:
#
# ```
# class Person
# property deceased = false
# getter witty = true
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/QueryBoolMethods:
# Enabled: true
# ```
class QueryBoolMethods < Base
include AST::Util
properties do
since_version "1.4.0"
description "Reports boolean properties without the `?` suffix"
end
MSG = "Consider using `%s?` for `%s`"
CALL_NAMES = %w[getter class_getter property class_property]
def test(source, node : Crystal::ClassDef | Crystal::ModuleDef)
each_call_node(node) do |exp|
next unless exp.name.in?(CALL_NAMES)
exp.args.each do |arg|
name_node, is_bool =
case arg
when Crystal::Assign
{arg.target, arg.value.is_a?(Crystal::BoolLiteral)}
when Crystal::TypeDeclaration
{arg.var, path_named?(arg.declared_type, "Bool")}
else
{nil, false}
end
if name_node && is_bool
issue_for name_node, MSG % {exp.name, name_node}
end
end
end
end
private def each_call_node(node, &)
case body = node.body
when Crystal::Call
yield body
when Crystal::Expressions
body.expressions.each do |exp|
yield exp if exp.is_a?(Crystal::Call)
end
end
end
end
end
================================================
FILE: src/ameba/rule/naming/rescued_exceptions_variable_name.cr
================================================
module Ameba::Rule::Naming
# A rule that makes sure that rescued exceptions variables are named as expected.
#
# For example, these are considered valid:
#
# def foo
# # potentially raising computations
# rescue ex
# Log.error(exception: ex) { "Error" }
# end
#
# And these are invalid variable names:
#
# def foo
# # potentially raising computations
# rescue wtf
# Log.error(exception: wtf) { "Error" }
# end
#
# YAML configuration example:
#
# ```
# Naming/RescuedExceptionsVariableName:
# Enabled: true
# AllowedNames: [e, ex, exception, err, error]
# ```
class RescuedExceptionsVariableName < Base
include AST::Util
properties do
since_version "1.6.0"
description "Makes sure that rescued exceptions variables are named as expected"
allowed_names %w[e ex exception err error]
end
MSG = "Disallowed variable name, use one of these instead: %s"
MSG_SINGULAR = "Disallowed variable name, use %s instead"
def test(source, node : Crystal::Rescue)
return unless name = node.name
return if name.in?(allowed_names)
message =
allowed_names.size == 1 ? MSG_SINGULAR : MSG
issue_for name_location_or(node, adjust_location_column_number: {{ "rescue ".size }}),
message % allowed_names.map { |val| "`#{val}`" }.join(", ")
end
end
end
================================================
FILE: src/ameba/rule/naming/type_names.cr
================================================
module Ameba::Rule::Naming
# A rule that enforces type names in camelcase manner.
#
# For example, these are considered valid:
#
# ```
# class ParseError < Exception
# end
#
# module HTTP
# class RequestHandler
# end
# end
#
# alias NumericValue = Float32 | Float64 | Int32 | Int64
#
# lib LibYAML
# end
#
# struct TagDirective
# end
#
# enum Time::DayOfWeek
# end
# ```
#
# And these are invalid type names
#
# ```
# class My_class
# end
#
# module HTT_p
# end
#
# alias Numeric_value = Int32
#
# lib Lib_YAML
# end
#
# struct Tag_directive
# end
#
# enum Time_enum::Day_of_week
# end
# ```
#
# YAML configuration example:
#
# ```
# Naming/TypeNames:
# Enabled: true
# ```
class TypeNames < Base
properties do
since_version "0.2.0"
description "Enforces type names in camelcase manner"
end
MSG = "Type name should be camelcased: `%s`, not `%s`"
def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef)
name = node.name.to_s
return if (expected = name.camelcase) == name
issue_for node.name, MSG % {expected, name}
end
end
end
================================================
FILE: src/ameba/rule/naming/variable_names.cr
================================================
module Ameba::Rule::Naming
# A rule that enforces variable names to be in underscored case.
#
# For example, these variable names are considered valid:
#
# ```
# var_name = 1
# name = 2
# _another_good_name = 3
# ```
#
# And these are invalid variable names:
#
# ```
# myBadNamedVar = 1
# wrong_Name = 2
# ```
#
# YAML configuration example:
#
# ```
# Naming/VariableNames:
# Enabled: true
# ```
class VariableNames < Base
properties do
since_version "0.2.0"
description "Enforces variable names to be in underscored case"
end
MSG = "Variable name should be underscore-cased: `%s`, not `%s`"
def test(source : Source)
VarVisitor.new self, source
end
def test(source, node : Crystal::Var | Crystal::InstanceVar | Crystal::ClassVar)
name = node.name.to_s
return if (expected = name.underscore) == name
issue_for node, MSG % {expected, name}, prefer_name_location: true
end
private class VarVisitor < AST::NodeVisitor
private getter var_locations = [] of Crystal::Location
def visit(node : Crystal::Var)
!node.location.in?(var_locations) && super
end
def visit(node : Crystal::InstanceVar | Crystal::ClassVar)
if location = node.location
var_locations << location
end
super
end
end
end
end
================================================
FILE: src/ameba/rule/performance/any_after_filter.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `any?` calls that follow filters.
#
# For example, this is considered invalid:
#
# ```
# [1, 2, 3].select { |e| e > 2 }.any?
# [1, 2, 3].reject { |e| e >= 2 }.any?
# ```
#
# And it should be written as this:
#
# ```
# [1, 2, 3].any? { |e| e > 2 }
# [1, 2, 3].any? { |e| e < 2 }
# ```
#
# YAML configuration example:
#
# ```
# Performance/AnyAfterFilter:
# Enabled: true
# FilterNames:
# - select
# - reject
# ```
class AnyAfterFilter < Base
include AST::Util
properties do
since_version "0.8.1"
description "Identifies usage of `any?` calls that follow filters"
filter_names %w[select reject]
end
MSG = "Use `any? {...}` instead of `%s {...}.any?`"
def test(source, node : Crystal::Call)
return unless node.name == "any?" && (obj = node.obj)
return if has_block?(node)
return unless obj.is_a?(Crystal::Call) && has_block?(obj)
return unless obj.name.in?(filter_names)
issue_for name_location(obj), name_end_location(node), MSG % obj.name
end
end
end
================================================
FILE: src/ameba/rule/performance/any_instead_of_present.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of arg-less `Enumerable#any?` calls.
#
# Using `Enumerable#any?` instead of `Enumerable#present?` might lead to an
# unexpected results (like `[nil, false].any? # => false`). In some cases
# it also might be less efficient, since it iterates until the block will
# return a _truthy_ value, instead of just checking if there's at least
# one value present.
#
# For example, this is considered invalid:
#
# ```
# [1, 2, 3].any?
# ```
#
# And it should be written as this:
#
# ```
# [1, 2, 3].present?
# ```
#
# YAML configuration example:
#
# ```
# Performance/AnyInsteadOfPresent:
# Enabled: true
# ```
class AnyInsteadOfPresent < Base
include AST::Util
properties do
since_version "1.7.0"
description "Identifies usage of arg-less `any?` calls"
end
MSG = "Use `{...}.present?` instead of `{...}.any?`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "any?" && (obj = node.obj)
return if has_arguments?(node) || has_block?(node)
issue_for node, MSG, prefer_name_location: true do |corrector|
obj_code =
node_source(obj, source.lines)
corrector.replace(node, "#{obj_code}.present?")
end
end
end
end
================================================
FILE: src/ameba/rule/performance/base.cr
================================================
require "../base"
module Ameba::Rule::Performance
# A general base class for performance rules.
abstract class Base < Ameba::Rule::Base
def catch(source : Source)
source.spec? ? source : super
end
end
end
================================================
FILE: src/ameba/rule/performance/chained_call_with_no_bang.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of chained calls not utilizing
# the bang method variants.
#
# For example, this is considered inefficient:
#
# ```
# names = %w[Alice Bob]
# chars = names
# .flat_map(&.chars)
# .uniq
# .sort
# ```
#
# And can be written as this:
#
# ```
# names = %w[Alice Bob]
# chars = names
# .flat_map(&.chars)
# .uniq!
# .sort!
# ```
#
# YAML configuration example:
#
# ```
# Performance/ChainedCallWithNoBang:
# Enabled: true
# CallNames:
# - uniq
# - unstable_sort
# - sort
# - sort_by
# - shuffle
# - reverse
# ```
class ChainedCallWithNoBang < Base
include AST::Util
properties do
since_version "0.14.0"
description "Identifies usage of chained calls not utilizing the bang method variants"
# All of those have bang method variants returning `self`
# and are not modifying the receiver type (like `compact` does),
# thus are safe to switch to the bang variant.
call_names %w[uniq unstable_sort sort sort_by shuffle reverse]
end
MSG = "Use bang method variant `%s!` after chained `%s` call"
# All these methods allocate a new object
ALLOCATING_METHOD_NAMES = %w[
keys values values_at map map_with_index flat_map compact_map
flatten compact select reject sample group_by chunks tally merge
combinations repeated_combinations permutations repeated_permutations
transpose invert split chars lines captures named_captures clone
]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless node.name.in?(call_names)
return unless obj.name.in?(call_names) || obj.name.in?(ALLOCATING_METHOD_NAMES)
if end_location = name_end_location(node)
issue_for node, MSG % {node.name, obj.name}, prefer_name_location: true do |corrector|
corrector.insert_after(end_location, '!')
end
else
issue_for node, MSG % {node.name, obj.name}, prefer_name_location: true
end
end
end
end
================================================
FILE: src/ameba/rule/performance/compact_after_map.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `compact` calls that follow `map`.
#
# For example, this is considered inefficient:
#
# ```
# %w[Alice Bob].map(&.match(/^A./)).compact
# ```
#
# And can be written as this:
#
# ```
# %w[Alice Bob].compact_map(&.match(/^A./))
# ```
#
# YAML configuration example:
#
# ```
# Performance/CompactAfterMap:
# Enabled: true
# ```
class CompactAfterMap < Base
include AST::Util
properties do
since_version "0.14.0"
description "Identifies usage of `compact` calls that follow `map`"
end
MSG = "Use `compact_map {...}` instead of `map {...}.compact`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "compact" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && has_block?(obj)
return unless obj.name == "map"
issue_for name_location(obj), name_end_location(node), MSG
end
end
end
================================================
FILE: src/ameba/rule/performance/excessive_allocations.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify excessive collection allocations,
# that can be avoided by using `each_` instead of `.each`.
#
# For example, this is considered inefficient:
#
# ```
# "Alice".chars.each { |c| puts c }
# "Alice\nBob".lines.each { |l| puts l }
# ```
#
# And can be written as this:
#
# ```
# "Alice".each_char { |c| puts c }
# "Alice\nBob".each_line { |l| puts l }
# ```
#
# YAML configuration example:
#
# ```
# Performance/ExcessiveAllocations:
# Enabled: true
# CallNames:
# codepoints: each_codepoint
# graphemes: each_grapheme
# chars: each_char
# lines: each_line
# ```
class ExcessiveAllocations < Base
include AST::Util
properties do
since_version "1.5.0"
description "Identifies usage of excessive collection allocations"
call_names({
"codepoints" => "each_codepoint",
"graphemes" => "each_grapheme",
"chars" => "each_char",
"lines" => "each_line",
# "keys" => "each_key",
# "values" => "each_value",
# "children" => "each_child",
})
end
MSG = "Use `%s {...}` instead of `%s.each {...}` to avoid excessive allocation"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "each"
return if has_arguments?(node)
return unless (obj = node.obj).is_a?(Crystal::Call)
return if has_arguments?(obj) || has_block?(obj)
return unless method = call_names[obj.name]?
return unless name_location = name_location(obj)
return unless end_location = name_end_location(node)
msg = MSG % {method, obj.name}
issue_for name_location, end_location, msg do |corrector|
corrector.replace(name_location, end_location, method)
end
end
end
end
================================================
FILE: src/ameba/rule/performance/first_last_after_filter.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `first/last/first?/last?` calls that follow filters.
#
# For example, this is considered inefficient:
#
# ```
# [-1, 0, 1, 2].select { |e| e > 0 }.first?
# [-1, 0, 1, 2].select { |e| e > 0 }.last?
# ```
#
# And can be written as this:
#
# ```
# [-1, 0, 1, 2].find { |e| e > 0 }
# [-1, 0, 1, 2].reverse_each.find { |e| e > 0 }
# ```
#
# YAML configuration example:
#
# ```
# Performance/FirstLastAfterFilter:
# Enabled: true
# FilterNames:
# - select
# ```
class FirstLastAfterFilter < Base
include AST::Util
properties do
since_version "0.8.1"
description "Identifies usage of `first/last/first?/last?` calls that follow filters"
filter_names %w[select]
end
MSG = "Use `find {...}` instead of `%s {...}.%s`"
MSG_REVERSE = "Use `reverse_each.find {...}` instead of `%s {...}.%s`"
CALL_NAMES = %w[first last first? last?]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name.in?(CALL_NAMES)
return if has_arguments?(node) || has_block?(node)
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless obj.name.in?(filter_names) && has_block?(obj)
message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
issue_for name_location(obj), name_end_location(node),
message % {obj.name, node.name}
end
end
end
================================================
FILE: src/ameba/rule/performance/flatten_after_map.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `flatten` calls that follow `map`.
#
# For example, this is considered inefficient:
#
# ```
# %w[Alice Bob].map(&.chars).flatten
# ```
#
# And can be written as this:
#
# ```
# %w[Alice Bob].flat_map(&.chars)
# ```
#
# YAML configuration example:
#
# ```
# Performance/FlattenAfterMap:
# Enabled: true
# ```
class FlattenAfterMap < Base
include AST::Util
properties do
since_version "0.14.0"
description "Identifies usage of `flatten` calls that follow `map`"
end
MSG = "Use `flat_map {...}` instead of `map {...}.flatten`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "flatten" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && has_block?(obj)
return unless obj.name == "map"
issue_for name_location(obj), name_end_location(node), MSG
end
end
end
================================================
FILE: src/ameba/rule/performance/map_instead_of_block.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `sum/product` calls
# that follow `map`.
#
# For example, this is considered inefficient:
#
# ```
# (1..3).map(&.*(2)).sum
# ```
#
# And can be written as this:
#
# ```
# (1..3).sum(&.*(2))
# ```
#
# YAML configuration example:
#
# ```
# Performance/MapInsteadOfBlock:
# Enabled: true
# ```
class MapInsteadOfBlock < Base
include AST::Util
properties do
since_version "0.14.0"
description "Identifies usage of `sum/product` calls that follow `map`"
end
MSG = "Use `%s {...}` instead of `map {...}.%s`"
CALL_NAMES = %w[sum product]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name.in?(CALL_NAMES) && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name == "map"
issue_for name_location(obj), name_end_location(node),
MSG % {node.name, node.name}
end
end
end
================================================
FILE: src/ameba/rule/performance/minmax_after_map.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `min/max/minmax` calls that follow `map`.
#
# For example, this is considered invalid:
#
# ```
# %w[Alice Bob].map(&.size).min
# %w[Alice Bob].map(&.size).max
# %w[Alice Bob].map(&.size).minmax
# ```
#
# And it should be written as this:
#
# ```
# %w[Alice Bob].min_of(&.size)
# %w[Alice Bob].max_of(&.size)
# %w[Alice Bob].minmax_of(&.size)
# ```
#
# YAML configuration example:
#
# ```
# Performance/MinMaxAfterMap:
# Enabled: true
# ```
class MinMaxAfterMap < Base
include AST::Util
properties do
since_version "1.5.0"
description "Identifies usage of `min/max/minmax` calls that follow `map`"
end
MSG = "Use `%s {...}` instead of `map {...}.%s`"
CALL_NAMES = %w[min min? max max? minmax minmax?]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name.in?(CALL_NAMES)
return if has_arguments?(node) || has_block?(node)
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless obj.name == "map" && has_block?(obj)
return if has_arguments?(obj)
return unless name_location = name_location(obj)
return unless end_location = name_end_location(node)
of_name = node.name.sub(/(.+?)(\?)?$/, "\\1_of\\2")
message = MSG % {of_name, node.name}
issue_for name_location, end_location, message do |corrector|
next unless node_name_location = name_location(node)
# TODO: switching the order of the below calls breaks the corrector
corrector.replace(
name_location,
name_location.adjust(column_number: {{ "map".size - 1 }}),
of_name
)
corrector.remove(
node_name_location.adjust(column_number: -1),
end_location
)
end
end
end
end
================================================
FILE: src/ameba/rule/performance/size_after_filter.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `size` calls that follow filter.
#
# For example, this is considered invalid:
#
# ```
# [1, 2, 3].select { |e| e > 2 }.size
# [1, 2, 3].reject { |e| e < 2 }.size
# [1, 2, 3].select(&.< 2).size
# [0, 1, 2].select(&.zero?).size
# [0, 1, 2].reject(&.zero?).size
# ```
#
# And it should be written as this:
#
# ```
# [1, 2, 3].count { |e| e > 2 }
# [1, 2, 3].count { |e| e >= 2 }
# [1, 2, 3].count(&.< 2)
# [0, 1, 2].count(&.zero?)
# [0, 1, 2].count(&.!= 0)
# ```
#
# YAML configuration example:
#
# ```
# Performance/SizeAfterFilter:
# Enabled: true
# FilterNames:
# - select
# - reject
# ```
class SizeAfterFilter < Base
include AST::Util
properties do
since_version "0.8.1"
description "Identifies usage of `size` calls that follow filter"
filter_names %w[select reject]
end
MSG = "Use `count {...}` instead of `%s {...}.size`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "size" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && has_block?(obj)
return unless obj.name.in?(filter_names)
issue_for name_location(obj), name_end_location(node), MSG % obj.name
end
end
end
================================================
FILE: src/ameba/rule/performance/times_map.cr
================================================
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `times.map { ... }.to_a` calls.
#
# For example, this is considered invalid:
#
# ```
# 5.times.map { |i| i * 2 }.to_a
# ```
#
# And it should be written as this:
#
# ```
# Array.new(5) { |i| i * 2 }
# ```
#
# YAML configuration example:
#
# ```
# Performance/TimesMap:
# Enabled: true
# ```
class TimesMap < Base
include AST::Util
properties do
since_version "1.7.0"
description "Identifies usage of `times.map { ... }.to_a` calls"
end
MSG = "Use `Array.new(%1$s) {...}` instead of `%1$s.times.map {...}.to_a`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
# ameba:disable Metrics/CyclomaticComplexity
def test(source, node : Crystal::Call)
return if has_block?(node)
return unless node.name == "to_a" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && has_block?(obj)
return unless obj.name == "map" && (obj2 = obj.obj)
return if !obj2.is_a?(Crystal::Call) || has_block?(obj2)
return unless obj2.name == "times" && (obj3 = obj2.obj)
return unless location = obj3.location
return unless end_location = name_end_location(node)
issue_for location, end_location, MSG % obj3 do |corrector|
corrected_code =
case
when block = obj.block
block_code =
node_source(block, source.lines)
"Array.new(%s) %s" % {obj3, block_code}
when block_arg = obj.block_arg
"Array.new(%s, &%s)" % {obj3, block_arg}
end
next unless corrected_code
corrector.replace(location, end_location, corrected_code)
end
end
end
end
================================================
FILE: src/ameba/rule/style/array_literal_syntax.cr
================================================
module Ameba::Rule::Style
# Encourages the use of `Array(T).new` syntax for creating an array over `[] of T`.
#
# Favour this:
#
# ```
# Array(Int32 | String?).new
# ```
#
# Over this:
#
# ```
# [] of Int32 | String?
# ```
#
# YAML configuration example:
#
# ```
# Style/ArrayLiteralSyntax:
# Enabled: true
# ```
class ArrayLiteralSyntax < Base
properties do
since_version "1.7.0"
enabled false
description "Encourages the use of `Array(T).new` over `[] of T`"
end
MSG = "Use `Array(%s).new` for creating an empty array"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::ArrayLiteral)
return unless node.elements.empty? && (array_type = node.of)
issue_for node, MSG % array_type do |corrector|
corrector.replace(node, "Array(#{array_type}).new")
end
end
end
end
================================================
FILE: src/ameba/rule/style/call_parentheses.cr
================================================
module Ameba::Rule::Style
# A rule that enforces usage of parentheses in method or macro calls.
#
# For example, this (and all of its variants) is considered invalid:
#
# ```
# user.update name: "John", age: 30
# ```
#
# And should be replaced by the following:
#
# ```
# user.update(name: "John", age: 30)
# ```
#
# ### Options
#
# - `ExcludeTypeDeclarations` — controls whether calls with type declarations should be checked.
# - `ExcludeHeredocs` — controls whether calls with heredoc arguments should be checked.
# - `ExcludedToplevelCallNames` — contains a list of top-level method names that should not be checked.
# - `ExcludedCallNames` — contains a list of non-top-level method names that should not be checked.
#
# YAML configuration example:
#
# ```
# Style/CallParentheses:
# Enabled: true
# ExcludeTypeDeclarations: true
# ExcludeHeredocs: false
# ExcludedToplevelCallNames: [spawn, raise, super, previous_def, exit, abort, sleep, print, printf, puts, p, p!, pp, pp!, record, class_getter, class_getter?, class_getter!, class_property, class_property?, class_property!, class_setter, getter, getter?, getter!, property, property?, property!, setter, def_equals_and_hash, def_equals, def_hash, delegate, forward_missing_to, describe, context, it, pending, fail, use_json_discriminator]
# ExcludedCallNames: [should, should_not]
# ```
class CallParentheses < Base
include AST::Util
properties do
since_version "1.7.0"
description "Enforces usage of parentheses in method calls"
enabled false
exclude_type_declarations true
exclude_heredocs false
excluded_toplevel_call_names %w[
spawn raise super previous_def exit abort sleep
print printf puts p p! pp pp! record
class_getter class_getter? class_getter!
class_property class_property? class_property!
class_setter getter getter? getter!
property property? property! setter
def_equals_and_hash def_equals def_hash
delegate forward_missing_to
describe context it pending fail
use_json_discriminator
]
excluded_call_names %w[should should_not]
end
MSG = "Missing parentheses in method call"
def test(source)
super unless source.ecr?
end
# ameba:disable Metrics/CyclomaticComplexity
def test(source, node : Crystal::Call)
return if node.args_in_brackets? ||
node.has_parentheses? ||
node.expansion?
return if setter_method?(node) ||
operator_method?(node)
return if exclude_type_declarations? &&
node.args.any?(Crystal::TypeDeclaration)
heredoc_arg = find_heredoc_arg(node, source)
return if exclude_heredocs? && heredoc_arg
return if !node.obj && node.name.in?(excluded_toplevel_call_names)
return if node.obj && node.name.in?(excluded_call_names)
return unless node.block_arg ||
has_arguments?(node) ||
has_short_block?(node, source.lines)
location, end_location =
replacement_locations(node, heredoc_arg, source.lines)
if location && end_location
line = source.lines[location.line_number - 1]
rest = line[(location.column_number - 1)..-1]
location_end = location
location_end = location.with(column_number: line.size) if rest.strip == "\\"
issue_for node, MSG do |corrector|
corrector.replace(location, location_end, "(")
corrector.insert_before(end_location, ")")
end
else
issue_for node, MSG
end
end
# Returns the replacement start and end locations for the call *node*.
#
# foo.bar baz: 42 do |what, is|
# ^--- x ^--- y
# # ...
# end
#
private def replacement_locations(node, heredoc_arg, source_lines)
location = name_end_location(node)
end_location =
case
when block = node.block
if short_block?(block, source_lines)
block.body.end_location
else
block.location.try(&.adjust(column_number: -2))
end
when heredoc_arg
if arg_location = heredoc_arg.location
if line = source_lines[arg_location.line_number - 1]?
if line.rstrip.ends_with?(',')
node.end_location
else
arg_location.with(column_number: line.size)
end
end
end
else
if node_end_location = node.end_location
# handle edge-cases in which the end location is not valid
node_end_location if node_end_location.line_number.positive? &&
node_end_location.column_number.positive?
end
end
location &&= location.adjust(column_number: 1)
end_location &&= end_location.adjust(column_number: 1)
{location, end_location}
end
private def find_heredoc_arg(node : Crystal::Call, source)
node.named_args.try &.reverse_each.find { |arg| find_heredoc_arg(arg.value, source) } ||
node.args.reverse_each.find { |arg| find_heredoc_arg(arg, source) }
end
private def find_heredoc_arg(node, source)
heredoc?(node, source)
end
end
end
================================================
FILE: src/ameba/rule/style/elsif.cr
================================================
module Ameba::Rule::Style
# A rule that encourages the use of `case/when` syntax over `if/elsif`.
#
# For example, this is considered invalid:
#
# ```
# if foo
# do_something_foo
# elsif bar
# do_something_bar
# end
# ```
#
# And should be replaced by the following:
#
# ```
# case
# when foo
# do_something_foo
# when bar
# do_something_bar
# end
# ```
#
# If `IgnoreSuffix` option is set to `true` (which is the default),
# the suffix `if` nodes will be ignored, i.e., considered valid.
#
# ```
# if foo
# do_something
# else
# do_something_else if bar # <- suffix if node
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/Elsif:
# Enabled: true
# IgnoreSuffix: true
# MaxBranches: 0
# ```
class Elsif < Base
properties do
since_version "1.7.0"
description "Encourages the use of `case/when` syntax over `if/elsif`"
enabled false
ignore_suffix true
max_branches 0
end
MSG = "Prefer `case/when` over `if/elsif`"
def test(source)
AST::ElseIfAwareNodeVisitor.new self, source, skip: :macro,
exclude_suffix: ignore_suffix?
end
def test(source, node : Crystal::If, ifs : Enumerable(Crystal::If))
return if valid_branches_amount?(ifs)
issue_for node, MSG
end
private def valid_branches_amount?(ifs)
# 1st item is always the `if` branch
ifs.size - 1 <= max_branches
end
end
end
================================================
FILE: src/ameba/rule/style/guard_clause.cr
================================================
module Ameba::Rule::Style
# Use a guard clause instead of wrapping the code inside a conditional
# expression
#
# ```
# # bad
# def test
# if something
# work
# end
# end
#
# # good
# def test
# return unless something
#
# work
# end
#
# # also good
# def test
# work if something
# end
#
# # bad
# if something
# raise "exception"
# else
# ok
# end
#
# # good
# raise "exception" if something
# ok
#
# # bad
# if something
# foo || raise("exception")
# else
# ok
# end
#
# # good
# foo || raise("exception") if something
# ok
# ```
#
# YAML configuration example:
#
# ```
# Style/GuardClause:
# Enabled: true
# ```
class GuardClause < Base
include AST::Util
properties do
since_version "1.0.0"
enabled false
description "Check for conditionals that can be replaced with guard clauses"
end
MSG = "Use a guard clause (`%s`) instead of wrapping the " \
"code inside a conditional expression"
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Assign,
]
end
def test(source, node : Crystal::Def)
final_expression =
if (body = node.body).is_a?(Crystal::Expressions)
body.last
else
body
end
case final_expression
when Crystal::If, Crystal::Unless
check_ending_if(source, final_expression)
end
end
def test(source, node : Crystal::If | Crystal::Unless)
return if accepted_form?(source, node, ending: false)
case
when guard_clause = guard_clause(node.then)
parent, conditional_keyword = node.then, keyword(node)
when guard_clause = guard_clause(node.else)
parent, conditional_keyword = node.else, opposite_keyword(node)
end
return unless guard_clause && parent && conditional_keyword
guard_clause_source = guard_clause_source(source, guard_clause, parent)
report_issue(source, node, guard_clause_source, conditional_keyword)
end
private def check_ending_if(source, node)
return if accepted_form?(source, node, ending: true)
report_issue(source, node, "return", opposite_keyword(node))
end
private def report_issue(source, node, scope_exiting_keyword, conditional_keyword)
return unless keyword_loc = node.location
return unless cond_code = node_source(node.cond, source.lines)
keyword_end_loc = keyword_loc.adjust(column_number: keyword(node).size - 1)
example = "#{scope_exiting_keyword} #{conditional_keyword} #{cond_code}"
# TODO: check if example is too long for single line
if node.else.is_a?(Crystal::Nop)
return unless end_end_loc = node.end_location
end_loc = end_end_loc.adjust(column_number: {{ 1 - "end".size }})
issue_for keyword_loc, keyword_end_loc, MSG % example do |corrector|
replacement = "#{scope_exiting_keyword} #{conditional_keyword}"
corrector.replace(keyword_loc, keyword_end_loc, replacement)
corrector.remove(end_loc, end_end_loc)
end
else
issue_for keyword_loc, keyword_end_loc, MSG % example
end
end
private def keyword(node : Crystal::If)
"if"
end
private def keyword(node : Crystal::Unless)
"unless"
end
private def opposite_keyword(node : Crystal::If)
"unless"
end
private def opposite_keyword(node : Crystal::Unless)
"if"
end
private def accepted_form?(source, node, ending)
return true if node.is_a?(Crystal::If) && node.ternary?
return true unless cond_loc = node.cond.location
return true unless cond_loc.same_line?(node.cond.end_location)
return true unless (then_loc = node.then.location).nil? || cond_loc < then_loc
if ending
!node.else.is_a?(Crystal::Nop)
else
return true if node.else.is_a?(Crystal::Nop)
return true unless code = node_source(node, source.lines)
code.starts_with?("elsif")
end
end
private def guard_clause(node)
node = node.right if node.is_a?(Crystal::BinaryOp)
return unless location = node.location
return unless location.same_line?(node.end_location)
case node
when Crystal::Call
node if node.obj.nil? && node.name == "raise"
when Crystal::Return, Crystal::Break, Crystal::Next
node
end
end
private def guard_clause_source(source, guard_clause, parent)
node = parent.is_a?(Crystal::BinaryOp) ? parent : guard_clause
node_source(node, source.lines)
end
end
end
================================================
FILE: src/ameba/rule/style/hash_literal_syntax.cr
================================================
module Ameba::Rule::Style
# Encourages the use of `Hash(K, V).new` syntax for creating a hash over `{} of K => V`.
#
# Favour this:
#
# ```
# Hash(Int32, String?).new
# ```
#
# Over this:
#
# ```
# {} of Int32 => String?
# ```
#
# YAML configuration example:
#
# ```
# Style/HashLiteralSyntax:
# Enabled: true
# ```
class HashLiteralSyntax < Base
properties do
since_version "1.7.0"
enabled false
description "Encourages the use of `Hash(K, V).new` over `{} of K => V`"
end
MSG = "Use `Hash(%s, %s).new` for creating an empty hash"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::HashLiteral)
return unless node.entries.empty? && (hash_type = node.of)
issue_for node, MSG % {hash_type.key, hash_type.value} do |corrector|
corrector.replace(node, "Hash(#{hash_type.key}, #{hash_type.value}).new")
end
end
end
end
================================================
FILE: src/ameba/rule/style/heredoc_escape.cr
================================================
module Ameba::Rule::Style
# A rule that enforces heredoc variant that escapes interpolation or control
# chars in a heredoc body. The opposite is enforced too - i.e. regular heredoc
# variant that doesn't escape interpolation or control chars in a heredoc body,
# when there is no need to escape it.
#
# For example, this is considered invalid:
#
# ```
# <<-DOC
# This is an escaped \#{:interpolated} string \\n
# DOC
# ```
#
# And should be written as:
#
# ```
# <<-'DOC'
# This is an escaped #{:interpolated} string \n
# DOC
# ```
#
# YAML configuration example:
#
# ```
# Style/HeredocEscape:
# Enabled: true
# ```
class HeredocEscape < Base
include AST::Util
properties do
since_version "1.7.0"
description "Recommends using the heredoc variant that escapes interpolation or control chars in a heredoc body"
end
MSG_ESCAPE_NEEDED = "Use an escaped heredoc marker: `<<-'%s'`"
MSG_ESCAPE_NOT_NEEDED = "Use an unescaped heredoc marker: `<<-%s`"
ESCAPE_SEQUENCE_PATTERN =
/\\(?:[abefnrtv]|[CdDhHRsSvVwWX]|[0-7]{1,3}|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\{[0-9a-fA-F]{1,6}\})/
def test(source, node : Crystal::StringInterpolation)
# Heredocs without interpolations have always size of 1
return unless node.expressions.size == 1
return unless expr = node.expressions.first.as?(Crystal::StringLiteral)
return unless code = node_source(node, source.lines)
return unless code.starts_with?("<<-")
body = code.lines[1..-2].join('\n')
if code.starts_with?("<<-'")
return if has_escape_sequence?(expr.value) || has_escaped_escape_sequence?(body)
marker = code.lchop("<<-'").match!(/^(\w+)/)[1]
msg = MSG_ESCAPE_NOT_NEEDED % marker
else
return if !has_escape_sequence?(expr.value) || has_escape_sequence?(body)
marker = code.lchop("<<-").match!(/^(\w+)/)[1]
msg = MSG_ESCAPE_NEEDED % marker
end
issue_for node, msg
end
private def has_escape_sequence?(value : String)
value.matches? /(?
# ...
#
# HEREDOC
# ```
#
# Will be automatically dedented to:
#
# ```
# <<-HEREDOC
#
# ...
#
# HEREDOC
# ```
#
# YAML configuration example:
#
# ```
# Style/HeredocIndent:
# Enabled: true
# IndentBy: 2
# BodyAutoDedent: true
# ```
class HeredocIndent < Base
include AST::Util
properties do
since_version "1.7.0"
description "Recommends heredoc bodies are indented consistently"
indent_by 2
body_auto_dedent true
end
MSG = "Heredoc body should be indented by %d spaces"
def test(source, node : Crystal::StringInterpolation)
return unless location = node.location
return unless node_source = node_source(node, source.lines)
return unless node_source.starts_with?("<<-")
correct_indent = line_indent(source, location) + indent_by
heredoc_indent = node.heredoc_indent
return if heredoc_indent == correct_indent
issue_for node, MSG % indent_by do |corrector|
source_lines = node_source.lines
if body_auto_dedent?
body_dedent =
source_lines[1...-1]
.reject!(&.empty?)
.min_of?(&.each_char.take_while(&.whitespace?).size)
end
body_dedent ||= heredoc_indent
corrected_code = source_lines
.map_with_index! do |line, idx|
# ignore 1st line containing the marker
next line if idx.zero? || line.empty?
dedent =
idx == source_lines.size - 1 ? heredoc_indent : body_dedent
"#{" " * correct_indent}#{line[dedent..]}"
end
.join('\n')
corrector.replace(node, corrected_code)
end
end
private def line_indent(source, location) : Int32
line = source.lines[location.line_number - 1]
line.size - line.lstrip.size
end
end
end
================================================
FILE: src/ameba/rule/style/is_a_filter.cr
================================================
module Ameba::Rule::Style
# This rule is used to identify usage of `is_a?/nil?` calls within filters.
#
# For example, this is considered invalid:
#
# ```
# matches = %w[Alice Bob].map(&.match(/^A./))
#
# matches.any?(&.is_a?(Regex::MatchData)) # => true
# matches.one?(&.nil?) # => true
#
# typeof(matches.reject(&.nil?)) # => Array(Regex::MatchData | Nil)
# typeof(matches.select(&.is_a?(Regex::MatchData))) # => Array(Regex::MatchData | Nil)
# ```
#
# And it should be written as this:
#
# ```
# matches = %w[Alice Bob].map(&.match(/^A./))
#
# matches.any?(Regex::MatchData) # => true
# matches.one?(Nil) # => true
#
# typeof(matches.reject(Nil)) # => Array(Regex::MatchData)
# typeof(matches.select(Regex::MatchData)) # => Array(Regex::MatchData)
# ```
#
# YAML configuration example:
#
# ```
# Style/IsAFilter:
# Enabled: true
# FilterNames:
# - select
# - reject
# - any?
# - all?
# - none?
# - one?
# ```
class IsAFilter < Base
include AST::Util
properties do
since_version "0.14.0"
description "Identifies usage of `is_a?/nil?` calls within filters"
filter_names %w[select reject any? all? none? one?]
end
MSG = "Use `%s` instead of `%s`"
OLD = "%s {...}"
NEW = "%s(%s)"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name.in?(filter_names)
return unless location = name_location(node)
return unless (block = node.block) && block.args.size == 1
return unless (body = block.body).is_a?(Crystal::IsA)
return unless (path = body.const).is_a?(Crystal::Path)
return unless body.obj.is_a?(Crystal::Var)
name = path.names.join("::")
name = "::#{name}" if path.global? && !body.nil_check?
old = OLD % node.name
new = NEW % {node.name, name}
msg = MSG % {new, old}
if end_location = node.end_location
issue_for location, end_location, msg do |corrector|
corrector.replace(location, end_location, new)
end
else
issue_for location, nil, msg
end
end
end
end
================================================
FILE: src/ameba/rule/style/is_a_nil.cr
================================================
module Ameba::Rule::Style
# A rule that disallows calls to `is_a?(Nil)` in favor of `nil?`.
#
# This is considered bad:
#
# ```
# var.is_a?(Nil)
# ```
#
# And needs to be written as:
#
# ```
# var.nil?
# ```
#
# YAML configuration example:
#
# ```
# Style/IsANil:
# Enabled: true
# ```
class IsANil < Base
include AST::Util
properties do
since_version "0.13.0"
description "Disallows calls to `is_a?(Nil)` in favor of `nil?`"
end
MSG = "Use `nil?` instead of `is_a?(Nil)`"
def test(source, node : Crystal::IsA)
return if node.nil_check?
const = node.const
return unless path_named?(const, "Nil")
issue_for const, MSG do |corrector|
corrector.replace(node, "#{node.obj}.nil?")
end
end
end
end
================================================
FILE: src/ameba/rule/style/large_numbers.cr
================================================
module Ameba::Rule::Style
# A rule that disallows usage of large numbers without underscore.
# These do not affect the value of the number, but can help read
# large numbers more easily.
#
# For example, these are considered invalid:
#
# ```
# 100000
# 141592654
# 5.123456
# ```
#
# And has to be rewritten as the following:
#
# ```
# 100_000
# 141_592_654
# 5.123_456
# ```
#
# YAML configuration example:
#
# ```
# Style/LargeNumbers:
# Enabled: true
# IntMinDigits: 6 # i.e. integers higher than 99999
# ```
class LargeNumbers < Base
include AST::Util
properties do
since_version "0.2.0"
enabled false
description "Disallows usage of large numbers without underscore"
int_min_digits 6
end
MSG = "Large numbers should be written with underscores: `%s`"
def test(source)
Tokenizer.new(source).run do |token|
next unless token.type.number? && decimal?(token.raw)
parsed = parse_number(token.raw)
if allowed?(*parsed) && (expected = underscored *parsed) != token.raw
location = name_location_or(token, token.raw)
issue_for *location, MSG % expected do |corrector|
corrector.replace(*location, expected)
end
end
end
end
private def decimal?(value)
value !~ /^0(x|b|o)/
end
private def allowed?(_sign, value, fraction, _suffix)
return true if fraction && fraction.size > 3
digits = value.chars.select!(&.number?)
digits.size >= int_min_digits
end
private def underscored(sign, value, fraction, suffix)
value = slice_digits(value.reverse).reverse
fraction = ".#{slice_digits(fraction)}" if fraction
"#{sign}#{value}#{fraction}#{suffix}"
end
private def slice_digits(value, by = 3)
%w[].tap do |slices|
value.chars.reject!(&.== '_').each_slice(by) do |slice|
slices << slice.join
end
end.join('_')
end
private def parse_number(value)
value, sign = parse_sign(value)
value, suffix = parse_suffix(value)
value, fraction = parse_fraction(value)
{sign, value, fraction, suffix}
end
private def parse_sign(value)
if value[0].in?('+', '-')
sign = value[0]
value = value[1..-1]
end
{value, sign}
end
private def parse_suffix(value)
if pos = (value =~ /(e|_?(i|u|f))/)
suffix = value[pos..-1]
value = value[0..pos - 1]
end
{value, suffix}
end
private def parse_fraction(value)
if comma = value.index('.')
fraction = value[comma + 1..-1]
value = value[0..comma - 1]
end
{value, fraction}
end
end
end
================================================
FILE: src/ameba/rule/style/multiline_curly_block.cr
================================================
module Ameba::Rule::Style
# A rule that disallows multi-line blocks that use curly brackets
# instead of `do`...`end`.
#
# For example, this is considered invalid:
#
# ```
# (0..10).map { |i|
# i * 2
# }
# ```
#
# And should be rewritten to the following:
#
# ```
# (0..10).map do |i|
# i * 2
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/MultilineCurlyBlock:
# Enabled: true
# ```
class MultilineCurlyBlock < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows multi-line blocks using curly block syntax"
end
MSG = "Use `do`...`end` instead of curly brackets for multi-line blocks"
def test(source, node : Crystal::Block)
return unless location = node.location
return if location.same_line?(node.end_location)
return unless source.code[source.pos(location)]? == '{'
issue_for node, MSG
end
end
end
================================================
FILE: src/ameba/rule/style/multiline_string_literal.cr
================================================
module Ameba::Rule::Style
# A rule that disallows multiline string literals not using
# `<<-HEREDOC` markers.
#
# For example, this is considered invalid:
#
# ```
# %(
# foo
# bar
# )
# ```
#
# And should be rewritten to the following:
#
# ```
# <<-HEREDOC
# foo
# bar
# HEREDOC
# ```
#
# YAML configuration example:
#
# ```
# Style/MultilineStringLiteral:
# Enabled: true
# AllowBackslashSplitStrings: true
# ```
class MultilineStringLiteral < Base
properties do
since_version "1.7.0"
description "Disallows multiline string literals not using `<<-HEREDOC` markers"
allow_backslash_split_strings true
end
MSG = "Use `<<-HEREDOC` markers for multiline strings"
def test(source, node : Crystal::StringLiteral | Crystal::StringInterpolation)
return unless location = node.location
return if location.same_line?(node.end_location)
location_pos = source.pos(location)
# ignore regex and command literals
return if source.code[location_pos]?.in?('/', '`')
return if source.code[location_pos..(location_pos + 1)]?.in?("%r", "%x")
# ignore heredoc string literals
return if source.code[location_pos..(location_pos + 2)]? == "<<-"
# ignore string literals split by \
return if allow_backslash_split_strings? &&
source.code.lines[location.line_number - 1].ends_with?('\\')
issue_for node, MSG
end
end
end
================================================
FILE: src/ameba/rule/style/negated_conditions_in_unless.cr
================================================
module Ameba::Rule::Style
# A rule that disallows negated conditions in `unless`.
#
# For example, this is considered invalid:
#
# ```
# unless !s.empty?
# :ok
# end
# ```
#
# And should be rewritten to the following:
#
# ```
# if s.empty?
# :ok
# end
# ```
#
# It is pretty difficult to wrap your head around a block of code
# that is executed if a negated condition is NOT met.
#
# YAML configuration example:
#
# ```
# Style/NegatedConditionsInUnless:
# Enabled: true
# ```
class NegatedConditionsInUnless < Base
properties do
since_version "0.2.0"
description "Disallows negated conditions in `unless`"
end
MSG = "Avoid negated conditions in unless blocks"
def test(source, node : Crystal::Unless)
issue_for node, MSG if negated_condition?(node.cond)
end
private def negated_condition?(node)
case node
when Crystal::BinaryOp
negated_condition?(node.left) || negated_condition?(node.right)
when Crystal::Expressions
node.expressions.any? { |exp| negated_condition?(exp) }
when Crystal::Not
true
else
false
end
end
end
end
================================================
FILE: src/ameba/rule/style/parentheses_around_condition.cr
================================================
module Ameba::Rule::Style
# A rule that checks for the presence of superfluous parentheses
# around the condition of `if`, `unless`, `case`, `while` and `until`.
#
# For example, this is considered invalid:
#
# ```
# if (foo == 42)
# do_something
# end
# ```
#
# And should be replaced by the following:
#
# ```
# if foo == 42
# do_something
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/ParenthesesAroundCondition:
# Enabled: true
# ExcludeTernary: false
# ExcludeMultiline: false
# AllowSafeAssignment: false
# ```
class ParenthesesAroundCondition < Base
include AST::Util
properties do
since_version "1.4.0"
description "Disallows redundant parentheses around control expressions"
exclude_ternary false
exclude_multiline false
allow_safe_assignment false
end
MSG_REDUNDANT = "Redundant parentheses"
MSG_MISSING = "Missing parentheses"
def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until)
return unless cond = node.cond
if cond.is_a?(Crystal::Assign) && allow_safe_assignment?
issue_for cond, MSG_MISSING do |corrector|
corrector.wrap(cond, '(', ')')
end
return
end
return unless redundant_parentheses?(node, cond)
issue_for cond, MSG_REDUNDANT do |corrector|
corrector.remove_trailing(cond, 1)
corrector.remove_leading(cond, 1)
end
end
private def redundant_parentheses?(node, cond) : Bool
is_ternary = node.is_a?(Crystal::If) && node.ternary?
return false if is_ternary && exclude_ternary?
return false unless cond.is_a?(Crystal::Expressions)
return false unless cond.keyword.paren?
return false unless exp = cond.single_expression?
return false unless strip_parentheses?(exp, is_ternary)
if exclude_multiline?
if (location = node.location) && (end_location = node.end_location)
return false unless location.same_line?(end_location)
end
end
true
end
private def strip_parentheses?(node, in_ternary) : Bool
case node
when Crystal::BinaryOp
!in_ternary
when Crystal::Call
!in_ternary || node.has_parentheses? || !has_arguments?(node)
when Crystal::ExceptionHandler, Crystal::If, Crystal::Unless
false
when Crystal::Yield
!in_ternary || node.has_parentheses? || node.exps.empty?
when Crystal::Assign, Crystal::OpAssign, Crystal::MultiAssign
!in_ternary && !allow_safe_assignment?
else
true
end
end
end
end
================================================
FILE: src/ameba/rule/style/percent_literal_delimiters.cr
================================================
module Ameba::Rule::Style
# A rule that enforces the consistent usage of `%`-literal delimiters.
#
# Specifying `DefaultDelimiters` option will set all preferred delimiters at once. You
# can continue to specify individual preferred delimiters via `PreferredDelimiters`
# setting to override the default. In both cases the delimiters should be specified
# as a string of two characters, or `nil` to ignore a particular `%`-literal / default.
#
# Setting `IgnoreLiteralsContainingDelimiters` to `true` will ignore `%`-literals that
# contain one or both delimiters.
#
# YAML configuration example:
#
# ```
# Style/PercentLiteralDelimiters:
# Enabled: true
# DefaultDelimiters: '()'
# PreferredDelimiters:
# '%w': '[]'
# '%i': '[]'
# '%r': '{}'
# IgnoreLiteralsContainingDelimiters: false
# ```
class PercentLiteralDelimiters < Base
properties do
since_version "1.7.0"
description "Enforces the consistent usage of `%`-literal delimiters"
default_delimiters "()", as: String?
preferred_delimiters({
"%w" => "[]",
"%i" => "[]",
"%r" => "{}",
} of String => String?)
ignore_literals_containing_delimiters false
end
MSG = "`%s`-literals should be delimited by `%s` and `%s`"
def test(source)
processor = TokenProcessor.new(source, self)
processor.run do |state|
msg = MSG % {state.literal, state.opening_delimiter, state.closing_delimiter}
x = state.start_token.location.adjust(column_number: state.literal.size)
y = state.end_token.location
issue_for state.location, state.end_location, msg do |corrector|
corrector.replace(x, x, state.opening_delimiter)
corrector.replace(y, y, state.closing_delimiter)
end
end
end
private class TokenProcessor
def initialize(source, @rule : PercentLiteralDelimiters)
@tokenizer = Tokenizer.new(source)
end
def run(&on_literal : LiteralState -> Nil) : Nil
current_state = nil
@tokenizer.run do |token|
case token.type
when .string_array_start?, .symbol_array_start?, .delimiter_start?
if literal = extract_percent_literal(token.raw)
if delimiters = delimiters_for_literal(literal)
current_state = LiteralState.new(token.dup, literal, delimiters)
end
end
when .string?
if (state = current_state) && @rule.ignore_literals_containing_delimiters?
current_state = nil if state.includes_delimiters?(token.raw)
end
when .string_array_end?, .delimiter_end?
if state = current_state
unless state.correct_delimiters?
state.end_token = token
on_literal.call(state)
end
current_state = nil
end
end
end
end
private def extract_percent_literal(string)
string.match(/^(%\w*)\W/).try &.[1]
end
private def delimiters_for_literal(literal)
@rule.preferred_delimiters.fetch(literal) { @rule.default_delimiters }
end
struct LiteralState
getter start_token : Crystal::Token
property! end_token : Crystal::Token
getter literal : String
getter opening_delimiter : Char
getter closing_delimiter : Char
def initialize(@start_token, @literal, delimiters)
@opening_delimiter = delimiters[0]
@closing_delimiter = delimiters[1]
end
def includes_delimiters?(string)
string.includes?(opening_delimiter) ||
string.includes?(closing_delimiter)
end
def correct_delimiters?
start_token.delimiter_state.nest == opening_delimiter &&
start_token.delimiter_state.end == closing_delimiter
end
def location
start_token.location
end
def end_location
location.adjust(column_number: literal.size - 1)
end
end
end
end
end
================================================
FILE: src/ameba/rule/style/redundant_begin.cr
================================================
module Ameba::Rule::Style
# A rule that disallows redundant `begin` blocks.
#
# Currently it is able to detect:
#
# 1. Exception handler block that can be used as a part of the method.
#
# For example, this:
#
# ```
# def method
# begin
# read_content
# rescue
# close_file
# end
# end
# ```
#
# should be rewritten as:
#
# ```
# def method
# read_content
# rescue
# close_file
# end
# ```
#
# 2. `begin`..`end` block as a top level block in a method.
#
# For example this is considered invalid:
#
# ```
# def method
# begin
# a = 1
# b = 2
# end
# end
# ```
#
# and has to be written as the following:
#
# ```
# def method
# a = 1
# b = 2
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/RedundantBegin:
# Enabled: true
# ```
class RedundantBegin < Base
include AST::Util
properties do
since_version "0.3.0"
description "Disallows redundant `begin` blocks"
end
MSG = "Redundant `begin` block detected"
def test(source, node : Crystal::Def)
return unless def_loc = node.location
case body = node.body
when Crystal::ExceptionHandler
return if begin_exprs_in_handler?(body) || inner_handler?(body)
when Crystal::Expressions
return unless redundant_begin_in_expressions?(body)
else
return
end
return unless begin_range = def_redundant_begin_range(source, node)
begin_loc, end_loc = begin_range
begin_loc, end_loc = def_loc.seek(begin_loc), def_loc.seek(end_loc)
begin_end_loc = begin_loc.adjust(column_number: {{ "begin".size - 1 }})
end_end_loc = end_loc.adjust(column_number: {{ "end".size - 1 }})
issue_for begin_loc, begin_end_loc, MSG do |corrector|
corrector.remove(begin_loc, begin_end_loc)
corrector.remove(end_loc, end_end_loc)
end
end
private def redundant_begin_in_expressions?(node)
!!node.keyword.try(&.begin?)
end
private def inner_handler?(handler)
handler.body.is_a?(Crystal::ExceptionHandler)
end
private def begin_exprs_in_handler?(handler)
return unless (body = handler.body).is_a?(Crystal::Expressions)
body.expressions.first?.is_a?(Crystal::ExceptionHandler)
end
private def def_redundant_begin_range(source, node)
return unless code = node_source(node, source.lines)
lexer = Crystal::Lexer.new code
return unless begin_loc = def_redundant_begin_loc(lexer)
return unless end_loc = def_redundant_end_loc(lexer)
{begin_loc, end_loc}
end
private def def_redundant_begin_loc(lexer)
in_body = in_argument_list = false
loop do
token = lexer.next_token
case token.type
when .eof?, .op_minus_gt?
break
when .ident?
next unless in_body
return unless token.value == Crystal::Keyword::BEGIN
return token.location
when .op_lparen?
in_argument_list = true
when .op_rparen?
in_argument_list = false
when .newline?
in_body = true unless in_argument_list
when .space?
# ignore
else
return if in_body
end
end
end
private def def_redundant_end_loc(lexer)
end_loc = def_end_loc = nil
Tokenizer.new(lexer).run do |token|
next unless token.value == Crystal::Keyword::END
end_loc, def_end_loc = def_end_loc, token.location
end
end_loc
end
end
end
================================================
FILE: src/ameba/rule/style/redundant_next.cr
================================================
module Ameba::Rule::Style
# A rule that disallows redundant `next` expressions. A `next` keyword allows
# a block to skip to the next iteration early, however, it is considered
# redundant in cases where it is the last expression in a block or combines
# into the node which is the last in a block.
#
# For example, this is considered invalid:
#
# ```
# block do |v|
# next v + 1
# end
# ```
#
# ```
# block do |v|
# case v
# when .nil?
# next "nil"
# when .blank?
# next "blank"
# else
# next "empty"
# end
# end
# ```
#
# And has to be written as the following:
#
# ```
# block do |v|
# v + 1
# end
# ```
#
# ```
# block do |v|
# case arg
# when .nil?
# "nil"
# when .blank?
# "blank"
# else
# "empty"
# end
# end
# ```
#
# ### Configuration params
#
# 1. *allow_multi_next*, default: `true`
#
# Allows end-user to configure whether to report or not the `next` statements
# which yield tuple literals i.e.
#
# ```
# block do
# next a, b
# end
# ```
#
# If this param equals to `false`, the block above will be forced to be written as:
#
# ```
# block do
# {a, b}
# end
# ```
#
# 2. *allow_empty_next*, default: `true`
#
# Allows end-user to configure whether to report or not the `next` statements
# without arguments. Sometimes such statements are used to yield the `nil` value explicitly.
#
# ```
# block do
# @foo = :empty
# next
# end
# ```
#
# If this param equals to `false`, the block above will be forced to be written as:
#
# ```
# block do
# @foo = :empty
# nil
# end
# ```
#
# ### YAML config example
#
# ```
# Style/RedundantNext:
# Enabled: true
# AllowMultiNext: true
# AllowEmptyNext: true
# ```
class RedundantNext < Base
include AST::Util
properties do
since_version "0.12.0"
description "Reports redundant `next` expressions"
allow_multi_next true
allow_empty_next true
end
MSG = "Redundant `next` detected"
def test(source, node : Crystal::Block)
AST::RedundantControlExpressionVisitor.new(self, source, node.body)
end
def test(source, node : Crystal::Next, visitor : AST::RedundantControlExpressionVisitor)
return if allow_multi_next? && node.exp.is_a?(Crystal::TupleLiteral)
return if allow_empty_next? && (node.exp.nil? || node.exp.try(&.nop?))
if exp_code = control_exp_code(node, source.lines)
issue_for node, MSG do |corrector|
corrector.replace(node, exp_code)
end
else
issue_for node, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/style/redundant_nil_in_control_expression.cr
================================================
module Ameba::Rule::Style
# A rule that disallows control expressions (`return`, `break` and `next`)
# with `nil` argument.
#
# This is considered invalid:
#
# ```
# def greeting(name)
# return nil unless name
#
# "Hello, #{name}"
# end
# ```
#
# And this is valid:
#
# ```
# def greeting(name)
# return unless name
#
# "Hello, #{name}"
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/RedundantNilInControlExpression:
# Enabled: true
# ```
class RedundantNilInControlExpression < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows control expressions with `nil` argument"
end
MSG = "Redundant `nil` detected"
def test(source, node : Crystal::ControlExpression)
return unless (exp = node.exp).is_a?(Crystal::NilLiteral)
node_code =
node_source(node, source.lines) || node.to_s
# `return(nil)`
if node_code.includes?('(')
issue_for exp, MSG
else
issue_for exp, MSG do |corrector|
corrector.replace(node, node_code.sub(/\s*\(?nil\)?$/, ""))
end
end
end
end
end
================================================
FILE: src/ameba/rule/style/redundant_return.cr
================================================
module Ameba::Rule::Style
# A rule that disallows redundant `return` expressions.
#
# For example, this is considered invalid:
#
# ```
# def foo
# return :bar
# end
# ```
#
# ```
# def bar(arg)
# case arg
# when .nil?
# return "nil"
# when .blank?
# return "blank"
# else
# return "empty"
# end
# end
# ```
#
# And has to be written as the following:
#
# ```
# def foo
# :bar
# end
# ```
#
# ```
# def bar(arg)
# case arg
# when .nil?
# "nil"
# when .blank?
# "blank"
# else
# "empty"
# end
# end
# ```
#
# ### Configuration params
#
# 1. *allow_multi_return*, default: `true`
#
# Allows end-user to configure whether to report or not the `return` statements
# which return tuple literals i.e.
#
# ```
# def method(a, b)
# return a, b
# end
# ```
#
# If this param equals to `false`, the method above has to be written as:
#
# ```
# def method(a, b)
# {a, b}
# end
# ```
#
# 2. *allow_empty_return*, default: `true`
#
# Allows end-user to configure whether to report or not the `return` statements
# without arguments. Sometimes such returns are used to return the `nil` value explicitly.
#
# ```
# def method
# @foo = :empty
# return
# end
# ```
#
# If this param equals to `false`, the method above has to be written as:
#
# ```
# def method
# @foo = :empty
# nil
# end
# ```
#
# ### YAML config example
#
# ```
# Style/RedundantReturn:
# Enabled: true
# AllowMultiReturn: true
# AllowEmptyReturn: true
# ```
class RedundantReturn < Base
include AST::Util
properties do
since_version "0.9.0"
description "Reports redundant `return` expressions"
allow_multi_return true
allow_empty_return true
end
MSG = "Redundant `return` detected"
def test(source, node : Crystal::Def)
AST::RedundantControlExpressionVisitor.new(self, source, node.body)
end
def test(source, node : Crystal::Return, visitor : AST::RedundantControlExpressionVisitor)
return if allow_multi_return? && node.exp.is_a?(Crystal::TupleLiteral)
return if allow_empty_return? && (node.exp.nil? || node.exp.try(&.nop?))
if exp_code = control_exp_code(node, source.lines)
issue_for node, MSG do |corrector|
corrector.replace(node, exp_code)
end
else
issue_for node, MSG
end
end
end
end
================================================
FILE: src/ameba/rule/style/redundant_self.cr
================================================
require "compiler/crystal/syntax/token"
module Ameba::Rule::Style
# A rule that disallows redundant uses of `self`.
#
# This is considered bad:
#
# ```
# class Greeter
# getter name : String
#
# def self.init
# self.new("Crystal").greet
# end
#
# def initialize(@name)
# end
#
# def greet
# puts "Hello, my name is #{self.name}"
# end
#
# self.init
# end
# ```
#
# And needs to be written as:
#
# ```
# class Greeter
# getter name : String
#
# def self.init
# new("Crystal").greet
# end
#
# def initialize(@name)
# end
#
# def greet
# puts "Hello, my name is #{name}"
# end
#
# init
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/RedundantSelf:
# Enabled: true
# AllowedMethodNames:
# - in?
# - inspect
# - not_nil!
# ```
class RedundantSelf < Base
include AST::Util
properties do
since_version "1.7.0"
description "Disallows redundant uses of `self`"
allowed_method_names %w[in? inspect not_nil!]
end
MSG = "Redundant `self` detected"
CRYSTAL_KEYWORDS = Crystal::Keyword.values.map(&.to_s)
def test(source)
AST::ScopeCallsWithSelfReceiverVisitor.new self, source
end
def test(source, node : Crystal::Call, scope : AST::Scope)
return if setter_method?(node) || operator_method?(node)
# Guard against auto-expanded `OpAssign` nodes, i.e.
# `self.a += b` is expanded to `self.a = self.a + b`.
return unless node.location && node.end_location
return unless (obj = node.obj).is_a?(Crystal::Var)
name = node.name
return if name.in?(CRYSTAL_KEYWORDS)
return if name.in?(allowed_method_names)
vars = Set(String).new
while scope
break if scope.type_definition?
scope.arguments.each do |arg|
vars << arg.name
end
scope.variables.each do |var|
var.assignments.each do |assign|
vars << assign.variable.name
end
end
scope = scope.outer_scope
end
return if name.in?(vars)
return unless node_source = node_source(node, source.lines)
issue_for obj, MSG do |corrector|
corrector.replace(node, node_source.sub(/\Aself\s*\./, ""))
end
end
end
end
================================================
FILE: src/ameba/rule/style/unless_else.cr
================================================
module Ameba::Rule::Style
# A rule that disallows the use of an `else` block with the `unless`.
#
# For example, the rule considers these valid:
#
# ```
# unless something
# :ok
# end
#
# if something
# :one
# else
# :two
# end
# ```
#
# But it considers this one invalid as it is an `unless` with an `else`:
#
# ```
# unless something
# :one
# else
# :two
# end
# ```
#
# The solution is to swap the order of the blocks, and change the `unless` to
# an `if`, so the previous invalid example would become this:
#
# ```
# if something
# :two
# else
# :one
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/UnlessElse:
# Enabled: true
# ```
class UnlessElse < Base
properties do
since_version "0.1.0"
description "Disallows the use of an `else` block with the `unless`"
end
MSG = "Favour `if` over `unless` with `else`"
def test(source, node : Crystal::Unless)
return if node.else.nop?
location = node.location
cond_end_location = node.cond.end_location
else_location = node.else_location
end_location = node.end_location
unless location && cond_end_location && else_location && end_location
issue_for node, MSG
return
end
issue_for location, cond_end_location, MSG do |corrector|
keyword_begin_pos = source.pos(location)
keyword_end_pos = keyword_begin_pos + {{ "unless".size }}
keyword_range = keyword_begin_pos...keyword_end_pos
cond_end_pos = source.pos(cond_end_location, end: true)
else_begin_pos = source.pos(else_location)
body_range = cond_end_pos...else_begin_pos
else_end_pos = else_begin_pos + {{ "else".size }}
end_end_pos = source.pos(end_location, end: true)
end_begin_pos = end_end_pos - {{ "end".size }}
else_range = else_end_pos...end_begin_pos
corrector.replace(keyword_range, "if")
corrector.replace(body_range, source.code[else_range])
corrector.replace(else_range, source.code[body_range])
end
end
end
end
================================================
FILE: src/ameba/rule/style/verbose_block.cr
================================================
module Ameba::Rule::Style
# This rule is used to identify usage of single expression blocks with
# argument as a receiver, that can be collapsed into a short form.
#
# For example, this is considered invalid:
#
# ```
# (1..3).any? { |i| i.odd? }
# ```
#
# And it should be written as this:
#
# ```
# (1..3).any?(&.odd?)
# ```
#
# YAML configuration example:
#
# ```
# Style/VerboseBlock:
# Enabled: true
# ExcludeMultipleLineBlocks: true
# ExcludeCallsWithBlock: true
# ExcludePrefixOperators: true
# ExcludeOperators: true
# ExcludeSetters: false
# MaxLineLength: ~
# MaxLength: 50 # use ~ to disable
# ```
class VerboseBlock < Base
include AST::Util
properties do
since_version "0.14.0"
description "Identifies usage of collapsible single expression blocks"
exclude_multiple_line_blocks true
exclude_calls_with_block true
exclude_prefix_operators true
exclude_operators true
exclude_setters false
max_line_length nil, as: Int32?
max_length 50, as: Int32?
end
MSG = "Use short block notation instead: `%s`"
CALL_PATTERN = "%s(%s&.%s)"
private PREFIX_OPERATORS = {"+", "-", "~"}
def test(source, node : Crystal::Call)
# we are interested only in calls with block taking a single argument
#
# ```
# (1..3).any? { |i| i.to_i64.odd? }
# ^--- ^ ^------------
# block arg body
# ```
return unless (block = node.block) && block.args.size == 1
arg = block.args.first
# we filter out the blocks that are of call type - `i.to_i64.odd?`
return unless (body = block.body).is_a?(Crystal::Call)
# we need to "unwind" the call chain, so the final receiver object
# ends up being a variable - `i`
obj = body.obj
while obj.is_a?(Crystal::Call)
obj = obj.obj
end
# only calls with a first argument used as a receiver are the valid game
return unless obj == arg
# we bail out if the block node include the block argument
return if reference_count(body, arg) > 1
# add issue if the given nodes pass all of the checks
issue_for_valid source, node, block, body
end
# ameba:disable Metrics/CyclomaticComplexity
private def issue_for_valid(source, call : Crystal::Call, block : Crystal::Block, body : Crystal::Call)
return if exclude_calls_with_block? && body.block
return if exclude_multiple_line_blocks? && !same_location_lines?(call, body)
return if exclude_prefix_operators? && prefix_operator?(body)
return if exclude_operators? && operator_method?(body)
return if exclude_setters? && setter_method?(body)
call_code =
call_code(source, call, body)
return unless valid_line_length?(call, call_code)
return unless valid_length?(call_code)
return unless location = name_location(call)
return unless end_location = block.end_location
if call_code.includes?("{...}")
issue_for location, end_location, MSG % call_code
else
issue_for location, end_location, MSG % call_code do |corrector|
corrector.replace(location, end_location, call_code)
end
end
end
private def call_code(source, call, body)
args = String.build { |io| args_to_s(io, call) }.presence
args += ", " if args
call_chain = %w[].tap do |arr|
obj = body.obj
while obj.is_a?(Crystal::Call)
arr << node_to_s(source, obj)
obj = obj.obj
end
arr.reverse!
arr << node_to_s(source, body)
end
name =
call_chain.join('.')
CALL_PATTERN % {call.name, args, name}
end
private def node_to_s(source, node : Crystal::Call)
String.build do |str|
case name = node.name
when "[]"
str << '['
args_to_s(str, node)
str << ']'
when "[]?"
str << '['
args_to_s(str, node)
str << "]?"
when "[]="
str << '['
args_to_s(str, node, skip_last_arg: true)
str << "]=(" << node.args.last? << ')'
else
short_block = short_block_code(source, node)
str << name
if has_arguments?(node) || short_block
str << '('
args_to_s(str, node, short_block)
str << ')'
end
str << " {...}" if node.block && short_block.nil?
end
end
end
private def args_to_s(io : IO, node : Crystal::Call, short_block = nil, skip_last_arg = false) : Nil
args = node.args.dup
args.pop? if skip_last_arg
args.join io, ", "
named_args = node.named_args
if named_args
io << ", " unless args.empty? || named_args.empty?
named_args.join io, ", " do |arg, inner_io|
inner_io << arg.name << ": " << arg.value
end
end
if short_block
io << ", " unless args.empty? && (named_args.nil? || named_args.empty?)
io << short_block
end
end
private def short_block_code(source, node : Crystal::Call)
return unless block = node.block
return unless block_location = block.location
return unless block_end_location = block.body.end_location
block_code = source_between(block_location, block_end_location, source.lines)
block_code if block_code.try(&.starts_with?("&."))
end
private def same_location_lines?(a, b)
name_location(a).try &.same_line?(b.location)
end
private def prefix_operator?(node)
node.name.in?(PREFIX_OPERATORS) && !has_arguments?(node)
end
private def valid_length?(code)
if max_length = self.max_length
return code.size <= max_length
end
true
end
private def valid_line_length?(node, code)
if max_line_length = self.max_line_length
if location = name_location(node)
final_line_length = location.column_number + code.size
return final_line_length <= max_line_length
end
end
true
end
private def reference_count(node, obj : Crystal::Var)
i = 0
case node
when Crystal::Call
i += reference_count(node.obj, obj)
i += reference_count(node.block, obj)
node.args.each do |arg|
i += reference_count(arg, obj)
end
node.named_args.try &.each do |arg|
i += reference_count(arg.value, obj)
end
when Crystal::BinaryOp
i += reference_count(node.left, obj)
i += reference_count(node.right, obj)
when Crystal::Block
i += reference_count(node.body, obj)
when Crystal::Var
i += 1 if node == obj
end
i
end
end
end
================================================
FILE: src/ameba/rule/style/verbose_nil_type.cr
================================================
module Ameba::Rule::Style
# A rule that enforces consistent naming of `Nil` in type unions.
#
# For example, this is considered invalid:
#
# ```
# foo : String | Nil = nil
# ```
#
# And should be replaced by the following:
#
# ```
# foo : String? = nil
# ```
#
# Enable the `ExplicitNil` option to enforce the opposite behavior.
#
# YAML configuration example:
#
# ```
# Style/VerboseNilType:
# Enabled: true
# ExplicitNil: false
# ```
class VerboseNilType < Base
include AST::Util
properties do
since_version "1.7.0"
description "Enforces consistent naming of `Nil` in type unions"
explicit_nil false
end
MSG_VERBOSE = "Prefer `?` instead of `| Nil` in unions"
MSG_SHORT = "Prefer `| Nil` instead of `?` in unions"
private NIL_TYPE_PATTERN = /(\s*\|\s*Nil(?=\W|$))|((?<=\W|^)Nil\s*\|\s*)/
private SINGLE_TYPE_PATTERN = /\((\w+)\)/
def test(source, node : Crystal::Union)
return unless has_nil?(node)
return unless node_source = node_source(node, source.lines)
# https://github.com/crystal-lang/crystal/issues/11071
return if node_source.includes?(".class")
# `String?` -> `String | Nil`
if explicit_nil?
return unless node_source.ends_with?('?')
issue_for node, MSG_SHORT do |corrector|
corrector.replace(node, "%s | Nil" % node_source.rstrip('?'))
end
return
end
# `String | Nil` -> `String?`
return unless node_source.matches?(NIL_TYPE_PATTERN)
# Unions that _do not_ contain generic types are safe to modify using
# simple find-and-replace, due to the fact that their types are being
# flattened in the end, so removing `Nil` type from anywhere in the
# union and appending `?` should be semantically the same and should
# not affect the type of the union.
#
# If union contains generic type however, we need to be careful, as
# simple find-and-replace might change the type of the union -
# and that's why we skip the auto-correction of those.
if has_generic?(node)
issue_for node, MSG_VERBOSE
return
end
issue_for node, MSG_VERBOSE do |corrector|
corrected_code = "%s?" % node_source
.gsub(NIL_TYPE_PATTERN, "")
.gsub('?', "")
# handle `(String | ((Symbol)))` cases
while corrected_code.matches?(SINGLE_TYPE_PATTERN)
corrected_code = corrected_code
.gsub(SINGLE_TYPE_PATTERN, "\\1")
end
corrector.replace(node, corrected_code)
end
end
private def has_generic?(node : Crystal::Union)
node.types.any? { |type| has_generic?(type) }
end
private def has_generic?(node)
node.is_a?(Crystal::Generic)
end
private def has_nil?(node : Crystal::Union)
node.types.any? { |type| has_nil?(type) }
end
private def has_nil?(node)
path_named?(node, "Nil")
end
end
end
================================================
FILE: src/ameba/rule/style/while_true.cr
================================================
module Ameba::Rule::Style
# A rule that disallows the use of `while true` instead of using the idiomatic `loop`
#
# For example, this is considered invalid:
#
# ```
# while true
# do_something
# break if some_condition
# end
# ```
#
# And should be replaced by the following:
#
# ```
# loop do
# do_something
# break if some_condition
# end
# ```
#
# YAML configuration example:
#
# ```
# Style/WhileTrue:
# Enabled: true
# ```
class WhileTrue < Base
properties do
since_version "0.3.0"
description "Disallows `while` statements with a `true` literal as condition"
end
MSG = "While statement using `true` literal as condition"
def test(source, node : Crystal::While)
return unless node.cond.true_literal?
return unless location = node.location
return unless end_location = node.cond.end_location
issue_for node, MSG do |corrector|
corrector.replace(location, end_location, "loop do")
end
end
end
end
================================================
FILE: src/ameba/rule/typing/macro_call_argument_type_restriction.cr
================================================
module Ameba::Rule::Typing
# A rule that enforces call arguments to specific macros have a type restriction.
# By default these macros are: `(class_)getter/setter/property(?/!)` and `record`.
#
# For example, these are considered invalid:
#
# ```
# class Greeter
# getter name
# getter age = 0.days
# getter :height
# end
#
# record Task,
# cmd = "",
# args = %w[]
# ```
#
# And these are considered valid:
#
# ```
# class Greeter
# getter name : String?
# getter age : Time::Span = 0.days
# getter height : Float64?
# end
#
# record Task,
# cmd : String = "",
# args : Array(String) = %w[]
# ```
#
# The `DefaultValue` configuration option controls whether this rule applies to
# call arguments that have a default value.
#
# YAML configuration example:
#
# ```
# Typing/MacroCallArgumentTypeRestriction:
# Enabled: true
# DefaultValue: false
# MacroNames:
# - getter
# - getter?
# - getter!
# - class_getter
# - class_getter?
# - class_getter!
# - setter
# - setter?
# - setter!
# - class_setter
# - class_setter?
# - class_setter!
# - property
# - property?
# - property!
# - class_property
# - class_property?
# - class_property!
# - record
# ```
class MacroCallArgumentTypeRestriction < Base
properties do
since_version "1.7.0"
description "Recommends that call arguments to certain macros have type restrictions"
enabled false
default_value false
macro_names %w[
getter getter? getter! class_getter class_getter? class_getter!
setter setter? setter! class_setter class_setter? class_setter!
property property? property! class_property class_property? class_property!
record
]
end
MSG = "Argument should have a type restriction"
def test(source, node : Crystal::Call)
return unless node.name.in?(macro_names)
node.args.each do |arg|
case arg
when Crystal::Assign
issue_for arg.target, MSG if default_value?
when Crystal::Var, Crystal::Call, Crystal::StringLiteral, Crystal::SymbolLiteral
issue_for arg, MSG
end
end
end
end
end
================================================
FILE: src/ameba/rule/typing/method_parameter_type_restriction.cr
================================================
module Ameba::Rule::Typing
# A rule that enforces method parameters have type restrictions, with optional enforcement of block parameters.
#
# For example, this is considered invalid:
#
# ```
# def add(a, b)
# a + b
# end
# ```
#
# And this is considered valid:
#
# ```
# def add(a : String, b : String)
# a + b
# end
# ```
#
# When the config options `PrivateMethods` and `ProtectedMethods`
# are true, this rule is also applied to private and protected methods, respectively.
#
# The `NodocMethods` configuration option controls whether this rule applies to
# methods with a `:nodoc:` directive.
#
# The `BlockParameters` configuration option will extend this to block parameters, where these are invalid:
#
# ```
# def exec(&)
# end
#
# def exec(&block)
# end
# ```
#
# And this is valid:
#
# ```
# def exec(&block : String -> String)
# yield "cmd"
# end
# ```
#
# The config option `DefaultValue` controls whether this rule applies to parameters that have a default value.
#
# YAML configuration example:
#
# ```
# Typing/MethodParameterTypeRestriction:
# Enabled: true
# DefaultValue: false
# BlockParameters: false
# PrivateMethods: false
# ProtectedMethods: false
# NodocMethods: false
# ```
class MethodParameterTypeRestriction < Base
include AST::Util
properties do
since_version "1.7.0"
description "Recommends that method parameters have type restrictions"
enabled false
default_value false
block_parameters false
private_methods false
protected_methods false
nodoc_methods false
end
MSG = "Method parameter should have a type restriction"
def test(source, node : Crystal::Def)
return if valid_visibility?(node)
node.args.each do |arg|
next if arg.restriction || arg.name.empty?
next if !default_value? && arg.default_value
issue_for arg, MSG, prefer_name_location: true
end
if block_parameters? && (block_arg = node.block_arg) && !block_arg.restriction
issue_for block_arg, MSG, prefer_name_location: true
end
end
private def valid_visibility?(node : Crystal::ASTNode) : Bool
(!private_methods? && node.visibility.private?) ||
(!protected_methods? && node.visibility.protected?) ||
(!nodoc_methods? && nodoc?(node))
end
end
end
================================================
FILE: src/ameba/rule/typing/method_return_type_restriction.cr
================================================
module Ameba::Rule::Typing
# A rule that enforces method definitions have a return type restriction.
#
# For example, this are considered invalid:
#
# ```
# def hello(name = "World")
# "Hello #{name}"
# end
# ```
#
# And this is valid:
#
# ```
# def hello(name = "World") : String
# "Hello #{name}"
# end
# ```
#
# When the config options `PrivateMethods` and `ProtectedMethods`
# are true, this rule is also applied to private and protected methods, respectively.
#
# The `NodocMethods` configuration option controls whether this rule applies to
# methods with a `:nodoc:` directive.
#
# YAML configuration example:
#
# ```
# Typing/MethodReturnTypeRestriction:
# Enabled: true
# PrivateMethods: false
# ProtectedMethods: false
# NodocMethods: false
# ```
class MethodReturnTypeRestriction < Base
include AST::Util
properties do
since_version "1.7.0"
description "Recommends that methods have a return type restriction"
enabled false
private_methods false
protected_methods false
nodoc_methods false
end
MSG = "Method should have a return type restriction"
def test(source, node : Crystal::Def)
issue_for node, MSG unless valid_return_type?(node)
end
private def valid_return_type?(node : Crystal::ASTNode) : Bool
!!node.return_type ||
(node.visibility.private? && !private_methods?) ||
(node.visibility.protected? && !protected_methods?) ||
(!nodoc_methods? && nodoc?(node))
end
end
end
================================================
FILE: src/ameba/rule/typing/proc_literal_return_type_restriction.cr
================================================
module Ameba::Rule::Typing
# A rule that enforces that `Proc` literals have a return type.
#
# For example, these are considered invalid:
#
# ```
# greeter = ->(name : String) { "Hello #{name}" }
# ```
#
# ```
# task = -> { Task.new("execute this command") }
# ```
#
# And these are valid:
#
# ```
# greeter = ->(name : String) : String { "Hello #{name}" }
# ```
#
# ```
# task = -> : Task { Task.new("execute this command") }
# ```
#
# YAML configuration example:
#
# ```
# Typing/ProcLiteralReturnTypeRestriction:
# Enabled: true
# ```
class ProcLiteralReturnTypeRestriction < Base
properties do
since_version "1.7.0"
description "Disallows proc literals without return type restriction"
enabled false
end
MSG = "Proc literal should have a return type restriction"
def test(source, node : Crystal::ProcLiteral)
issue_for node, MSG unless node.def.return_type
end
end
end
================================================
FILE: src/ameba/runner.cr
================================================
module Ameba
# Represents a runner for inspecting sources files.
# Holds a list of rules to do inspection based on,
# list of sources to run inspection on and a formatter
# to prepare a report.
#
# ```
# config = Ameba::Config.load
# runner = Ameba::Runner.new config
# runner.run.success? # => true or false
# ```
class Runner
# An error indicating that the inspection loop got stuck correcting
# issues back and forth.
class InfiniteCorrectionLoopError < RuntimeError
def initialize(path, issues_by_iteration, loop_start = -1)
root_cause =
issues_by_iteration[loop_start..-1]
.join(" -> ", &.map(&.rule.name).uniq!.join(", "))
message = String.build do |io|
io << "Infinite loop"
io << " in " << path unless path.empty?
io << " caused by " << root_cause
end
super message
end
end
# A list of rules to do inspection based on.
@rules : Array(Rule::Base)
# Project root path.
getter root : Path
# A list of sources to run inspection on.
getter sources : Array(Source)
# A level of severity to be reported.
@severity : Severity
# A formatter to prepare report.
@formatter : Formatter::BaseFormatter
# A syntax rule which always inspects a source first
@syntax_rule = Rule::Lint::Syntax.new
# Checks for unneeded disable directives. Always inspects a source last
@unneeded_disable_directive_rule : Rule::Lint::UnneededDisableDirective?
# Returns `true` if correctable issues should be autocorrected.
private getter? autocorrect : Bool
# Returns an ameba version up to which the rules should be ran.
property version : SemanticVersion?
# Instantiates a runner using a `config`.
#
# ```
# config = Ameba::Config.load
# config.files = files
# config.formatter = formatter
#
# Ameba::Runner.new config
# ```
def initialize(config : Config)
initialize(
config.rules,
config.sources,
config.formatter,
config.severity,
config.autocorrect?,
config.version,
config.root,
)
end
protected def initialize(rules, sources, @formatter, @severity, @autocorrect = false, @version = nil, @root = Path[Dir.current])
@sources = sources.sort_by(&.path)
@rules =
rules.select(&->rule_runnable?(Rule::Base))
@unneeded_disable_directive_rule =
rules.find(&.class.==(Rule::Lint::UnneededDisableDirective))
.as?(Rule::Lint::UnneededDisableDirective)
end
protected def rule_runnable?(rule)
rule.enabled? && !rule.special? &&
rule_satisfies_severity?(rule, @severity) &&
rule_satisfies_version?(rule, @version)
end
protected def rule_satisfies_severity?(rule, severity)
rule.severity <= severity
end
protected def rule_satisfies_version?(rule, version)
!version || !(since_version = rule.since_version) ||
since_version <= version
end
# Performs the inspection. Iterates through all sources and test it using
# list of rules. If a specific rule fails on a specific source, it adds
# an issue to that source.
#
# This action also notifies formatter when inspection is started/finished,
# and when a specific source started/finished to be inspected.
#
# ```
# runner = Ameba::Runner.new config
# runner.run # => returns runner again
# ```
def run
@formatter.started @sources
channels = @sources.map { Channel(Exception?).new }
@sources.zip(channels).each do |source, channel|
spawn do
run_source(source)
rescue ex
channel.send(ex)
else
channel.send(nil)
end
end
channels.each do |chan|
chan.receive.try { |ex| raise ex }
end
self
ensure
@formatter.finished @sources
end
private def run_source(source) : Nil
@formatter.source_started source
# This variable is a 2D array used to track corrected issues after each
# inspection iteration. This is used to output meaningful infinite loop
# error message.
corrected_issues = [] of Array(Issue)
# When running with --fix, we need to inspect the source until no more
# corrections are made (because automatic corrections can introduce new
# issues). In the normal case the loop is only executed once.
loop_unless_infinite(source, corrected_issues) do
# We have to reprocess the source to pick up any changes. Since a
# change could (theoretically) introduce syntax errors, we break the
# loop if we find any.
@syntax_rule.test(source)
break unless source.valid?
excluded_rules = Set(String).new
@rules.each do |rule|
if rule.excluded?(source, root)
excluded_rules << rule.name
next
end
rule.test(source)
end
check_unneeded_directives(source, excluded_rules)
break unless autocorrect? && source.correct!
# The issues that couldn't be corrected will be found again so we
# only keep the corrected ones in order to avoid duplicate reporting.
corrected_issues << source.issues.select(&.correctable?)
source.issues.clear
end
corrected_issues.flatten.reverse_each do |issue|
source.issues.unshift(issue)
end
File.write(source.path, source.code) unless corrected_issues.empty?
ensure
source.issues.sort_by! do |issue|
{
issue.location.try(&.line_number) || 0,
issue.location.try(&.column_number) || 0,
}
end
@formatter.source_finished source
end
# Explains an issue at a specified *location*.
#
# Runner should perform inspection before doing the explain.
# This is necessary to be able to find the issue at a specified location.
#
# ```
# runner = Ameba::Runner.new config
# runner.run
# runner.explain(Crystal::Location.new(file, line, column))
# ```
def explain(location, output = STDOUT)
Formatter::ExplainFormatter.new(output, location).finished @sources
end
# Indicates whether the last inspection successful or not.
# It returns `true` if no issues are found, `false` otherwise.
#
# ```
# runner = Ameba::Runner.new config
# runner.run
# runner.success? # => true or false
# ```
def success?
@sources.all? &.issues.none? &.enabled?
end
private MAX_ITERATIONS = 200
private def loop_unless_infinite(source, corrected_issues, &)
# Keep track of the state of the source. If a rule modifies the source
# and another rule undoes it producing identical source we have an
# infinite loop.
processed_sources = [] of UInt64
# It is possible for a rule to keep adding indefinitely to a file,
# making it bigger and bigger. If the inspection loop runs for an
# excessively high number of iterations, this is likely happening.
iterations = 0
loop do
check_for_infinite_loop(source, corrected_issues, processed_sources)
if (iterations += 1) > MAX_ITERATIONS
raise InfiniteCorrectionLoopError.new(source.path, corrected_issues)
end
yield
end
end
# Check whether a run created source identical to a previous run, which
# means that we definitely have an infinite loop.
private def check_for_infinite_loop(source, corrected_issues, processed_sources)
checksum = source.code.hash
if loop_start = processed_sources.index(checksum)
raise InfiniteCorrectionLoopError.new(
source.path,
corrected_issues,
loop_start: loop_start
)
end
processed_sources << checksum
end
private def check_unneeded_directives(source, excluded_rules = Set(String).new)
return unless rule = @unneeded_disable_directive_rule
return unless rule.enabled?
return if rule.excluded?(source, root)
rule.test(source, excluded_rules)
end
end
end
================================================
FILE: src/ameba/severity.cr
================================================
require "colorize"
module Ameba
enum Severity
Error
Warning
Convention
# Returns a symbol uniquely indicating severity.
#
# ```
# Severity::Warning.symbol # => 'W'
# ```
def symbol : Char
case self
in Error then 'E'
in Warning then 'W'
in Convention then 'C'
end
end
# Returns a color uniquely indicating severity.
#
# ```
# Severity::Warning.color # => Colorize::ColorANSI::Red
# ```
def color : Colorize::Color
case self
in Error then Colorize::ColorANSI::Red
in Warning then Colorize::ColorANSI::Red
in Convention then Colorize::ColorANSI::Blue
end
end
# Creates Severity by the name.
#
# ```
# Severity.parse("convention") # => Severity::Convention
# Severity.parse("foo-bar") # => Exception: Incorrect severity name
# ```
def self.parse(name : String)
super name
rescue ArgumentError
raise "Incorrect severity name #{name}. Try one of: #{values.join(", ")}"
end
end
# Converter for `YAML.mapping` which converts severity enum to and from YAML.
class SeverityYamlConverter
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node)
unless node.is_a?(YAML::Nodes::Scalar)
raise "Severity must be a scalar, not #{node.class}"
end
case value = node.value
when String then Severity.parse(value)
when Nil then raise "Missing severity"
else
raise "Incorrect severity: #{value}"
end
end
def self.to_yaml(value : Severity, yaml : YAML::Nodes::Builder)
yaml.scalar value
end
end
end
================================================
FILE: src/ameba/source/corrector.cr
================================================
require "./rewriter"
class Ameba::Source
# This class takes source code and rewrites it based
# on the different correction actions supplied.
class Corrector
@line_sizes = [] of Int32
def initialize(code : String)
code.each_line(chomp: false) do |line|
@line_sizes << line.size
end
@rewriter = Rewriter.new(code)
end
# Replaces the code of the given range with *content*.
def replace(location, end_location, content)
@rewriter.replace(loc_to_pos(location), loc_to_pos(end_location) + 1, content)
end
# :ditto:
def replace(range : Range(Int32, Int32), content)
begin_pos, end_pos = range.begin, range.end
end_pos -= 1 unless range.excludes_end?
@rewriter.replace(begin_pos, end_pos, content)
end
# Inserts the given strings before and after the given range.
def wrap(location, end_location, insert_before, insert_after)
@rewriter.wrap(loc_to_pos(location), loc_to_pos(end_location) + 1, insert_before, insert_after)
end
# :ditto:
def wrap(range : Range(Int32, Int32), insert_before, insert_after)
begin_pos, end_pos = range.begin, range.end
end_pos -= 1 unless range.excludes_end?
@rewriter.wrap(begin_pos, end_pos, insert_before, insert_after)
end
# Shortcut for `replace(location, end_location, "")`
def remove(location, end_location)
@rewriter.remove(loc_to_pos(location), loc_to_pos(end_location) + 1)
end
# Shortcut for `replace(range, "")`
def remove(range : Range(Int32, Int32))
begin_pos, end_pos = range.begin, range.end
end_pos -= 1 unless range.excludes_end?
@rewriter.remove(begin_pos, end_pos)
end
# Shortcut for `wrap(location, end_location, content, nil)`
def insert_before(location, end_location, content)
@rewriter.insert_before(loc_to_pos(location), loc_to_pos(end_location) + 1, content)
end
# Shortcut for `wrap(range, content, nil)`
def insert_before(range : Range(Int32, Int32), content)
begin_pos, end_pos = range.begin, range.end
end_pos -= 1 unless range.excludes_end?
@rewriter.insert_before(begin_pos, end_pos, content)
end
# Shortcut for `wrap(location, end_location, nil, content)`
def insert_after(location, end_location, content)
@rewriter.insert_after(loc_to_pos(location), loc_to_pos(end_location) + 1, content)
end
# Shortcut for `wrap(range, nil, content)`
def insert_after(range : Range(Int32, Int32), content)
begin_pos, end_pos = range.begin, range.end
end_pos -= 1 unless range.excludes_end?
@rewriter.insert_after(begin_pos, end_pos, content)
end
# Shortcut for `insert_before(location, location, content)`
def insert_before(location, content)
@rewriter.insert_before(loc_to_pos(location), content)
end
# Shortcut for `insert_before(pos.., content)`
def insert_before(pos : Int32, content)
@rewriter.insert_before(pos, content)
end
# Shortcut for `insert_after(location, location, content)`
def insert_after(location, content)
@rewriter.insert_after(loc_to_pos(location) + 1, content)
end
# Shortcut for `insert_after(...pos, content)`
def insert_after(pos : Int32, content)
@rewriter.insert_after(pos, content)
end
# Removes *size* characters prior to the source range.
def remove_preceding(location, end_location, size)
@rewriter.remove(loc_to_pos(location) - size, loc_to_pos(location))
end
# :ditto:
def remove_preceding(range : Range(Int32, Int32), size)
begin_pos = range.begin
@rewriter.remove(begin_pos - size, begin_pos)
end
# Removes *size* characters from the beginning of the given range.
# If *size* is greater than the size of the range, the removed region can
# overrun the end of the range.
def remove_leading(location, end_location, size)
@rewriter.remove(loc_to_pos(location), loc_to_pos(location) + size)
end
# :ditto:
def remove_leading(range : Range(Int32, Int32), size)
begin_pos = range.begin
@rewriter.remove(begin_pos, begin_pos + size)
end
# Removes *size* characters from the end of the given range.
# If *size* is greater than the size of the range, the removed region can
# overrun the beginning of the range.
def remove_trailing(location, end_location, size)
@rewriter.remove(loc_to_pos(end_location) + 1 - size, loc_to_pos(end_location) + 1)
end
# :ditto:
def remove_trailing(range : Range(Int32, Int32), size)
end_pos = range.end
end_pos -= 1 unless range.excludes_end?
@rewriter.remove(end_pos - size, end_pos)
end
private def loc_to_pos(location : Crystal::Location | {Int32, Int32})
if location.is_a?(Crystal::Location)
line, column = location.line_number, location.column_number
else
line, column = location
end
@line_sizes[0...line - 1].sum + (column - 1)
end
# Replaces the code of the given node with *content*.
def replace(node : Crystal::ASTNode, content)
replace(location(node), end_location(node), content)
end
# Inserts the given strings before and after the given node.
def wrap(node : Crystal::ASTNode, insert_before, insert_after)
wrap(location(node), end_location(node), insert_before, insert_after)
end
# Shortcut for `replace(node, "")`
def remove(node : Crystal::ASTNode)
remove(location(node), end_location(node))
end
# Shortcut for `wrap(node, content, nil)`
def insert_before(node : Crystal::ASTNode, content)
insert_before(location(node), content)
end
# Shortcut for `wrap(node, nil, content)`
def insert_after(node : Crystal::ASTNode, content)
insert_after(end_location(node), content)
end
# Removes *size* characters prior to the given node.
def remove_preceding(node : Crystal::ASTNode, size)
remove_preceding(location(node), end_location(node), size)
end
# Removes *size* characters from the beginning of the given node.
# If *size* is greater than the size of the node, the removed region can
# overrun the end of the node.
def remove_leading(node : Crystal::ASTNode, size)
remove_leading(location(node), end_location(node), size)
end
# Removes *size* characters from the end of the given node.
# If *size* is greater than the size of the node, the removed region can
# overrun the beginning of the node.
def remove_trailing(node : Crystal::ASTNode, size)
remove_trailing(location(node), end_location(node), size)
end
private def location(node : Crystal::ASTNode)
node.location || raise "Missing location"
end
private def end_location(node : Crystal::ASTNode)
node.end_location || raise "Missing end location"
end
# Applies all scheduled changes and returns modified source as a new string.
def process
@rewriter.process
end
end
end
================================================
FILE: src/ameba/source/rewriter/action.cr
================================================
class Ameba::Source::Rewriter
# :nodoc:
#
# Actions are arranged in a tree and get combined so that:
# - children are strictly contained by their parent
# - siblings all disjoint from one another and ordered
# - only actions with `replacement == nil` may have children
class Action
getter begin_pos : Int32
getter end_pos : Int32
getter replacement : String?
getter insert_before : String
getter insert_after : String
protected getter children : Array(Action)
def initialize(@begin_pos,
@end_pos,
@insert_before = "",
@replacement = nil,
@insert_after = "",
@children = [] of Action)
end
def combine(action)
return self if action.empty? # Ignore empty action
if action.begin_pos == @begin_pos && action.end_pos == @end_pos
merge(action)
else
place_in_hierarchy(action)
end
end
def empty?
replacement = @replacement
@insert_before.empty? &&
@insert_after.empty? &&
@children.empty? &&
(replacement.nil? ||
(replacement.empty? && @begin_pos == @end_pos))
end
def ordered_replacements
replacement = @replacement
reps = [] of {Int32, Int32, String}
reps << {@begin_pos, @begin_pos, @insert_before} unless @insert_before.empty?
reps << {@begin_pos, @end_pos, replacement} if replacement
reps.concat(@children.flat_map(&.ordered_replacements))
reps << {@end_pos, @end_pos, @insert_after} unless @insert_after.empty?
reps
end
def insertion?
replacement = @replacement
!@insert_before.empty? ||
!@insert_after.empty? ||
(replacement && !replacement.empty?)
end
protected def with(*,
begin_pos = @begin_pos,
end_pos = @end_pos,
insert_before = @insert_before,
replacement = @replacement,
insert_after = @insert_after,
children = @children)
children = [] of Action if replacement
self.class.new(begin_pos, end_pos, insert_before, replacement, insert_after, children)
end
protected def place_in_hierarchy(action)
family = analyze_hierarchy(action)
sibling_left, sibling_right = family[:sibling_left], family[:sibling_right]
if fusible = family[:fusible]
child = family[:child]
child ||= [] of Action
fuse_deletions(action, fusible, sibling_left + child + sibling_right)
else
extra_sibling =
case
when parent = family[:parent]
# action should be a descendant of one of the children
parent.combine(action)
when child = family[:child]
# or it should become the parent of some of the children,
action.with(children: child).combine_children(action.children)
else
# or else it should become an additional child
action
end
self.with(children: sibling_left + [extra_sibling] + sibling_right)
end
end
# Assumes *more_children* all contained within `@begin_pos...@end_pos`
protected def combine_children(more_children)
more_children.reduce(self) do |parent, new_child|
parent.place_in_hierarchy(new_child)
end
end
protected def fuse_deletions(action, fusible, other_siblings)
without_fusible = self.with(children: other_siblings)
fusible = [action] + fusible
fused_begin_pos = fusible.min_of(&.begin_pos)
fused_end_pos = fusible.max_of(&.end_pos)
fused_deletion = action.with(begin_pos: fused_begin_pos, end_pos: fused_end_pos)
without_fusible.combine(fused_deletion)
end
# Similar to `@children.bsearch_index || size` except allows for a starting point
protected def bsearch_child_index(from = 0, &)
size = @children.size
(from...size).bsearch { |i| yield @children[i] } || size
end
# Returns the children in a hierarchy with respect to *action*:
#
# - `:sibling_left`, `:sibling_right` (for those that are disjoint from *action*)
# - `:parent` (in case one of our children contains *action*)
# - `:child` (in case *action* strictly contains some of our children)
# - `:fusible` (in case *action* overlaps some children but they can be fused in one deletion)
#
# In case a child has equal range to *action*, it is returned as `:parent`
#
# NOTE: an empty range `1...1` is considered disjoint from `1...10`
protected def analyze_hierarchy(action)
# left_index is the index of the first child that isn't completely to the left of action
left_index = bsearch_child_index do |child|
child.end_pos > action.begin_pos
end
# right_index is the index of the first child that is completely on the right of action
start = left_index == 0 ? 0 : left_index - 1 # See "corner case" below for reason of -1
right_index = bsearch_child_index(start) do |child|
child.begin_pos >= action.end_pos
end
center = right_index - left_index
case center
when 0
# All children are disjoint from action, nothing else to do
when -1
# Corner case: if a child has empty range == action's range
# then it will appear to be both disjoint and to the left of action,
# as well as disjoint and to the right of action.
# Since ranges are equal, we return it as parent
left_index -= 1 # Fix indices, as otherwise this child would be
right_index += 1 # considered as a sibling (both left and right!)
parent = @children[left_index]
else
overlap_left = @children[left_index].begin_pos <=> action.begin_pos
overlap_right = @children[right_index - 1].end_pos <=> action.end_pos
raise "Unable to compare begin pos" unless overlap_left
raise "Unable to compare end pos" unless overlap_right
# For one child to be the parent of action, we must have:
if center == 1 && overlap_left <= 0 && overlap_right >= 0
parent = @children[left_index]
else
# Otherwise consider all non disjoint elements (center) to be contained...
contained = @children[left_index...right_index]
fusible = [] of Action
fusible << contained.shift if overlap_left < 0 # ... but check first and last one
fusible << contained.pop if overlap_right > 0 # ... for overlaps
fusible = nil if fusible.empty?
end
end
{
parent: parent,
sibling_left: @children[0...left_index],
sibling_right: @children[right_index...@children.size],
fusible: fusible,
child: contained,
}
end
# Assumes *action* has the exact same range and has no children
protected def merge(action)
self.with(
insert_before: "#{action.insert_before}#{insert_before}",
replacement: action.replacement || @replacement,
insert_after: "#{insert_after}#{action.insert_after}",
).combine_children(action.children)
end
end
end
================================================
FILE: src/ameba/source/rewriter.cr
================================================
class Ameba::Source
# This class performs the heavy lifting in the source rewriting process.
# It schedules code updates to be performed in the correct order.
#
# For simple cases, the resulting source will be obvious.
#
# Examples for more complex cases follow. Assume these examples are acting on
# the source `puts(:hello, :world)`. The methods `#wrap`, `#remove`, etc.
# receive a range as the first two arguments; for clarity, examples below use
# English sentences and a string of raw code instead.
#
# ## Overlapping deletions:
#
# * remove `:hello, `
# * remove `, :world`
#
# The overlapping ranges are merged and `:hello, :world` will be removed.
#
# ## Multiple actions at the same end points:
#
# Results will always be independent of the order they were given.
# Exception: rewriting actions done on exactly the same range (covered next).
#
# Example:
#
# * replace `, ` by ` => `
# * wrap `:hello, :world` with `{` and `}`
# * replace `:world` with `:everybody`
# * wrap `:world` with `[`, `]`
#
# The resulting string will be `puts({:hello => [:everybody]})`
# and this result is independent of the order the instructions were given in.
#
# ## Multiple wraps on same range:
#
# * wrap `:hello` with `(` and `)`
# * wrap `:hello` with `[` and `]`
#
# The wraps are combined in order given and results would be `puts([(:hello)], :world)`.
#
# ## Multiple replacements on same range:
#
# * replace `:hello` by `:hi`, then
# * replace `:hello` by `:hey`
#
# The replacements are made in the order given, so the latter replacement
# supersedes the former and `:hello` will be replaced by `:hey`.
#
# ## Swallowed insertions:
#
# * wrap `world` by `__`, `__`
# * replace `:hello, :world` with `:hi`
#
# A containing replacement will swallow the contained rewriting actions
# and `:hello, :world` will be replaced by `:hi`.
#
# ## Implementation
#
# The updates are organized in a tree, according to the ranges they act on
# (where children are strictly contained by their parent).
class Rewriter
getter code : String
def initialize(@code)
@action_root = Rewriter::Action.new(0, code.size)
end
# Returns `true` if no (non trivial) update has been recorded
def empty?
@action_root.empty?
end
# Replaces the code of the given range with *content*.
def replace(begin_pos, end_pos, content)
combine begin_pos, end_pos,
replacement: content.to_s
end
# Inserts the given strings before and after the given range.
def wrap(begin_pos, end_pos, insert_before, insert_after)
combine begin_pos, end_pos,
insert_before: insert_before.to_s,
insert_after: insert_after.to_s
end
# Shortcut for `replace(begin_pos, end_pos, "")`
def remove(begin_pos, end_pos)
replace(begin_pos, end_pos, "")
end
# Shortcut for `wrap(begin_pos, end_pos, content, nil)`
def insert_before(begin_pos, end_pos, content)
wrap(begin_pos, end_pos, content, nil)
end
# Shortcut for `wrap(begin_pos, end_pos, nil, content)`
def insert_after(begin_pos, end_pos, content)
wrap(begin_pos, end_pos, nil, content)
end
# Shortcut for `insert_before(pos, pos, content)`
def insert_before(pos, content)
insert_before(pos, pos, content)
end
# Shortcut for `insert_after(pos, pos, content)`
def insert_after(pos, content)
insert_after(pos, pos, content)
end
# Applies all scheduled changes and returns modified source as a new string.
def process
String.build do |io|
last_end = 0
@action_root.ordered_replacements.each do |begin_pos, end_pos, replacement|
io << code[last_end...begin_pos] << replacement
last_end = end_pos
end
io << code[last_end...code.size]
end
end
protected def combine(begin_pos, end_pos, **attributes)
check_range_validity(begin_pos, end_pos)
action = Rewriter::Action.new(begin_pos, end_pos, **attributes)
@action_root = @action_root.combine(action)
end
private def check_range_validity(begin_pos, end_pos)
return unless begin_pos < 0 || end_pos > code.size
raise IndexError.new(
"The range #{begin_pos}...#{end_pos} is outside the bounds of " \
"the source (0...#{code.size})"
)
end
end
end
================================================
FILE: src/ameba/source.cr
================================================
module Ameba
# An entity that represents a Crystal source file.
# Has path, lines of code and issues reported by rules.
class Source
include InlineComments
include Reportable
# Path to the source file.
getter path : String
# Absolute path to the source file.
getter fullpath : String do
File.expand_path(path)
end
# Crystal code (content of a source file).
getter code : String
# Creates a new source by `code` and `path`.
#
# For example:
#
# ```
# path = "./src/source.cr"
# Ameba::Source.new(File.read(path), path)
# ```
def initialize(@code = "", @path = "")
end
# Corrects any correctable issues and updates `code`.
# Returns `false` if no issues were corrected.
def correct!
corrector = Corrector.new(code)
issues.each { |issue| issue.correct(corrector) if issue.enabled? }
corrected_code = corrector.process
return false if code == corrected_code
@code = corrected_code
@lines = nil
@ast = nil
true
end
# Returns lines of code split by new line character.
# Since `code` is immutable and can't be changed, this
# method caches lines in an instance variable, so calling
# it second time will not perform a split, but will return
# lines instantly.
#
# ```
# source = Ameba::Source.new("a = 1\nb = 2", path)
# source.lines # => ["a = 1", "b = 2"]
# ```
getter lines : Array(String) { code.split(/\r?\n/) }
# Returns AST nodes constructed by `Crystal::Parser`.
#
# ```
# source = Ameba::Source.new(code, path)
# source.ast
# ```
getter ast : Crystal::ASTNode do
code = @code
if ecr?
begin
code = ECR.process_string(code, path)
rescue ex : ECR::Lexer::SyntaxException
# Need to rescue to add the filename
raise Crystal::SyntaxException.new(
ex.message,
ex.line_number,
ex.column_number,
path
)
end
end
Crystal::Parser.new(code)
.tap(&.wants_doc = true)
.tap(&.filename = path)
.parse
end
# Returns `true` if the source is a spec file, `false` otherwise.
def spec?
path.ends_with?("_spec.cr")
end
# Returns `true` if the source is an ECR template, `false` otherwise.
def ecr?
path.ends_with?(".ecr")
end
# Converts an AST location to a string position.
def pos(location : Crystal::Location, end end_pos = false) : Int32
line, column = location.line_number, location.column_number
pos = lines[0...line - 1].sum(&.size) + line + column - 2
pos += 1 if end_pos
pos
end
end
end
================================================
FILE: src/ameba/spec/annotated_source.cr
================================================
# Parsed representation of code annotated with the `# ^^^ error: Message` style
class Ameba::Spec::AnnotatedSource
ANNOTATION_PATTERN_1 = /\A\s*(# )?(\^+|\^{})( error:)? /
ANNOTATION_PATTERN_2 = " # error: "
ABBREV = "[...]"
getter lines : Array(String)
# Each entry is the line number, annotation prefix, and message.
# The prefix is empty if the annotation is at the end of a code line.
getter annotations : Array({Int32, String, String})
# Separates annotation lines from code lines. Tracks the real
# code line number that each annotation corresponds to.
def self.parse(annotated_code)
lines = [] of String
annotations = [] of {Int32, String, String}
code_lines = annotated_code.split('\n') # must preserve trailing newline
code_lines.each do |code_line|
case
when annotation_match = ANNOTATION_PATTERN_1.match(code_line)
message_index = annotation_match.end
prefix = code_line[0...message_index]
message = code_line[message_index...]
annotations << {lines.size, prefix, message}
when annotation_index = code_line.index(ANNOTATION_PATTERN_2)
lines << code_line[...annotation_index]
message_index = annotation_index + ANNOTATION_PATTERN_2.size
message = code_line[message_index...]
annotations << {lines.size, "", message}
else
lines << code_line
end
end
annotations.map! { |_, prefix, message| {1, prefix, message} } if lines.empty?
new(lines, annotations)
end
# NOTE: Annotations are sorted so that reconstructing the annotation
# text via `#to_s` is deterministic.
def initialize(@lines, annotations : Enumerable({Int32, String, String}))
@annotations = annotations.to_a.sort_by do |line, _, message|
{line, message}
end
end
# Annotates the source code with the Ameba issues provided.
#
# NOTE: Annotations are sorted so that reconstructing the annotation
# text via `#to_s` is deterministic.
def initialize(@lines, issues : Enumerable(Issue))
@annotations = issues_to_annotations(issues).sort_by do |line, _, message|
{line, message}
end
end
def ==(other)
other.is_a?(AnnotatedSource) && other.lines == lines && match_annotations?(other)
end
private def match_annotations?(other)
return false unless annotations.size == other.annotations.size
annotations.zip(other.annotations) do |(actual_line, actual_prefix, actual_message), (expected_line, expected_prefix, expected_message)|
return false unless actual_line == expected_line
return false unless expected_prefix.empty? || actual_prefix == expected_prefix
next if actual_message == expected_message
return false unless expected_message.includes?(ABBREV)
regex = /\A#{message_to_regex(expected_message)}\Z/
return false unless actual_message.matches?(regex)
end
true
end
private def message_to_regex(expected_annotation)
String.build do |io|
offset = 0
while index = expected_annotation.index(ABBREV, offset)
io << Regex.escape(expected_annotation[offset...index])
io << ".*?"
offset = index + ABBREV.size
end
io << Regex.escape(expected_annotation[offset..])
end
end
# Constructs an annotated source string (like what we parse).
#
# Reconstructs a deterministic annotated source string. This is
# useful for eliminating semantically irrelevant annotation
# ordering differences.
#
# source1 = AnnotatedSource.parse(<<-CRYSTAL)
# line1
# ^ Annotation 1
# ^^ Annotation 2
# CRYSTAL
#
# source2 = AnnotatedSource.parse(<<-CRYSTAL)
# line1
# ^^ Annotation 2
# ^ Annotation 1
# CRYSTAL
#
# source1.to_s == source2.to_s # => true
def to_s(io)
reconstructed = lines.dup
annotations.reverse_each do |line_number, prefix, message|
if prefix.empty?
reconstructed[line_number - 1] += "#{ANNOTATION_PATTERN_2}#{message}"
else
line_number = 0 if lines.empty?
reconstructed.insert(line_number, "#{prefix}#{message}")
end
end
io << reconstructed.join('\n')
end
private def issues_to_annotations(issues)
issues.map do |issue|
line, column, end_line, end_column = validate_location(issue)
indent_count = column - 3
indent = if indent_count < 0
""
else
" " * indent_count
end
caret_count = column_length(line, column, end_line, end_column)
caret_count += indent_count if indent_count < 0
carets = if caret_count <= 0
"^{}"
else
"^" * caret_count
end
{line, "#{indent}# #{carets} error: ", issue.message}
end
end
private def validate_location(issue)
loc, end_loc = issue.location, issue.end_location
raise "Missing location for issue '#{issue.message}'" unless loc
line, column = loc.line_number, loc.column_number
if line > lines.size || line < 1 || column < 1
raise "Invalid issue location: #{loc}"
end
if end_loc
if end_loc < loc
raise <<-MSG
Invalid issue location
start: #{loc}
end: #{end_loc}
MSG
end
end_line, end_column = end_loc.line_number, end_loc.column_number
if end_line > lines.size || end_line < 1 || end_column < 1
raise "Invalid issue end location: #{end_loc}"
end
end
{line, column, end_line, end_column}
end
private def column_length(line, column, end_line, end_column)
return 1 unless end_line && end_column
if line < end_line
code_line = lines[line - 1]
end_column = code_line.size
end
end_column - column + 1
end
end
================================================
FILE: src/ameba/spec/be_valid.cr
================================================
module Ameba::Spec
module BeValid
def be_valid
BeValidExpectation.new
end
end
struct BeValidExpectation
def match(source)
source.valid?
end
def failure_message(source)
String.build do |str|
str << "Source expected to be valid, but there are issues: \n\n"
source.issues.reject(&.disabled?).each do |issue|
str << " * #{issue.rule.name}: #{issue.message}\n"
end
end
end
def negative_failure_message(source)
"Source expected to be invalid, but it is valid."
end
end
end
================================================
FILE: src/ameba/spec/expect_issue.cr
================================================
require "./annotated_source"
require "./util"
# Mixin for `expect_issue` and `expect_no_issues`
#
# This mixin makes it easier to specify strict issue expectations
# in a declarative and visual fashion. Just type out the code that
# should generate an issue, annotate code by writing '^'s
# underneath each character that should be highlighted, and follow
# the carets with a string (separated by a space) that is the
# message of the issue. You can include multiple issues in
# one code snippet.
#
# Usage:
#
# expect_issue subject, <<-CRYSTAL
# a do
# b
# end.c
# # ^^^ error: Avoid chaining a method call on a do...end block.
# CRYSTAL
#
# Equivalent assertion without `expect_issue`:
#
# source = Source.new <<-CRYSTAL, "source.cr"
# a do
# b
# end.c
# CRYSTAL
# subject.catch(source).should_not be_valid
# source.issues.size.should be(1)
#
# issue = source.issues.first
# issue.location.to_s.should eq "source.cr:3:1"
# issue.end_location.to_s.should eq "source.cr:3:5"
# issue.message.should eq(
# "Avoid chaining a method call on a do...end block."
# )
#
# Autocorrection can be tested using `expect_correction` after
# `expect_issue`.
#
# source = expect_issue subject, <<-CRYSTAL
# x % 2 == 0
# # ^^^^^^^^ error: Replace with `Int#even?`.
# CRYSTAL
#
# expect_correction source, <<-CRYSTAL
# x.even?
# CRYSTAL
#
# If you do not want to specify an issue then use the
# companion method `expect_no_issues`. This method is a much
# simpler assertion since it just inspects the code and checks
# that there were no issues. The `expect_issue` method has
# to do more work by parsing out lines that contain carets.
#
# If the code produces an issue that could not be auto-corrected, you can
# use `expect_no_corrections` after `expect_issue`.
#
# source = expect_issue subject, <<-CRYSTAL
# a do
# b
# end.c
# # ^^^ error: Avoid chaining a method call on a do...end block.
# CRYSTAL
#
# expect_no_corrections source
#
# If your code has variables of different lengths, you can use `%{foo}`,
# `^{foo}`, and `_{foo}` to format your template; you can also abbreviate
# issue messages with `[...]`:
#
# %w[raise fail].each do |keyword|
# expect_issue subject, <<-CRYSTAL, keyword: keyword
# %{keyword} Exception.new(msg)
# # ^{keyword}^^^^^^^^^^^^^^^^^ error: Redundant `Exception.new` [...]
# CRYSTAL
#
# %w[has_one has_many].each do |type|
# expect_issue subject, <<-CRYSTAL, type: type
# class Book
# %{type} :chapter, foreign_key: "book_id"
# _{type} # ^^^^^^^^^^^^^^^^^^^^^^ error: Specifying the default [...]
# end
# CRYSTAL
# end
#
# If you need to specify an issue on a blank line, use the empty `^{}` marker:
#
# expect_issue subject, <<-CRYSTAL
#
# # ^{} error: Missing frozen string literal comment.
# puts 1
# CRYSTAL
module Ameba::Spec::ExpectIssue
include Spec::Util
def expect_issue(rules : Rule::Base | Enumerable(Rule::Base),
annotated_code : String,
path = "",
*,
file = __FILE__,
line = __LINE__,
**replacements)
annotated_code = format_issue(annotated_code, **replacements)
expected_annotations = AnnotatedSource.parse(annotated_code)
lines = expected_annotations.lines
code = lines.join('\n')
if code == annotated_code
raise "Use `expect_no_issues` to assert that no issues are found"
end
source, actual_annotations = actual_annotations(rules, code, path, lines)
unless actual_annotations == expected_annotations
fail <<-MSG, file, line
Expected:
#{expected_annotations}
Got:
#{actual_annotations}
MSG
end
source
end
def expect_correction(source, correction, *, file = __FILE__, line = __LINE__)
raise "Use `expect_no_corrections` if the code will not change" unless source.correct!
return if correction == source.code
fail <<-MSG, file, line
Expected correction:
#{correction}
Got:
#{source.code}
MSG
end
def expect_no_corrections(source, *, file = __FILE__, line = __LINE__)
return unless source.correct!
fail <<-MSG, file, line
Expected no corrections, but got:
#{source.code}
MSG
end
def expect_no_issues(rules : Rule::Base | Enumerable(Rule::Base),
code : String,
path = "",
*,
file = __FILE__,
line = __LINE__)
lines = code.split('\n') # must preserve trailing newline
_, actual_annotations = actual_annotations(rules, code, path, lines)
return if actual_annotations.to_s == code
fail <<-MSG, file, line
Expected no issues, but got:
#{actual_annotations}
MSG
end
private def actual_annotations(rules, code, path, lines)
source = Source.new(code, path, normalize: false)
if rules.is_a?(Enumerable)
rules.each(&.catch(source))
else
rules.catch(source)
end
{source, AnnotatedSource.new(lines, source.issues)}
end
private def format_issue(code, **replacements)
replacements.each do |keyword, value|
value = value.to_s
code = code
.gsub("%{#{keyword}}", value)
.gsub("^{#{keyword}}", "^" * value.size)
.gsub("_{#{keyword}}", " " * value.size)
end
code
end
end
================================================
FILE: src/ameba/spec/support.cr
================================================
# Require this file to load code that supports testing Ameba rules.
require "./be_valid"
require "./expect_issue"
require "./util"
module Ameba
class Source
include Spec::Util
def initialize(code : String, @path = "", normalize = true)
@code = normalize ? normalize_code(code) : code
end
end
end
include Ameba::Spec::BeValid
include Ameba::Spec::ExpectIssue
================================================
FILE: src/ameba/spec/util.cr
================================================
module Ameba::Spec::Util
def normalize_code(code, separator = '\n')
lines = code.split(separator)
# remove unneeded first blank lines if any
lines.shift if lines[0].blank? && lines.size > 1
# find the minimum indentation
min_indent = lines.min_of do |line|
line.blank? ? code.size : line.size - line.lstrip.size
end
# remove the width of minimum indentation in each line
lines.join(separator) do |line|
line.blank? ? line : line[min_indent..]
end
end
end
================================================
FILE: src/ameba/tokenizer.cr
================================================
require "compiler/crystal/syntax/*"
module Ameba
# Represents Crystal syntax tokenizer based on `Crystal::Lexer`.
#
# ```
# source = Ameba::Source.new(code, path)
# tokenizer = Ameba::Tokenizer.new(source)
# tokenizer.run do |token|
# puts token
# end
# ```
class Tokenizer
# Instantiates Tokenizer using a `source`.
#
# ```
# source = Ameba::Source.new(code, path)
# Ameba::Tokenizer.new(source)
# ```
def initialize(source)
@lexer = Crystal::Lexer.new source.code
@lexer.count_whitespace = true
@lexer.comments_enabled = true
@lexer.wants_raw = true
@lexer.filename = source.path
end
# Instantiates Tokenizer using a `lexer`.
#
# ```
# lexer = Crystal::Lexer.new(code)
# Ameba::Tokenizer.new(lexer)
# ```
def initialize(@lexer : Crystal::Lexer)
end
# Runs the tokenizer and yields each token as a block argument.
#
# ```
# Ameba::Tokenizer.new(source).run do |token|
# puts token
# end
# ```
def run(&block : Crystal::Token -> _)
run_normal_state @lexer, &block
true
rescue Crystal::SyntaxException
false
end
private def run_normal_state(lexer, break_on_rcurly = false, &block : Crystal::Token -> _)
loop do
token = @lexer.next_token
block.call token
case token.type
when .delimiter_start?
run_delimiter_state lexer, token, &block
when .string_array_start?, .symbol_array_start?
run_array_state lexer, token, &block
when .eof?
break
when .op_rcurly?
break if break_on_rcurly
end
end
end
private def run_delimiter_state(lexer, token, &block : Crystal::Token -> _)
loop do
token = @lexer.next_string_token(token.delimiter_state)
block.call token
case token.type
when .interpolation_start?
run_normal_state lexer, break_on_rcurly: true, &block
when .delimiter_end?, .eof?
break
end
end
end
private def run_array_state(lexer, token, &block : Crystal::Token -> _)
loop do
# NOTE: Crystal::Token is a class and the lexer modifies @token in place,
# so the assignment here is for clarity/consistency with run_delimiter_state,
# not for correctness (the behavior is identical without the assignment).
token = lexer.next_string_array_token
block.call token
case token.type
when .string_array_end?, .eof?
break
end
end
end
end
end
================================================
FILE: src/ameba/version.cr
================================================
module Ameba
VERSION = detect_version
# Detects Ameba version from several sources.
private macro detect_version
{%
version =
env("AMEBA_BUILD_VERSION") ||
read_file?("#{__DIR__}/../../VERSION") ||
`shards version "#{__DIR__}"`.stringify
%}
{{ version.chomp }}
end
class Version
{% if flag?(:windows) %}
private GIT_SHA = nil
{% else %}
private GIT_SHA =
{{ `(git rev-parse --short HEAD || true) 2>/dev/null`.chomp.stringify }}.presence
{% end %}
# Cached version object.
INSTANCE = begin
version = SemanticVersion.parse(VERSION)
unless version.build
version = version.copy_with \
build: GIT_SHA.try { |commit| "git.commit.#{commit}" }
end
new(version)
end
# Returns the current version as a `SemanticVersion` object.
getter version : SemanticVersion
def initialize(@version)
end
# Appends the version string to the given *io*.
def to_s(io : IO) : Nil
version.to_s(io)
end
# Returns the `version` without prerelease and build metadata.
def for_production : String
version.copy_with(prerelease: nil, build: nil).to_s
end
# Returns the `version` without prerelease and build metadata.
def for_docs : String
dev? ? "master" : for_production
end
# Returns `true` if the current `version` is a development version.
def dev? : Bool
version.prerelease.identifiers.any?("dev")
end
# # Returns `true` if the current `version` is a release candidate version.
def release_candidate? : Bool
version.prerelease.identifiers.any?(/^rc-?(\d+)?$/)
end
end
end
================================================
FILE: src/ameba.cr
================================================
# Ameba's entry module.
#
# To run the linter with default parameters:
#
# ```
# Ameba.run
# ```
#
# To configure and run it:
#
# ```
# config = Ameba::Config.load
# config.formatter = formatter
# config.files = file_paths
#
# Ameba.run config
# ```
module Ameba
extend self
# Returns the version object of Ameba.
def version : Version
Version::INSTANCE
end
# Initializes `Ameba::Runner` and runs it.
# Can be configured via `config` parameter.
#
# Examples:
#
# ```
# Ameba.run
# Ameba.run config
# ```
def run(config = Config.load)
Runner.new(config).run
end
end
require "./ameba/*"
require "./ameba/ast/**"
require "./ameba/ext/**"
require "./ameba/rule/**"
require "./ameba/formatter/*"
require "./ameba/presenter/*"
require "./ameba/source/**"
================================================
FILE: src/cli.cr
================================================
require "./ameba/cli/cmd"
begin
exit Ameba::CLI.run ? 0 : 1
rescue ex
STDERR.puts "Error: #{ex.message}"
exit 255
end
================================================
FILE: src/contrib/read_type_doc.cr
================================================
require "compiler/crystal/syntax/*"
private class DocFinder < Crystal::Visitor
getter type_name : String
getter doc : String?
def initialize(nodes, @type_name)
nodes.accept self
end
def visit(node : Crystal::ASTNode)
return false if @doc
if node.responds_to?(:name) && (name = node.name).is_a?(Crystal::Path)
@doc = node.doc if name.names.last? == @type_name
end
true
end
end
type_name, path_to_source_file = ARGV
source = File.read(path_to_source_file)
nodes = Crystal::Parser.new(source)
.tap(&.wants_doc = true)
.parse
puts DocFinder.new(nodes, type_name).doc
================================================
FILE: src/json-schema-builder.cr
================================================
require "./ameba"
require "./ameba/json_schema/*"
JSON_SCHEMA_FILEPATH =
Path[__DIR__, "..", ".ameba.yml.schema.json"].expand
Ameba::JSONSchema::Builder.build(JSON_SCHEMA_FILEPATH)