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

{{ article.name }}

HTML CRYSTAL expect_correction source, <<-CRYSTAL <<-HTML

{{ article.name }}

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

{{ article.name }}

HTML CRYSTAL expect_correction source, <<-CRYSTAL <<-HTML

{{ article.name }}

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

{{ article.name }}

HTML CRYSTAL expect_correction source, <<-CRYSTAL <<-HTML

{{ article.name }}

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)) {% 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)