├── .credo.exs ├── .devcontainer └── devcontainer.json ├── .formatter.exs ├── .github └── workflows │ ├── ci-workflow.yml │ ├── code-scan-sarif.yml │ ├── compatibility-canary-smoke-tests.yml │ ├── compatibility-elixir.yml │ ├── compatibility-os-mac.yml │ ├── compatibility-os-windows.yml │ ├── compatibility-phoenix.yml │ ├── housekeeping-bugfix-reproducer.yml │ └── housekeeping.yml ├── .gitignore ├── .template.check.ex ├── .template.debug.html ├── .tool-versions ├── CHANGELOG.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── RELEASING.md ├── assets ├── credo-logo-with-trail-and-writing.png ├── credo-logo-with-trail.png ├── credo-logo.png └── screenshot.png ├── config └── config.exs ├── guides ├── commands │ ├── diff_command.md │ ├── explain_command.md │ ├── list_command.md │ └── suggest_command.md ├── configuration │ ├── check_params.md │ ├── cli_switches.md │ ├── config_comments.md │ └── config_file.md ├── custom_checks │ ├── adding_checks.md │ ├── improving_checks.md │ └── testing_checks.md ├── introduction │ ├── basic_usage.md │ ├── exit_statuses.md │ ├── installation.md │ ├── mix_tasks.md │ └── overview.md └── plugins │ ├── creating_plugins.md │ └── using_plugins.md ├── lib ├── credo.ex ├── credo │ ├── application.ex │ ├── check.ex │ ├── check │ │ ├── config_comment.ex │ │ ├── config_comment_finder.ex │ │ ├── consistency │ │ │ ├── collector.ex │ │ │ ├── exception_names.ex │ │ │ ├── exception_names │ │ │ │ └── collector.ex │ │ │ ├── line_endings.ex │ │ │ ├── line_endings │ │ │ │ └── collector.ex │ │ │ ├── multi_alias_import_require_use.ex │ │ │ ├── multi_alias_import_require_use │ │ │ │ └── collector.ex │ │ │ ├── parameter_pattern_matching.ex │ │ │ ├── parameter_pattern_matching │ │ │ │ └── collector.ex │ │ │ ├── space_around_operators.ex │ │ │ ├── space_around_operators │ │ │ │ ├── collector.ex │ │ │ │ └── space_helper.ex │ │ │ ├── space_in_parentheses.ex │ │ │ ├── space_in_parentheses │ │ │ │ └── collector.ex │ │ │ ├── tabs_or_spaces.ex │ │ │ ├── tabs_or_spaces │ │ │ │ └── collector.ex │ │ │ ├── unused_variable_names.ex │ │ │ └── unused_variable_names │ │ │ │ └── collector.ex │ │ ├── design │ │ │ ├── alias_usage.ex │ │ │ ├── duplicated_code.ex │ │ │ ├── skip_test_without_comment.ex │ │ │ ├── tag_fixme.ex │ │ │ ├── tag_helper.ex │ │ │ └── tag_todo.ex │ │ ├── params.ex │ │ ├── readability │ │ │ ├── alias_as.ex │ │ │ ├── alias_order.ex │ │ │ ├── block_pipe.ex │ │ │ ├── function_names.ex │ │ │ ├── impl_true.ex │ │ │ ├── large_numbers.ex │ │ │ ├── max_line_length.ex │ │ │ ├── module_attribute_names.ex │ │ │ ├── module_doc.ex │ │ │ ├── module_names.ex │ │ │ ├── multi_alias.ex │ │ │ ├── nested_function_calls.ex │ │ │ ├── one_arity_function_in_pipe.ex │ │ │ ├── one_pipe_per_line.ex │ │ │ ├── parentheses_in_condition.ex │ │ │ ├── parentheses_on_zero_arity_defs.ex │ │ │ ├── pipe_into_anonymous_functions.ex │ │ │ ├── predicate_function_names.ex │ │ │ ├── prefer_implicit_try.ex │ │ │ ├── prefer_unquoted_atoms.ex │ │ │ ├── redundant_blank_lines.ex │ │ │ ├── semicolons.ex │ │ │ ├── separate_alias_require.ex │ │ │ ├── single_function_to_block_pipe.ex │ │ │ ├── single_pipe.ex │ │ │ ├── space_after_commas.ex │ │ │ ├── specs.ex │ │ │ ├── strict_module_layout.ex │ │ │ ├── string_sigils.ex │ │ │ ├── trailing_blank_line.ex │ │ │ ├── trailing_white_space.ex │ │ │ ├── unnecessary_alias_expansion.ex │ │ │ ├── variable_names.ex │ │ │ ├── with_custom_tagged_tuple.ex │ │ │ └── with_single_clause.ex │ │ ├── refactor │ │ │ ├── abc_size.ex │ │ │ ├── append_single_item.ex │ │ │ ├── apply.ex │ │ │ ├── case_trivial_matches.ex │ │ │ ├── cond_statements.ex │ │ │ ├── cyclomatic_complexity.ex │ │ │ ├── double_boolean_negation.ex │ │ │ ├── enum_helpers.ex │ │ │ ├── filter_count.ex │ │ │ ├── filter_filter.ex │ │ │ ├── filter_reject.ex │ │ │ ├── function_arity.ex │ │ │ ├── io_puts.ex │ │ │ ├── long_quote_blocks.ex │ │ │ ├── map_into.ex │ │ │ ├── map_join.ex │ │ │ ├── map_map.ex │ │ │ ├── match_in_condition.ex │ │ │ ├── module_dependencies.ex │ │ │ ├── negated_conditions_in_unless.ex │ │ │ ├── negated_conditions_with_else.ex │ │ │ ├── negated_is_nil.ex │ │ │ ├── nesting.ex │ │ │ ├── pass_async_in_test_cases.ex │ │ │ ├── perceived_complexity.ex │ │ │ ├── pipe_chain_start.ex │ │ │ ├── redundant_with_clause_result.ex │ │ │ ├── reject_filter.ex │ │ │ ├── reject_reject.ex │ │ │ ├── unless_with_else.ex │ │ │ ├── utc_now_truncate.ex │ │ │ ├── variable_rebinding.ex │ │ │ └── with_clauses.ex │ │ ├── runner.ex │ │ └── warning │ │ │ ├── application_config_in_module_attribute.ex │ │ │ ├── bool_operation_on_same_values.ex │ │ │ ├── dbg.ex │ │ │ ├── expensive_empty_enum_check.ex │ │ │ ├── forbidden_module.ex │ │ │ ├── iex_pry.ex │ │ │ ├── io_inspect.ex │ │ │ ├── lazy_logging.ex │ │ │ ├── leaky_environment.ex │ │ │ ├── map_get_unsafe_pass.ex │ │ │ ├── missed_metadata_key_in_logger_config.ex │ │ │ ├── mix_env.ex │ │ │ ├── operation_on_same_values.ex │ │ │ ├── operation_with_constant_result.ex │ │ │ ├── raise_inside_rescue.ex │ │ │ ├── spec_with_struct.ex │ │ │ ├── unsafe_exec.ex │ │ │ ├── unsafe_to_atom.ex │ │ │ ├── unused_enum_operation.ex │ │ │ ├── unused_file_operation.ex │ │ │ ├── unused_function_return_helper.ex │ │ │ ├── unused_keyword_operation.ex │ │ │ ├── unused_list_operation.ex │ │ │ ├── unused_operation.ex │ │ │ ├── unused_path_operation.ex │ │ │ ├── unused_regex_operation.ex │ │ │ ├── unused_string_operation.ex │ │ │ ├── unused_tuple_operation.ex │ │ │ └── wrong_test_file_extension.ex │ ├── cli.ex │ ├── cli │ │ ├── command.ex │ │ ├── command │ │ │ ├── categories │ │ │ │ ├── categories_command.ex │ │ │ │ ├── categories_output.ex │ │ │ │ └── output │ │ │ │ │ ├── default.ex │ │ │ │ │ └── json.ex │ │ │ ├── diff │ │ │ │ ├── diff_command.ex │ │ │ │ ├── diff_output.ex │ │ │ │ ├── diff_summary.ex │ │ │ │ ├── output │ │ │ │ │ ├── default.ex │ │ │ │ │ ├── flycheck.ex │ │ │ │ │ ├── json.ex │ │ │ │ │ └── oneline.ex │ │ │ │ └── task │ │ │ │ │ ├── filter_issues.ex │ │ │ │ │ ├── filter_issues_for_exit_status.ex │ │ │ │ │ ├── get_git_diff.ex │ │ │ │ │ ├── print_before_info.ex │ │ │ │ │ └── print_results_and_summary.ex │ │ │ ├── explain │ │ │ │ ├── explain_command.ex │ │ │ │ ├── explain_output.ex │ │ │ │ └── output │ │ │ │ │ ├── default.ex │ │ │ │ │ └── json.ex │ │ │ ├── gen.check.ex │ │ │ ├── gen.config.ex │ │ │ ├── help.ex │ │ │ ├── info │ │ │ │ ├── info_command.ex │ │ │ │ ├── info_output.ex │ │ │ │ └── output │ │ │ │ │ ├── default.ex │ │ │ │ │ └── json.ex │ │ │ ├── list │ │ │ │ ├── list_command.ex │ │ │ │ ├── list_output.ex │ │ │ │ └── output │ │ │ │ │ ├── default.ex │ │ │ │ │ ├── flycheck.ex │ │ │ │ │ ├── json.ex │ │ │ │ │ ├── oneline.ex │ │ │ │ │ └── sarif.ex │ │ │ ├── suggest │ │ │ │ ├── output │ │ │ │ │ ├── default.ex │ │ │ │ │ ├── flycheck.ex │ │ │ │ │ ├── json.ex │ │ │ │ │ ├── oneline.ex │ │ │ │ │ └── sarif.ex │ │ │ │ ├── suggest_command.ex │ │ │ │ └── suggest_output.ex │ │ │ └── version.ex │ │ ├── exit_status.ex │ │ ├── filename.ex │ │ ├── filter.ex │ │ ├── options.ex │ │ ├── output.ex │ │ ├── output │ │ │ ├── first_run_hint.ex │ │ │ ├── format_delegator.ex │ │ │ ├── formatter │ │ │ │ ├── flycheck.ex │ │ │ │ ├── json.ex │ │ │ │ ├── oneline.ex │ │ │ │ └── sarif.ex │ │ │ ├── shell.ex │ │ │ ├── summary.ex │ │ │ └── ui.ex │ │ ├── sorter.ex │ │ ├── switch.ex │ │ └── task │ │ │ ├── load_and_validate_source_files.ex │ │ │ ├── prepare_checks_to_run.ex │ │ │ ├── run_checks.ex │ │ │ └── set_relevant_issues.ex │ ├── code.ex │ ├── code │ │ ├── block.ex │ │ ├── charlists.ex │ │ ├── heredocs.ex │ │ ├── interpolation_helper.ex │ │ ├── module.ex │ │ ├── name.ex │ │ ├── parameters.ex │ │ ├── scope.ex │ │ ├── sigils.ex │ │ ├── strings.ex │ │ ├── token.ex │ │ └── token_ast_correlation.ex │ ├── config_builder.ex │ ├── config_file.ex │ ├── execution.ex │ ├── execution │ │ ├── execution_config_files.ex │ │ ├── execution_issues.ex │ │ ├── execution_source_files.ex │ │ ├── execution_timing.ex │ │ ├── task.ex │ │ └── task │ │ │ ├── append_default_config.ex │ │ │ ├── append_extra_config.ex │ │ │ ├── assign_exit_status_for_issues.ex │ │ │ ├── convert_cli_options_to_config.ex │ │ │ ├── determine_command.ex │ │ │ ├── initialize_command.ex │ │ │ ├── initialize_plugins.ex │ │ │ ├── parse_options.ex │ │ │ ├── require_requires.ex │ │ │ ├── run_command.ex │ │ │ ├── set_default_command.ex │ │ │ ├── use_colors.ex │ │ │ ├── validate_config.ex │ │ │ ├── validate_options.ex │ │ │ └── write_debug_report.ex │ ├── exs_loader.ex │ ├── issue.ex │ ├── issue_meta.ex │ ├── plugin.ex │ ├── priority.ex │ ├── service │ │ ├── config_files.ex │ │ ├── ets_table_helper.ex │ │ ├── source_file_ast.ex │ │ ├── source_file_lines.ex │ │ ├── source_file_scope_priorities.ex │ │ ├── source_file_scopes.ex │ │ └── source_file_source.ex │ ├── severity.ex │ ├── source_file.ex │ ├── sources.ex │ ├── test │ │ ├── assertions.ex │ │ ├── case.ex │ │ ├── check_runner.ex │ │ └── source_files.ex │ └── watcher.ex └── mix │ └── tasks │ ├── credo.ex │ ├── credo.gen.check.ex │ └── credo.gen.config.ex ├── mix.exs ├── mix.lock └── test ├── check_formatted.sh ├── credo ├── check │ ├── config_comment_finder_test.exs │ ├── consistency │ │ ├── exception_names_test.exs │ │ ├── line_endings_test.exs │ │ ├── multi_alias_import_require_use │ │ │ └── collector_test.exs │ │ ├── multi_alias_import_require_use_test.exs │ │ ├── parameter_pattern_matching │ │ │ └── collector_test.exs │ │ ├── parameter_pattern_matching_test.exs │ │ ├── space_around_operators │ │ │ └── collector_test.exs │ │ ├── space_around_operators_test.exs │ │ ├── space_in_parentheses │ │ │ └── collector_test.exs │ │ ├── space_in_parentheses_test.exs │ │ ├── tabs_or_spaces_test.exs │ │ └── unused_variable_names_test.exs │ ├── design │ │ ├── alias_usage_test.exs │ │ ├── duplicated_code_test.exs │ │ ├── skip_test_without_comment_test.exs │ │ ├── tag_fixme_test.exs │ │ ├── tag_helper_test.exs │ │ └── tag_todo_test.exs │ ├── housekeeping_heredocs_in_tests.exs │ ├── housekeeping_params.exs │ ├── housekeeping_trigger.exs │ ├── params_test.exs │ ├── readability │ │ ├── alias_as_test.exs │ │ ├── alias_order_test.exs │ │ ├── block_pipe_test.exs │ │ ├── function_names_test.exs │ │ ├── impl_true_test.exs │ │ ├── large_numbers_test.exs │ │ ├── max_line_length_test.exs │ │ ├── module_attribute_names_test.exs │ │ ├── module_doc_test.exs │ │ ├── module_names_test.exs │ │ ├── multi_alias_test.exs │ │ ├── nested_function_calls_test.exs │ │ ├── one_arity_function_in_pipe_test.exs │ │ ├── one_pipe_per_line_test.exs │ │ ├── parentheses_in_condition_test.exs │ │ ├── parentheses_on_zero_arity_defs_test.exs │ │ ├── pipe_into_anonymous_functions_test.exs │ │ ├── predicate_function_names_test.exs │ │ ├── prefer_implicit_try_test.exs │ │ ├── prefer_unquoted_atoms_test.exs │ │ ├── redundant_blank_lines_test.exs │ │ ├── semicolons_test.exs │ │ ├── separate_alias_require_test.exs │ │ ├── single_function_to_block_pipe_test.exs │ │ ├── single_pipe_test.exs │ │ ├── space_after_commas_test.exs │ │ ├── specs_test.exs │ │ ├── strict_module_layout_test.exs │ │ ├── string_sigils_test.exs │ │ ├── trailing_blank_line_test.exs │ │ ├── trailing_white_space_test.exs │ │ ├── unnecessary_alias_expansion_test.exs │ │ ├── variable_names_test.exs │ │ ├── with_custom_tagged_tuple_test.exs │ │ └── with_single_clause_test.exs │ ├── refactor │ │ ├── abc_size_test.exs │ │ ├── append_single_item_test.exs │ │ ├── apply_test.exs │ │ ├── case_trivial_matches_test.exs │ │ ├── cond_statements_test.exs │ │ ├── cyclomatic_complexity_test.exs │ │ ├── double_boolean_negation_test.exs │ │ ├── filter_count_test.exs │ │ ├── filter_filter_test.exs │ │ ├── filter_reject_test.exs │ │ ├── function_arity_test.exs │ │ ├── io_puts_test.exs │ │ ├── long_quote_blocks_test.exs │ │ ├── map_into_test.exs │ │ ├── map_join_test.exs │ │ ├── map_map_test.exs │ │ ├── match_in_condition_test.exs │ │ ├── module_dependencies_test.exs │ │ ├── module_size_test.exs │ │ ├── negated_conditions_in_unless_test.exs │ │ ├── negated_conditions_with_else_test.exs │ │ ├── negated_is_nil_test.exs │ │ ├── nesting_test.exs │ │ ├── pass_async_in_test_cases_test.exs │ │ ├── perceived_complexity_test.exs │ │ ├── pipe_chain_start_test.exs │ │ ├── redundant_with_clause_result_test.exs │ │ ├── regex_empty_character_classes_test.exs │ │ ├── regex_multiple_spaces_test.exs │ │ ├── reject_filter_test.exs │ │ ├── reject_reject_test.exs │ │ ├── unless_with_else_test.exs │ │ ├── utc_now_truncate_test.exs │ │ ├── variable_rebinding_test.exs │ │ └── with_clauses_test.exs │ ├── runner_test.exs │ └── warning │ │ ├── application_config_in_module_attribute_test.exs │ │ ├── bool_operation_on_same_values_test.exs │ │ ├── dbg_test.exs │ │ ├── expensive_empty_enum_check_test.exs │ │ ├── forbidden_module_test.exs │ │ ├── iex_pry_test.exs │ │ ├── io_inspect_test.exs │ │ ├── lazy_logging_test.exs │ │ ├── leaky_environment_test.exs │ │ ├── map_get_unsafe_pass_test.exs │ │ ├── missed_metadata_key_in_logger_config_test.exs │ │ ├── mix_env_test.exs │ │ ├── operation_on_same_values_test.exs │ │ ├── operation_with_constant_result_test.exs │ │ ├── raise_inside_rescue_test.exs │ │ ├── spec_with_struct_test.exs │ │ ├── unreachable_code_test.exs │ │ ├── unsafe_exec_test.exs │ │ ├── unsafe_to_atom_test.exs │ │ ├── unused_enum_operation_test.exs │ │ ├── unused_file_operation_test.exs │ │ ├── unused_keyword_operation_test.exs │ │ ├── unused_list_operation_test.exs │ │ ├── unused_path_operation_test.exs │ │ ├── unused_regex_operation_test.exs │ │ ├── unused_string_operation_test.exs │ │ ├── unused_tuple_operation_test.exs │ │ └── wrong_test_file_extension_test.exs ├── check_test.exs ├── cli │ ├── command │ │ └── gen.check_test.exs │ ├── filename_test.exs │ ├── options_test.exs │ ├── output │ │ ├── formatter │ │ │ └── json_test.exs │ │ ├── output_test.exs │ │ ├── summary_test.exs │ │ └── ui_test.exs │ └── sorter_test.exs ├── cli_test.exs ├── code │ ├── block_test.exs │ ├── charlists_test.exs │ ├── heredocs_test.exs │ ├── interpolation_helper_test.exs │ ├── module_test.exs │ ├── name_test.exs │ ├── parameters_test.exs │ ├── scope_test.exs │ ├── sigils_test.exs │ ├── strings_test.exs │ ├── token_ast_correlation_test.exs │ └── token_test.exs ├── code_test.exs ├── config_file_test.exs ├── config_test.exs ├── execution_test.exs ├── exs_loader_test.exs ├── plugin_test.exs ├── priority_test.exs ├── source_file_test.exs ├── sources_test.exs ├── task_group_test.exs └── test │ ├── assertions_test.exs │ ├── check_runner_test.exs │ └── source_files_test.exs ├── error_if_warnings.sh ├── fixtures ├── custom-config.exs ├── custom-config.exs.malformed ├── example_code │ ├── browser.ex │ ├── browser2.ex │ ├── clean.ex │ ├── clean_redux.ex │ ├── large_heredoc.ex │ └── nested_escaped_heredocs.ex ├── example_plugin_integration │ ├── .credo.exs │ └── plugin │ │ ├── .credo.exs │ │ └── example_plugin.ex ├── file_exclusion │ ├── .credo.exs │ ├── bar.ex │ └── foo.ex ├── integration_diff_bare_repo │ ├── HEAD │ ├── config │ ├── description │ ├── hooks │ │ ├── applypatch-msg.sample │ │ ├── commit-msg.sample │ │ ├── fsmonitor-watchman.sample │ │ ├── post-update.sample │ │ ├── pre-applypatch.sample │ │ ├── pre-commit.sample │ │ ├── pre-push.sample │ │ ├── pre-rebase.sample │ │ ├── pre-receive.sample │ │ ├── prepare-commit-msg.sample │ │ └── update.sample │ ├── info │ │ └── exclude │ ├── objects │ │ ├── 10 │ │ │ └── df72ae94e2a666e0a7cbe5048e1833a357cfe5 │ │ ├── 11 │ │ │ └── 6272d9408606a4b8f165fb149af23d60be026d │ │ ├── 17 │ │ │ └── 8f9317eb57724fccf09c74e0baa197bb12f7f2 │ │ ├── 34 │ │ │ └── 210f45e4d1b0acdcc84355e2b97a58283000dc │ │ ├── 44 │ │ │ └── f8a14333a7420fe874edc720f4bfcc860c3ed6 │ │ ├── 47 │ │ │ └── 73d949779e8dddc38d6a83c9e61783bd339db8 │ │ ├── 48 │ │ │ └── 3d58ae9dc2f1605d42941db6b4758a04b17ac6 │ │ ├── 69 │ │ │ └── 98e81bb3bc22abdda5761f14be6629685665bc │ │ ├── 77 │ │ │ ├── 3fd798caaad39039c5f91b1cb31bc22ba15fc9 │ │ │ └── f632647a8fdfe6b315ab8b57f7c4dc313913dc │ │ ├── 09 │ │ │ └── 02a2715e3db943e9fffc8ed626025a136dbc4a │ │ ├── 2b │ │ │ └── 3a6a1db133fc0f2960bac3983164d4f9a9c0c5 │ │ ├── 2f │ │ │ └── 4df54614af76459e53a38d823f427cf63e2a18 │ │ ├── 5a │ │ │ └── 96e335044fa25ce25014326955c8bd29af5ec4 │ │ ├── 7c │ │ │ └── 9eeb29209cf8ffc7bcf8218a21ba2f342567ec │ │ ├── 8e │ │ │ └── 2b266f96f0e56d55ded352b6d31182e3097752 │ │ ├── 9b │ │ │ └── 86f40b6685ac801fc8689ed2f03d0738c3ea1c │ │ ├── 9e │ │ │ └── 033f62a8a2c66e6f145544391645398e7c5611 │ │ ├── ab │ │ │ └── 03fb0cb38ef86c7970f21850fa0b10d80f8980 │ │ ├── b3 │ │ │ └── 13235066408ce34cc5e63b4e12394752608c99 │ │ ├── b8 │ │ │ └── 263030c824a096f058e1f1f79902b7b93a609d │ │ ├── b9 │ │ │ └── 0909b828423fb723dddd82a97e35d3a325f6e5 │ │ ├── c9 │ │ │ └── 2b67021398e462ff5639754c90402551c26263 │ │ ├── cc │ │ │ └── 9d83be165d09ae5e5ebc1ad4af03bea02da2a8 │ │ ├── da │ │ │ └── 72fa8ddba1fc83545a0b438b9c5529d2293d88 │ │ ├── dd │ │ │ └── 5c65c9db1b5e530170e59660e39e2628fb2a1e │ │ ├── e5 │ │ │ └── 5746a6c642f990aaa909a1486cd10af696e240 │ │ ├── e6 │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ ├── e7 │ │ │ └── 0168dfb1ba5f51a80373b6af7fc4c342340c09 │ │ ├── ed │ │ │ └── 634b278ad31b9c327f6666e084779827ab2f59 │ │ ├── ef │ │ │ └── e7406369ccdc9262379450dd0a0540dd6627bd │ │ └── f2 │ │ │ └── dc5fdb387767bef8f24a34f68ee593b84de434 │ └── refs │ │ ├── heads │ │ └── master │ │ └── tags │ │ ├── add-doc-fun-with-cond-issue │ │ ├── add-fun-with-cond-issue │ │ ├── add-two-todo-comments │ │ └── move-todo-comments ├── integration_test_config │ ├── .credo.exs │ ├── .sarif.exs │ └── lib │ │ ├── clean.ex │ │ └── clean │ │ ├── clean_redux.ex │ │ └── dirty.ex └── options │ ├── cmd1 │ └── .keep │ ├── foo.ex │ └── src │ └── .keep ├── integration ├── categories_test.exs ├── diff_test.exs ├── explain_test.exs ├── gen_check_test.exs ├── gen_config_test.exs ├── help_test.exs ├── info_test.exs ├── list_test.exs ├── sarif_test.exs ├── suggest_test.exs └── version_test.exs ├── old_credo.exs ├── regression ├── run.sh └── run_older_credo_version.exs ├── run_on_project.sh ├── runtime_warnings.sh ├── smoke_test.sh ├── test_helper.exs ├── test_if_tests_fail_after_resetting_lib.sh └── test_phoenix_compatibility.sh /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers-contrib/features/elixir-asdf:2": {} 5 | }, 6 | "customizations": { 7 | "vscode": { 8 | "settings": { 9 | "editor.formatOnSave": true 10 | }, 11 | "extensions": [ 12 | "jakebecker.elixir-ls", 13 | "samuel-pordeus.elixir-test" 14 | ] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" and to export configuration. 2 | export_locals_without_parens = [ 3 | run: 1, 4 | run: 2, 5 | activity: 1, 6 | activity: 2 7 | ] 8 | 9 | [ 10 | inputs: ["{mix,.formatter,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"], 11 | locals_without_parens: export_locals_without_parens, 12 | export: [locals_without_parens: export_locals_without_parens] 13 | ] 14 | -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yml: -------------------------------------------------------------------------------- 1 | name: "CI Tests" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - release/* 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-24.04 14 | name: "Elixir ${{matrix.elixir}} OTP ${{matrix.otp}}" 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | otp: [24.3, 25.3, 26.2] 19 | elixir: [1.14.5, 1.15.7, 1.16.2, 1.17.3, 1.18.1] 20 | exclude: 21 | - elixir: 1.14.5 22 | otp: 26.2 23 | - elixir: 1.11.4 24 | otp: 25.3 25 | - elixir: 1.12.3 26 | otp: 25.3 27 | - elixir: 1.14.5 28 | otp: 25.3 29 | - elixir: 1.15.7 30 | otp: 23.3 31 | - elixir: 1.15.7 32 | otp: 24.3 33 | - elixir: 1.16.2 34 | otp: 24.3 35 | - elixir: 1.17.3 36 | otp: 24.3 37 | - elixir: 1.18.1 38 | otp: 24.3 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | - uses: erlef/setup-beam@v1 45 | with: 46 | otp-version: ${{matrix.otp}} 47 | elixir-version: ${{matrix.elixir}} 48 | - run: mix deps.get 49 | - run: mix deps.compile 50 | - run: mix compile --warnings-as-errors 51 | - run: mix test 52 | - run: ./test/runtime_warnings.sh 53 | - run: ./test/smoke_test.sh 54 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-elixir.yml: -------------------------------------------------------------------------------- 1 | name: "Compatibility: Elixir" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - release/* 7 | 8 | jobs: 9 | test_on_source: 10 | runs-on: ubuntu-24.04 11 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')" 12 | name: "Elixir ${{matrix.elixir}} OTP ${{matrix.otp}} - Credo running on Elixir ${{matrix.repo_branch}} source code" 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | repo_url: ["https://github.com/elixir-lang/elixir.git"] 17 | repo_branch: ["v1.13", "main"] 18 | otp: [24.3, 25.3, 26.2] 19 | elixir: [1.14.5, 1.15.7, 1.16.2, 1.17.3, 1.18.1] 20 | exclude: 21 | - elixir: 1.14.5 22 | otp: 26.2 23 | - elixir: 1.14.5 24 | otp: 25.3 25 | - elixir: 1.15.7 26 | otp: 23.3 27 | - elixir: 1.15.7 28 | otp: 24.3 29 | - elixir: 1.16.2 30 | otp: 24.3 31 | - elixir: 1.17.3 32 | otp: 24.3 33 | - elixir: 1.18.1 34 | otp: 24.3 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: erlef/setup-beam@v1 38 | with: 39 | otp-version: ${{matrix.otp}} 40 | elixir-version: ${{matrix.elixir}} 41 | - run: mix deps.get 42 | - run: mix deps.compile 43 | - run: mix compile 44 | - run: mkdir -p tmp 45 | - run: git clone ${{matrix.repo_url}} tmp/${{matrix.repo_branch}} --depth=1 --branch ${{matrix.repo_branch}} 46 | - run: mix credo tmp/${{matrix.repo_branch}} --strict --mute-exit-status 47 | -------------------------------------------------------------------------------- /.github/workflows/housekeeping-bugfix-reproducer.yml: -------------------------------------------------------------------------------- 1 | name: "Housekeeping: Reproducing Test-Case Detector (experimental)" 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**_test.exs' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-24.04 11 | name: "Test for lib/ changes" 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: 27.3 18 | elixir-version: 1.18.3 19 | 20 | - run: git fetch origin master:master 21 | 22 | - name: Check changes to lib/ 23 | id: check_changes 24 | run: echo "::set-output name=changes_to_lib::$(git diff --name-only master | grep "^lib")" 25 | 26 | - name: There are changes to lib/ 27 | if: "contains(steps.check_changes.outputs.changes_to_lib, 'lib')" 28 | run: | 29 | mix deps.get 30 | sh test/test_if_tests_fail_after_resetting_lib.sh 31 | 32 | - name: There are no changes to lib/ 33 | if: "!contains(steps.check_changes.outputs.changes_to_lib, 'lib')" 34 | run: echo "${{ toJSON(steps.check_changes.outputs.changes_to_lib) }}" 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /.credo 3 | /.elixir_ls/ 4 | /.idea 5 | /.vscode 6 | /cover 7 | /deps 8 | /doc 9 | /docs 10 | /lib/my_first_credo_check.ex 11 | /test/fixtures/integration_diff_cloned_repo 12 | /tmp 13 | .tool-versions-e 14 | *.ez 15 | *.iml 16 | credo-debug-log.html 17 | erl_crash.dump 18 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 27.0 2 | elixir ref:main 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 1. [Fork it!](http://github.com/rrrene/credo/fork) 2 | 2. Create your feature branch (`git checkout -b my-new-feature`) 3 | 3. Commit your changes (`git commit -am 'Add some feature'`) 4 | 4. Push to the branch (`git push origin my-new-feature`) 5 | 5. Create new Pull Request 6 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Precheck 2 | 3 | * Proposals for new features should be submitted via: https://github.com/rrrene/credo-proposals 4 | * For bugs, please do a quick search and make sure the bug has not yet been reported here 5 | 6 | ### Environment 7 | 8 | * Credo version (`mix credo -v`): 9 | * Erlang/Elixir version (`elixir -v`): 10 | * Operating system: 11 | 12 | ### What were you trying to do? 13 | 14 | 15 | 16 | ### Expected outcome 17 | 18 | 19 | 20 | ### Actual outcome 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2020 René Föhring 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing a new version 2 | 3 | ## Checklist 4 | 5 | * Ensure `CHANGELOG.md` is up-to-date 6 | * Ensure working dir is clean 7 | * Update version requirement in `README.md` 8 | * Update version in `mix.exs` 9 | * Create a commit: 10 | 11 | git commit -a -m "Bump version to 0.X.Y" 12 | git tag v0.X.Y 13 | mix test --warnings-as-errors && mix hex.publish 14 | git push origin master --tags 15 | 16 | * Enjoy! 17 | -------------------------------------------------------------------------------- /assets/credo-logo-with-trail-and-writing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/assets/credo-logo-with-trail-and-writing.png -------------------------------------------------------------------------------- /assets/credo-logo-with-trail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/assets/credo-logo-with-trail.png -------------------------------------------------------------------------------- /assets/credo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/assets/credo-logo.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/assets/screenshot.png -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/config/config.exs -------------------------------------------------------------------------------- /guides/commands/explain_command.md: -------------------------------------------------------------------------------- 1 | # mix credo explain 2 | 3 | `explain` allows you to dig deeper into an issue, by showing you details about the issue and the reasoning by it being reported. 4 | As a convenience, you can just copy-paste the `filename:line_number:column` string from the report behind the Credo command to check it out. 5 | 6 | Example usage: 7 | 8 | ```bash 9 | $ mix credo 10 | 11 | [...] 12 | 13 | ┃ [C] ↗ There is no whitespace around parentheses/brackets most of the time, but here there is. 14 | ┃ lib/my_app/server.ex:10:24 #(Credo.Code.InterpolationHelperTest) 15 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 16 | copy this and use it with `mix credo`! 17 | 18 | [...] 19 | 20 | $ mix credo lib/my_app/server.ex:10:24 # show explanation for the issue 21 | ``` 22 | 23 | Please note that you do *not* have to specify the `explain` command explicitly when using an issue location: 24 | 25 | ```bash 26 | $ mix credo lib/my_app/server.ex:10:24 # short-hand without `explain` 27 | $ mix credo explain lib/my_app/server.ex:10:24 # identical to this 28 | ``` 29 | 30 | *Credits:* This is inspired by how you can snap the info from failed tests behind `mix test`. 31 | 32 | ## Command Line Switches 33 | 34 | ### `--format` 35 | 36 | Display the explanation in a specific format (json) 37 | 38 | ```bash 39 | $ mix credo explain lib/my_app/server.ex:10:24 --format json 40 | ``` 41 | -------------------------------------------------------------------------------- /guides/commands/list_command.md: -------------------------------------------------------------------------------- 1 | # mix credo list 2 | 3 | `list` suggests issues, grouping them by file and NOT limitting the list to a certain count. 4 | 5 | ## Examples 6 | 7 | Example usage: 8 | 9 | $ mix credo list # show issues grouped by file 10 | $ mix credo list --format oneline # show issues grouped by file, one issue per line 11 | $ mix credo list --format oneline -a # same thing, include low priority issues 12 | 13 | $ mix credo list --help # more options 14 | 15 | ## Command Line Switches 16 | 17 | The command line switches are identical to [command line switches of the `suggest` command](suggest_command.html#command-line-switches). 18 | -------------------------------------------------------------------------------- /guides/custom_checks/improving_checks.md: -------------------------------------------------------------------------------- 1 | # Improving Custom Checks 2 | 3 | ```elixir 4 | defmodule Credo.Check.Readability.DuplicatedAliases do 5 | use Credo.Check, 6 | base_priority: :low, 7 | category: :readability, 8 | explanations: [ 9 | check: """ 10 | Sometimes during code reviews in large projects with modules that use many 11 | aliases, there can be issues when solving conflicts and some duplicated 12 | may end up not being noticed by reviewers and get merged into the main 13 | branch. 14 | 15 | These duplicated alias can accumulate over many different files over time 16 | and make the aliases section of a file larger and more confusing. 17 | """ 18 | ] 19 | 20 | alias Credo.SourceFile 21 | 22 | def run(source_file, params \\ []) do 23 | issue_meta = IssueMeta.for(source_file, params) 24 | source_ast = SourceFile.ast(source_file) 25 | 26 | {_, {_, _, issues}} = Macro.prewalk(source_ast, {%{}, issue_meta, []}, &traverse(&1, &2)) 27 | issues 28 | end 29 | 30 | defp traverse( 31 | {:alias, _, [{:__aliases__, meta, aliased_module} | _]} = ast, 32 | {cache, issue_meta, issues} 33 | ) do 34 | if Map.has_key?(cache, aliased_module) do 35 | existing_alias_meta = Map.fetch!(cache, aliased_module) 36 | issue = build_issue(Credo.Code.Name.full(aliased_module), meta[:line], existing_alias_meta[:line], issue_meta) 37 | 38 | {ast, {cache, issue_meta, [issue | issues]}} 39 | else 40 | {ast, {Map.put(cache, aliased_module, meta), issue_meta, issues}} 41 | end 42 | end 43 | 44 | defp traverse(ast, acc), do: {ast, acc} 45 | 46 | defp build_issue(trigger, line_no, existing_alias_line_no, issue_meta) do 47 | format_issue( 48 | issue_meta, 49 | message: 50 | "Duplicated alias: #{trigger}, already defined in line #{existing_alias_line_no}", 51 | trigger: trigger, 52 | line_no: line_no 53 | ) 54 | end 55 | end 56 | ``` 57 | -------------------------------------------------------------------------------- /guides/introduction/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | The easiest way to add Credo to your project is by [using Mix](http://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html). 4 | 5 | Add `:credo` as a dependency to your project's `mix.exs`: 6 | 7 | ```elixir 8 | defp deps do 9 | [ 10 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false} 11 | ] 12 | end 13 | ``` 14 | 15 | And run: 16 | 17 | ```bash 18 | $ mix deps.get 19 | ``` 20 | 21 | ## Compatibility 22 | 23 | Credo aims to stay compatible with the list of [Elixir minor releases mentioned in the Elixir docs](https://hexdocs.pm/elixir/compatibility-and-deprecations.html). 24 | 25 | These are the releases that are actively tested on CI. 26 | 27 | Please note that Credo sometimes stays technically compatible with even earlier versions coincidentally. 28 | -------------------------------------------------------------------------------- /guides/introduction/mix_tasks.md: -------------------------------------------------------------------------------- 1 | # Mix Tasks 2 | 3 | After including Credo in a project's dependencies (see [Installation](../introduction/installation.md)), there are a number of built-in mix tasks available: 4 | 5 | ```bash 6 | $ mix help | grep -i credo 7 | mix credo # Run code analysis (use `--help` for options) 8 | mix credo.gen.check # Generate a new custom check for Credo 9 | mix credo.gen.config # Generate a new config for Credo 10 | ``` 11 | 12 | If you want to know more about `mix`, check out [Introduction to Mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html). 13 | 14 | `mix credo` 15 | 16 | Runs Credo's analysis. 17 | 18 | Check out [Configuration](../configuration/cli_switches.md) on how to customize inputs and outputs. 19 | 20 | `mix credo.gen.check` 21 | 22 | Generates a custom Credo check. 23 | 24 | `mix credo.gen.config` 25 | 26 | Generates a Credo config file. 27 | 28 | Check out [Configuration](../configuration/config_file.md) on how to customize it. 29 | -------------------------------------------------------------------------------- /guides/introduction/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Credo is a static code analysis tool for the Elixir language with a focus on teaching and code consistency. 4 | 5 | ![Credo](https://raw.github.com/rrrene/credo/master/assets/screenshot.png) 6 | 7 | `credo` can show you refactoring opportunities in your code, complex code fragments, warn you about common mistakes, show inconsistencies in your naming scheme and - if needed - help you enforce a desired coding style. 8 | 9 | The basic functionality will be familiar to you if you used a linter like JavaScript's [ESLint](https://eslint.org/), Ruby's [RuboCop](https://github.com/rubocop-hq/rubocop) or C#'s [Stylecop](https://github.com/StyleCop/StyleCop) before. 10 | 11 | Contrary to the aforementioned tools, Credo puts a strong emphasis on teaching and code consistency. 12 | 13 | To get started, you might want to check out these guides: 14 | 15 | * [Installation](../introduction/installation.md) 16 | * [Basic usage](../introduction/basic_usage.md) 17 | * [Configuration via `.credo.exs`](../configuration/config_file.md) 18 | 19 | Once you are familiar with the basics, look into these topics: 20 | 21 | * [Adding custom checks](../custom_checks/adding_checks.md) 22 | * [Testing custom checks](../custom_checks/testing_checks.md) 23 | * [Plugins](../plugins/creating_plugins.md) 24 | 25 | Any incorrect or unclear information in these docs should be considered a bug. 26 | In case you find something, you can help maintaining these docs by [filing an issue on GitHub](https://github.com/rrrene/credo/issues) and/or opening a pull request. 27 | -------------------------------------------------------------------------------- /lib/credo.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo do 2 | @moduledoc """ 3 | Credo builds upon four building blocks: 4 | 5 | - `Credo.CLI` - everything related to the command line interface (CLI), which orchestrates the analysis 6 | - `Credo.Execution` - a struct which is handed down the pipeline during analysis 7 | - `Credo.Check` - the default Credo checks 8 | - `Credo.Code` - all analysis tools used by Credo during analysis 9 | """ 10 | 11 | alias Credo.Execution 12 | alias Credo.Execution.Task.WriteDebugReport 13 | 14 | @version Mix.Project.config()[:version] 15 | 16 | @doc """ 17 | Runs Credo with the given `argv` and returns its final `Credo.Execution` struct. 18 | 19 | Example: 20 | 21 | iex> exec = Credo.run(["--only", "Readability"]) 22 | iex> issues = Credo.Execution.get_issues(exec) 23 | iex> Enum.count(issues) > 0 24 | true 25 | 26 | """ 27 | def run(argv_or_exec) do 28 | argv_or_exec 29 | |> Execution.build() 30 | |> Execution.run() 31 | |> WriteDebugReport.call([]) 32 | end 33 | 34 | @doc false 35 | def run(argv_or_exec, files_that_changed) do 36 | argv_or_exec 37 | |> Execution.build(files_that_changed) 38 | |> Execution.run() 39 | |> WriteDebugReport.call([]) 40 | end 41 | 42 | @doc false 43 | def version, do: @version 44 | end 45 | -------------------------------------------------------------------------------- /lib/credo/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | @worker_modules [ 7 | Credo.CLI.Output.Shell, 8 | Credo.Service.SourceFileAST, 9 | Credo.Service.SourceFileLines, 10 | Credo.Service.SourceFileScopes, 11 | Credo.Service.SourceFileScopePriorities, 12 | Credo.Service.SourceFileSource 13 | ] 14 | 15 | if Version.match?(System.version(), ">= 1.10.0-rc") do 16 | def children do 17 | Enum.map(@worker_modules, &{&1, []}) 18 | end 19 | else 20 | def children do 21 | import Supervisor.Spec, warn: false 22 | Enum.map(@worker_modules, &worker(&1, [])) 23 | end 24 | end 25 | 26 | def start(_type, _args) do 27 | opts = [strategy: :one_for_one, name: Credo.Supervisor] 28 | Supervisor.start_link(children(), opts) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/credo/check/config_comment_finder.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.ConfigCommentFinder do 2 | @moduledoc false 3 | 4 | # This check is used internally by Credo. 5 | # 6 | # It traverses the given codebase to find `Credo.Check.ConfigComment` 7 | # compatible comments, which control Credo's behaviour. 8 | 9 | alias Credo.Check.ConfigComment 10 | alias Credo.SourceFile 11 | 12 | @doc false 13 | def run(source_files) when is_list(source_files) do 14 | source_files 15 | |> Enum.map(&find_and_set_in_source_file/1) 16 | |> Enum.reject(&is_nil/1) 17 | end 18 | 19 | def find_and_set_in_source_file(source_file) do 20 | case find_config_comments(source_file) do 21 | [] -> 22 | nil 23 | 24 | config_comments -> 25 | {source_file.filename, config_comments} 26 | end 27 | end 28 | 29 | defp find_config_comments(source_file) do 30 | source = SourceFile.source(source_file) 31 | 32 | if source =~ config_comment_format() do 33 | source 34 | |> Credo.Code.clean_charlists_strings_and_sigils() 35 | |> Credo.Code.to_lines() 36 | |> Enum.reduce([], &find_config_comment/2) 37 | else 38 | [] 39 | end 40 | end 41 | 42 | defp find_config_comment({line_no, string}, memo) do 43 | case Regex.run(config_comment_format(), string) do 44 | nil -> 45 | memo 46 | 47 | [_, instruction, param_string] -> 48 | memo ++ [ConfigComment.new(instruction, param_string, line_no)] 49 | end 50 | end 51 | 52 | # moved to private function due to deprecation of regexes 53 | # in module attributes in Elixir 1.19 54 | defp config_comment_format, do: ~r/#\s*credo\:([\w\-\:]+)\s*(.*)/im 55 | end 56 | -------------------------------------------------------------------------------- /lib/credo/check/consistency/line_endings.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.LineEndings do 2 | use Credo.Check, 3 | id: "EX1002", 4 | run_on_all: true, 5 | base_priority: :high, 6 | tags: [:formatter], 7 | param_defaults: [ 8 | force: nil 9 | ], 10 | explanations: [ 11 | check: """ 12 | Windows and Linux/macOS systems use different line-endings in files. 13 | 14 | It seems like a good idea not to mix these in the same codebase. 15 | 16 | While this is not necessarily a concern for the correctness of your code, 17 | you should use a consistent style throughout your codebase. 18 | """, 19 | params: [ 20 | force: "Force a choice, values can be `:unix` or `:windows`." 21 | ] 22 | ] 23 | 24 | @collector Credo.Check.Consistency.LineEndings.Collector 25 | 26 | @doc false 27 | @impl true 28 | def run_on_all_source_files(exec, source_files, params) do 29 | @collector.find_and_append_issues(source_files, exec, params, &issues_for/3) 30 | end 31 | 32 | defp issues_for(expected, source_file, params) do 33 | first_line_with_issue = @collector.first_line_with_issue(expected, source_file) 34 | 35 | message = 36 | case expected do 37 | :unix -> 38 | "File is using windows line endings while most of the files use unix line endings." 39 | 40 | :windows -> 41 | "File is using unix line endings while most of the files use windows line endings." 42 | end 43 | 44 | trigger = 45 | case expected do 46 | :unix -> "\r\n" 47 | :windows -> "\n" 48 | end 49 | 50 | source_file 51 | |> IssueMeta.for(params) 52 | |> format_issue(message: message, line_no: first_line_with_issue, trigger: trigger) 53 | |> List.wrap() 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/credo/check/consistency/line_endings/collector.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.LineEndings.Collector do 2 | @moduledoc false 3 | 4 | use Credo.Check.Consistency.Collector 5 | 6 | def collect_matches(source_file, _params) do 7 | source_file 8 | |> SourceFile.lines() 9 | # remove the last line since it behaves differently on windows and linux 10 | # and apparently does not help determining line endings (see #965) 11 | |> List.delete_at(-1) 12 | |> Enum.reduce(%{}, fn line, stats -> 13 | Map.update(stats, line_ending(line), 1, &(&1 + 1)) 14 | end) 15 | end 16 | 17 | def first_line_with_issue(expected, source_file) do 18 | {line_no, _} = 19 | source_file 20 | |> SourceFile.lines() 21 | |> Enum.find(&(line_ending(&1) != expected)) 22 | 23 | line_no 24 | end 25 | 26 | defp line_ending({_line_no, line}) do 27 | if String.ends_with?(line, "\r") do 28 | :windows 29 | else 30 | :unix 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/credo/check/consistency/multi_alias_import_require_use.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.MultiAliasImportRequireUse do 2 | use Credo.Check, 3 | id: "EX1003", 4 | run_on_all: true, 5 | base_priority: :high, 6 | tags: [:controversial], 7 | explanations: [ 8 | check: """ 9 | When using alias, import, require or use for multiple names from the same 10 | namespace, you have two options: 11 | 12 | Use single instructions per name: 13 | 14 | alias Ecto.Query 15 | alias Ecto.Schema 16 | alias Ecto.Multi 17 | 18 | or use one multi instruction per namespace: 19 | 20 | alias Ecto.{Query, Schema, Multi} 21 | 22 | While this is not necessarily a concern for the correctness of your code, 23 | you should use a consistent style throughout your codebase. 24 | """ 25 | ] 26 | 27 | @collector Credo.Check.Consistency.MultiAliasImportRequireUse.Collector 28 | 29 | @doc false 30 | @impl true 31 | def run_on_all_source_files(exec, source_files, params) do 32 | @collector.find_and_append_issues(source_files, exec, params, &issues_for/3) 33 | end 34 | 35 | defp issues_for(expected, source_file, params) do 36 | issue_meta = IssueMeta.for(source_file, params) 37 | 38 | issue_locations = @collector.find_locations_not_matching(expected, source_file) 39 | 40 | Enum.map(issue_locations, fn line_no -> 41 | format_issue(issue_meta, message: message_for(expected), line_no: line_no, trigger: "") 42 | end) 43 | end 44 | 45 | defp message_for(:multi = _expected) do 46 | "Most of the time you are using the multi-alias/require/import/use syntax, but here you are using multiple single directives" 47 | end 48 | 49 | defp message_for(:single = _expected) do 50 | "Most of the time you are using the multiple single line alias/require/import/use directives but here you are using the multi-alias/require/import/use syntax" 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/credo/check/consistency/tabs_or_spaces/collector.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.TabsOrSpaces.Collector do 2 | @moduledoc false 3 | 4 | use Credo.Check.Consistency.Collector 5 | 6 | def collect_matches(source_file, _params) do 7 | source_file 8 | |> SourceFile.lines() 9 | |> Enum.reduce(%{}, fn line, stats -> 10 | match = indentation(line) 11 | 12 | if match do 13 | Map.update(stats, match, 1, &(&1 + 1)) 14 | else 15 | stats 16 | end 17 | end) 18 | end 19 | 20 | def find_locations_not_matching(expected, source_file) do 21 | source_file 22 | |> SourceFile.lines() 23 | |> List.foldr([], fn {line_no, _} = line, line_nos -> 24 | if indentation(line) && indentation(line) != expected do 25 | [line_no | line_nos] 26 | else 27 | line_nos 28 | end 29 | end) 30 | end 31 | 32 | defp indentation({_line_no, " " <> _line}), do: :spaces 33 | defp indentation({_line_no, "\t" <> _line}), do: :tabs 34 | defp indentation({_, _}), do: nil 35 | end 36 | -------------------------------------------------------------------------------- /lib/credo/check/design/tag_fixme.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Design.TagFIXME do 2 | use Credo.Check, 3 | id: "EX2004", 4 | base_priority: :high, 5 | param_defaults: [include_doc: true], 6 | explanations: [ 7 | check: """ 8 | FIXME comments are used to indicate places where source code needs fixing. 9 | 10 | Example: 11 | 12 | # FIXME: this does no longer work, research new API url 13 | defp fun do 14 | # ... 15 | end 16 | 17 | The premise here is that FIXME should indeed be fixed as soon as possible and 18 | are therefore reported by Credo. 19 | 20 | Like all `Software Design` issues, this is just advice and might not be 21 | applicable to your project/situation. 22 | """, 23 | params: [ 24 | include_doc: "Set to `true` to also include tags from @doc attributes." 25 | ] 26 | ] 27 | 28 | @tag_name "FIXME" 29 | 30 | alias Credo.Check.Design.TagHelper 31 | 32 | @doc false 33 | @impl true 34 | def run(%SourceFile{} = source_file, params) do 35 | issue_meta = IssueMeta.for(source_file, params) 36 | include_doc? = Params.get(params, :include_doc, __MODULE__) 37 | 38 | source_file 39 | |> TagHelper.tags(@tag_name, include_doc?) 40 | |> Enum.map(&issue_for(issue_meta, &1)) 41 | end 42 | 43 | defp issue_for(issue_meta, {line_no, _line, trigger}) do 44 | format_issue( 45 | issue_meta, 46 | message: "Found a #{@tag_name} tag in a comment: #{trigger}", 47 | line_no: line_no, 48 | trigger: trigger 49 | ) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/credo/check/design/tag_todo.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Design.TagTODO do 2 | use Credo.Check, 3 | id: "EX2005", 4 | param_defaults: [include_doc: true], 5 | explanations: [ 6 | check: """ 7 | TODO comments are used to remind yourself of source code related things. 8 | 9 | Example: 10 | 11 | # TODO: move this to a Helper module 12 | defp fun do 13 | # ... 14 | end 15 | 16 | The premise here is that TODO should be dealt with in the near future and 17 | are therefore reported by Credo. 18 | 19 | Like all `Software Design` issues, this is just advice and might not be 20 | applicable to your project/situation. 21 | """, 22 | params: [ 23 | include_doc: "Set to `true` to also include tags from @doc attributes." 24 | ] 25 | ] 26 | 27 | alias Credo.Check.Design.TagHelper 28 | 29 | @tag_name "TODO" 30 | 31 | @doc false 32 | @impl true 33 | def run(%SourceFile{} = source_file, params) do 34 | issue_meta = IssueMeta.for(source_file, params) 35 | include_doc? = Params.get(params, :include_doc, __MODULE__) 36 | 37 | source_file 38 | |> TagHelper.tags(@tag_name, include_doc?) 39 | |> Enum.map(&issue_for(issue_meta, &1)) 40 | end 41 | 42 | defp issue_for(issue_meta, {line_no, _line, trigger}) do 43 | format_issue( 44 | issue_meta, 45 | message: "Found a #{@tag_name} tag in a comment: #{trigger}", 46 | line_no: line_no, 47 | trigger: trigger 48 | ) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/credo/check/readability/impl_true.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.ImplTrue do 2 | use Credo.Check, 3 | id: "EX3004", 4 | base_priority: :normal, 5 | explanations: [ 6 | check: """ 7 | `@impl true` is a shortform so you don't have to write the actual behaviour that is being implemented. 8 | This can make code harder to comprehend. 9 | 10 | # preferred 11 | 12 | @impl MyBehaviour 13 | def my_funcion() do 14 | # ... 15 | end 16 | 17 | # NOT preferred 18 | 19 | @impl true 20 | def my_funcion() do 21 | # ... 22 | end 23 | 24 | When implementing behaviour callbacks, `@impl true` indicates that a function implements a callback, but 25 | a more explicit way is to use the actual behaviour being implemented, for example `@impl MyBehaviour`. 26 | 27 | This not only improves readability, but adds extra validation in cases where multiple behaviours are 28 | implemented in a single module. 29 | 30 | Like all `Readability` issues, this one is not a technical concern. 31 | But you can improve the odds of others reading and liking your code by making 32 | it easier to follow. 33 | """ 34 | ] 35 | 36 | @doc false 37 | @impl true 38 | def run(%SourceFile{} = source_file, params) do 39 | issue_meta = IssueMeta.for(source_file, params) 40 | 41 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 42 | end 43 | 44 | defp traverse({:@, meta, [{:impl, _, [true]}]}, issues, issue_meta) do 45 | {nil, issues ++ [issue_for(issue_meta, meta[:line])]} 46 | end 47 | 48 | defp traverse(ast, issues, _issue_meta) do 49 | {ast, issues} 50 | end 51 | 52 | defp issue_for(issue_meta, line_no) do 53 | format_issue( 54 | issue_meta, 55 | message: "`@impl true` should be `@impl MyBehaviour`.", 56 | trigger: "@impl", 57 | line_no: line_no 58 | ) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/credo/check/readability/multi_alias.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.MultiAlias do 2 | use Credo.Check, 3 | id: "EX3011", 4 | base_priority: :low, 5 | tags: [:controversial], 6 | explanations: [ 7 | check: """ 8 | Multi alias expansion makes module uses harder to search for in large code bases. 9 | 10 | # preferred 11 | 12 | alias Module.Foo 13 | alias Module.Bar 14 | 15 | # NOT preferred 16 | 17 | alias Module.{Foo, Bar} 18 | 19 | Like all `Readability` issues, this one is not a technical concern. 20 | But you can improve the odds of others reading and liking your code by making 21 | it easier to follow. 22 | """ 23 | ] 24 | 25 | @doc false 26 | @impl true 27 | def run(%SourceFile{} = source_file, params) do 28 | issue_meta = IssueMeta.for(source_file, params) 29 | 30 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 31 | end 32 | 33 | defp traverse( 34 | {:alias, _, [{{_, _, [{alias, opts, _base_alias}, :{}]}, _, [multi_alias | _]}]} = ast, 35 | issues, 36 | issue_meta 37 | ) 38 | when alias in [:__aliases__, :__MODULE__] do 39 | {:__aliases__, _, module} = multi_alias 40 | module = Enum.join(module, ".") 41 | 42 | new_issue = issue_for(issue_meta, Keyword.get(opts, :line), module) 43 | 44 | {ast, [new_issue | issues]} 45 | end 46 | 47 | defp traverse(ast, issues, _issue_meta), do: {ast, issues} 48 | 49 | defp issue_for(issue_meta, line_no, trigger) do 50 | format_issue( 51 | issue_meta, 52 | message: 53 | "Avoid grouping aliases in '{ ... }'; please specify one fully-qualified alias per line.", 54 | trigger: trigger, 55 | line_no: line_no 56 | ) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/credo/check/readability/one_arity_function_in_pipe.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.OneArityFunctionInPipe do 2 | use Credo.Check, 3 | id: "EX3034", 4 | base_priority: :low, 5 | explanations: [ 6 | check: """ 7 | Use parentheses for one-arity functions when using the pipe operator (|>). 8 | 9 | # not preferred 10 | some_string |> String.downcase |> String.trim 11 | 12 | # preferred 13 | some_string |> String.downcase() |> String.trim() 14 | 15 | Like all `Readability` issues, this one is not a technical concern. 16 | But you can improve the odds of others reading and liking your code by making 17 | it easier to follow. 18 | """ 19 | ] 20 | 21 | @doc false 22 | @impl true 23 | def run(%SourceFile{} = source_file, params) do 24 | Credo.Code.prewalk(source_file, &traverse(&1, &2, IssueMeta.for(source_file, params))) 25 | end 26 | 27 | defp traverse({:|>, _, [_, {name, meta, nil}]} = ast, issues, issue_meta) when is_atom(name) do 28 | {ast, [issue_for(issue_meta, meta[:line], name) | issues]} 29 | end 30 | 31 | defp traverse(ast, issues, _) do 32 | {ast, issues} 33 | end 34 | 35 | defp issue_for(issue_meta, line, name) do 36 | format_issue( 37 | issue_meta, 38 | message: "One arity functions should have parentheses in pipes.", 39 | line_no: line, 40 | trigger: name 41 | ) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/credo/check/readability/one_pipe_per_line.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.OnePipePerLine do 2 | use Credo.Check, 3 | id: "EX3035", 4 | category: :readability, 5 | explanations: [ 6 | check: """ 7 | Don't use multiple pipes (|>) in the same line. 8 | Each function in the pipe should be in it's own line. 9 | 10 | # preferred 11 | 12 | foo 13 | |> bar() 14 | |> baz() 15 | 16 | # NOT preferred 17 | 18 | foo |> bar() |> baz() 19 | 20 | The code in this example ... 21 | 22 | 1 |> Integer.to_string() |> String.to_integer() 23 | 24 | ... should be refactored to look like this: 25 | 26 | 1 27 | |> Integer.to_string() 28 | |> String.to_integer() 29 | 30 | Like all `Readability` issues, this one is not a technical concern. 31 | But you can improve the odds of others reading and liking your code by making 32 | it easier to follow. 33 | """ 34 | ] 35 | 36 | @doc false 37 | @impl true 38 | def run(%SourceFile{} = source_file, params \\ []) do 39 | issue_meta = IssueMeta.for(source_file, params) 40 | 41 | source_file 42 | |> Credo.Code.prewalk(&traverse/2) 43 | |> Enum.uniq() 44 | |> Enum.map(&issue_for(issue_meta, &1)) 45 | end 46 | 47 | defp traverse({:|>, meta, [{:|>, meta2, _} | _]} = ast, acc) do 48 | if meta[:line] == meta2[:line] do 49 | {ast, [meta[:line] | acc]} 50 | else 51 | {ast, acc} 52 | end 53 | end 54 | 55 | defp traverse(ast, acc), do: {ast, acc} 56 | 57 | defp issue_for(issue_meta, line_no) do 58 | format_issue( 59 | issue_meta, 60 | message: "Avoid using multiple pipes (`|>`) on the same line.", 61 | line_no: line_no, 62 | trigger: "|>" 63 | ) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/credo/check/readability/pipe_into_anonymous_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.PipeIntoAnonymousFunctions do 2 | use Credo.Check, 3 | id: "EX3015", 4 | base_priority: :low, 5 | explanations: [ 6 | check: """ 7 | Avoid piping into anonymous functions. 8 | 9 | The code in this example ... 10 | 11 | def my_fun(foo) do 12 | foo 13 | |> (fn i -> i * 2 end).() 14 | |> my_other_fun() 15 | end 16 | 17 | ... should be refactored to define a private function: 18 | 19 | def my_fun(foo) do 20 | foo 21 | |> times_2() 22 | |> my_other_fun() 23 | end 24 | 25 | defp timex_2(i), do: i * 2 26 | 27 | ... or use `then/1`: 28 | 29 | def my_fun(foo) do 30 | foo 31 | |> then(fn i -> i * 2 end) 32 | |> my_other_fun() 33 | end 34 | 35 | Like all `Readability` issues, this one is not a technical concern. 36 | But you can improve the odds of others reading and liking your code by making 37 | it easier to follow. 38 | """ 39 | ] 40 | 41 | @impl true 42 | def run(source_file, params \\ []) do 43 | issue_meta = IssueMeta.for(source_file, params) 44 | 45 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 46 | end 47 | 48 | defp traverse( 49 | {:|>, meta, [_, {{:., _, [{:fn, _, _} | _]}, _, _}]} = ast, 50 | issues, 51 | issue_meta 52 | ) do 53 | {ast, [issue_for(issue_meta, meta[:line]) | issues]} 54 | end 55 | 56 | defp traverse(ast, issues, _) do 57 | {ast, issues} 58 | end 59 | 60 | defp issue_for(issue_meta, line_no) do 61 | format_issue( 62 | issue_meta, 63 | message: "Avoid piping into anonymous function calls.", 64 | trigger: "|>", 65 | line_no: line_no 66 | ) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/credo/check/readability/semicolons.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.Semicolons do 2 | use Credo.Check, 3 | id: "EX3020", 4 | base_priority: :high, 5 | tags: [:formatter], 6 | explanations: [ 7 | check: """ 8 | Don't use ; to separate statements and expressions. 9 | Statements and expressions should be separated by lines. 10 | 11 | # preferred 12 | 13 | a = 1 14 | b = 2 15 | 16 | # NOT preferred 17 | 18 | a = 1; b = 2 19 | 20 | Like all `Readability` issues, this one is not a technical concern. 21 | But you can improve the odds of others reading and liking your code by making 22 | it easier to follow. 23 | """ 24 | ] 25 | 26 | @doc false 27 | @impl true 28 | def run(%SourceFile{} = source_file, params) do 29 | issue_meta = IssueMeta.for(source_file, params) 30 | 31 | source_file 32 | |> Credo.Code.to_tokens() 33 | |> collect_issues([], issue_meta) 34 | end 35 | 36 | defp collect_issues([], acc, _issue_meta), do: acc 37 | 38 | defp collect_issues([{:";", {line_no, column1, _}} | rest], acc, issue_meta) do 39 | acc = [issue_for(issue_meta, line_no, column1) | acc] 40 | collect_issues(rest, acc, issue_meta) 41 | end 42 | 43 | defp collect_issues([_ | rest], acc, issue_meta), do: collect_issues(rest, acc, issue_meta) 44 | 45 | defp issue_for(issue_meta, line_no, column) do 46 | format_issue( 47 | issue_meta, 48 | message: "Don't use `;` to separate statements and expressions.", 49 | line_no: line_no, 50 | column: column, 51 | trigger: ";" 52 | ) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/credo/check/readability/trailing_blank_line.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.TrailingBlankLine do 2 | use Credo.Check, 3 | id: "EX3028", 4 | base_priority: :low, 5 | tags: [:formatter], 6 | explanations: [ 7 | check: """ 8 | Files should end in a trailing blank line. 9 | 10 | This is mostly for historical reasons: every text file should end with a \\n, 11 | or newline since this acts as `eol` or the end of the line character. 12 | 13 | See also: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_206 14 | 15 | Most text editors ensure this "final newline" automatically. 16 | 17 | Like all `Readability` issues, this one is not a technical concern. 18 | But you can improve the odds of others reading and liking your code by making 19 | it easier to follow. 20 | """ 21 | ] 22 | 23 | @doc false 24 | @impl true 25 | def run(%SourceFile{} = source_file, params) do 26 | issue_meta = IssueMeta.for(source_file, params) 27 | 28 | {line_no, last_line} = 29 | source_file 30 | |> SourceFile.lines() 31 | |> List.last() 32 | 33 | if String.trim(last_line) == "" do 34 | [] 35 | else 36 | [issue_for(issue_meta, line_no)] 37 | end 38 | end 39 | 40 | defp issue_for(issue_meta, line_no) do 41 | format_issue( 42 | issue_meta, 43 | message: "There should be a final \\n at the end of each file.", 44 | line_no: line_no, 45 | trigger: Issue.no_trigger() 46 | ) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/credo/check/readability/unnecessary_alias_expansion.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.UnnecessaryAliasExpansion do 2 | use Credo.Check, 3 | id: "EX3030", 4 | base_priority: :low, 5 | explanations: [ 6 | check: """ 7 | Alias expansion is useful but when aliasing a single module, 8 | it can be harder to read with unnecessary braces. 9 | 10 | # preferred 11 | 12 | alias ModuleA.Foo 13 | alias ModuleA.{Foo, Bar} 14 | 15 | # NOT preferred 16 | 17 | alias ModuleA.{Foo} 18 | 19 | Like all `Readability` issues, this one is not a technical concern. 20 | But you can improve the odds of others reading and liking your code by making 21 | it easier to follow. 22 | """ 23 | ] 24 | 25 | @doc false 26 | @impl true 27 | def run(%SourceFile{} = source_file, params) do 28 | issue_meta = IssueMeta.for(source_file, params) 29 | 30 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 31 | end 32 | 33 | defp traverse( 34 | {:alias, _, [{{:., _, [_, :{}]}, _, [{:__aliases__, opts, [child]}]}]} = ast, 35 | issues, 36 | issue_meta 37 | ) do 38 | {ast, issues ++ [issue_for(issue_meta, Keyword.get(opts, :line), child)]} 39 | end 40 | 41 | defp traverse(ast, issues, _issue_meta), do: {ast, issues} 42 | 43 | defp issue_for(issue_meta, line_no, trigger) do 44 | format_issue( 45 | issue_meta, 46 | message: "Unnecessary alias expansion for #{trigger}, consider removing braces.", 47 | trigger: trigger, 48 | line_no: line_no 49 | ) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/append_single_item.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.AppendSingleItem do 2 | use Credo.Check, 3 | id: "EX4002", 4 | base_priority: :low, 5 | tags: [:controversial], 6 | explanations: [ 7 | check: """ 8 | When building up large lists, it is faster to prepend than 9 | append. Therefore: It is sometimes best to prepend to the list 10 | during iteration and call Enum.reverse/1 at the end, as it is quite 11 | fast. 12 | 13 | Example: 14 | 15 | list = list_so_far ++ [new_item] 16 | 17 | # refactoring it like this can make the code faster: 18 | 19 | list = [new_item] ++ list_so_far 20 | # ... 21 | Enum.reverse(list) 22 | 23 | """ 24 | ] 25 | 26 | @doc false 27 | @impl true 28 | def run(%SourceFile{} = source_file, params) do 29 | issue_meta = IssueMeta.for(source_file, params) 30 | 31 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 32 | end 33 | 34 | # [a] ++ b is OK 35 | defp traverse({:++, _, [[_], _]} = ast, issues, _issue_meta) do 36 | {ast, issues} 37 | end 38 | 39 | # a ++ [b] is not 40 | defp traverse({:++, meta, [_, [_]]} = ast, issues, issue_meta) do 41 | {ast, [issue_for(issue_meta, meta[:line], :++) | issues]} 42 | end 43 | 44 | defp traverse(ast, issues, _issue_meta) do 45 | {ast, issues} 46 | end 47 | 48 | defp issue_for(issue_meta, line_no, trigger) do 49 | format_issue( 50 | issue_meta, 51 | message: 52 | "Appending a single item to a list is inefficient, use `[head | tail]` notation (and `Enum.reverse/1` when order matters).", 53 | trigger: trigger, 54 | line_no: line_no 55 | ) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/case_trivial_matches.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.CaseTrivialMatches do 2 | use Credo.Check, 3 | id: "EX4004", 4 | explanations: [ 5 | check: """ 6 | PLEASE NOTE: This check is deprecated as it might do more harm than good. 7 | 8 | Related discussion: https://github.com/rrrene/credo/issues/65 9 | """ 10 | ] 11 | 12 | @doc false 13 | @impl true 14 | def run(%SourceFile{} = source_file, params) do 15 | issue_meta = IssueMeta.for(source_file, params) 16 | 17 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 18 | end 19 | 20 | defp traverse({:case, meta, arguments} = ast, issues, issue_meta) do 21 | cases = 22 | arguments 23 | |> Credo.Code.Block.do_block_for!() 24 | |> List.wrap() 25 | |> Enum.map(&case_statement_for/1) 26 | |> Enum.sort() 27 | 28 | if cases == [false, true] do 29 | {ast, issues ++ [issue_for(issue_meta, meta[:line], :cond)]} 30 | else 31 | {ast, issues} 32 | end 33 | end 34 | 35 | defp traverse(ast, issues, _issue_meta) do 36 | {ast, issues} 37 | end 38 | 39 | defp case_statement_for({:->, _, [[true], _]}), do: true 40 | defp case_statement_for({:->, _, [[false], _]}), do: false 41 | defp case_statement_for(_), do: nil 42 | 43 | defp issue_for(issue_meta, line_no, trigger) do 44 | format_issue( 45 | issue_meta, 46 | message: "Case statements should not only contain `true` and `false`.", 47 | trigger: trigger, 48 | line_no: line_no 49 | ) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/filter_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.FilterFilter do 2 | use Credo.Check, 3 | id: "EX4008", 4 | explanations: [ 5 | check: """ 6 | One `Enum.filter/2` is more efficient than `Enum.filter/2 |> Enum.filter/2`. 7 | 8 | This should be refactored: 9 | 10 | ["a", "b", "c"] 11 | |> Enum.filter(&String.contains?(&1, "x")) 12 | |> Enum.filter(&String.contains?(&1, "a")) 13 | 14 | to look like this: 15 | 16 | Enum.filter(["a", "b", "c"], fn letter -> 17 | String.contains?(letter, "x") && String.contains?(letter, "a") 18 | end) 19 | 20 | The reason for this is performance, because the two separate calls 21 | to `Enum.filter/2` require two iterations whereas doing the 22 | functions in the single `Enum.filter/2` only requires one. 23 | """ 24 | ] 25 | 26 | alias Credo.Check.Refactor.EnumHelpers 27 | 28 | @doc false 29 | def run(source_file, params \\ []) do 30 | issue_meta = IssueMeta.for(source_file, params) 31 | 32 | message = "One `Enum.filter/2` is more efficient than `Enum.filter/2 |> Enum.filter/2`" 33 | trigger = "|>" 34 | 35 | Credo.Code.prewalk( 36 | source_file, 37 | &EnumHelpers.traverse(&1, &2, issue_meta, message, trigger, :filter, :filter, __MODULE__) 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/filter_reject.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.FilterReject do 2 | use Credo.Check, 3 | id: "EX4009", 4 | tags: [:controversial], 5 | explanations: [ 6 | check: """ 7 | One `Enum.filter/2` is more efficient than `Enum.filter/2 |> Enum.reject/2`. 8 | 9 | This should be refactored: 10 | 11 | ["a", "b", "c"] 12 | |> Enum.filter(&String.contains?(&1, "x")) 13 | |> Enum.reject(&String.contains?(&1, "a")) 14 | 15 | to look like this: 16 | 17 | Enum.filter(["a", "b", "c"], fn letter -> 18 | String.contains?(letter, "x") && !String.contains?(letter, "a") 19 | end) 20 | 21 | The reason for this is performance, because the two calls to 22 | `Enum.reject/2` and `Enum.filter/2` require two iterations whereas 23 | doing the functions in the single `Enum.filter/2` only requires one. 24 | """ 25 | ] 26 | 27 | alias Credo.Check.Refactor.EnumHelpers 28 | 29 | @doc false 30 | def run(source_file, params \\ []) do 31 | issue_meta = IssueMeta.for(source_file, params) 32 | 33 | message = "One `Enum.filter/2` is more efficient than `Enum.filter/2 |> Enum.reject/2`" 34 | trigger = "|>" 35 | 36 | Credo.Code.prewalk( 37 | source_file, 38 | &EnumHelpers.traverse(&1, &2, issue_meta, message, trigger, :filter, :reject, __MODULE__) 39 | ) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/io_puts.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.IoPuts do 2 | use Credo.Check, 3 | id: "EX4011", 4 | tags: [:controversial], 5 | explanations: [ 6 | check: """ 7 | Prefer using Logger statements over using `IO.puts/1`. 8 | 9 | This is a situational check. 10 | 11 | As such, it might be a great help for e.g. Phoenix projects, but 12 | a clear mismatch for CLI projects. 13 | """ 14 | ] 15 | 16 | @call_string "IO.puts" 17 | 18 | @doc false 19 | @impl true 20 | def run(%SourceFile{} = source_file, params) do 21 | issue_meta = IssueMeta.for(source_file, params) 22 | 23 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 24 | end 25 | 26 | defp traverse( 27 | {{:., _, [{:__aliases__, meta, [:IO]}, :puts]}, _, _arguments} = ast, 28 | issues, 29 | issue_meta 30 | ) do 31 | {ast, issues_for_call(meta, issues, issue_meta)} 32 | end 33 | 34 | defp traverse(ast, issues, _issue_meta) do 35 | {ast, issues} 36 | end 37 | 38 | defp issues_for_call(meta, issues, issue_meta) do 39 | [issue_for(issue_meta, meta, @call_string) | issues] 40 | end 41 | 42 | defp issue_for(issue_meta, meta, trigger) do 43 | format_issue( 44 | issue_meta, 45 | message: "There should be no calls to `IO.puts/1`.", 46 | trigger: trigger, 47 | line_no: meta[:line], 48 | column: meta[:column] 49 | ) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/map_map.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.MapMap do 2 | use Credo.Check, 3 | id: "EX4015", 4 | explanations: [ 5 | check: """ 6 | One `Enum.map/2` is more efficient than `Enum.map/2 |> Enum.map/2`. 7 | 8 | This should be refactored: 9 | 10 | [:a, :b, :c] 11 | |> Enum.map(&inspect/1) 12 | |> Enum.map(&String.upcase/1) 13 | 14 | to look like this: 15 | 16 | Enum.map([:a, :b, :c], fn letter -> 17 | letter 18 | |> inspect() 19 | |> String.upcase() 20 | end) 21 | 22 | The reason for this is performance, because the two separate calls 23 | to `Enum.map/2` require two iterations whereas doing the functions 24 | in the single `Enum.map/2` only requires one. 25 | """ 26 | ] 27 | 28 | alias Credo.Check.Refactor.EnumHelpers 29 | 30 | @doc false 31 | def run(source_file, params \\ []) do 32 | issue_meta = IssueMeta.for(source_file, params) 33 | 34 | message = "One `Enum.map/2` is more efficient than `Enum.map/2 |> Enum.map/2`" 35 | trigger = "|>" 36 | 37 | Credo.Code.prewalk( 38 | source_file, 39 | &EnumHelpers.traverse(&1, &2, issue_meta, message, trigger, :map, :map, __MODULE__) 40 | ) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/negated_conditions_in_unless.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.NegatedConditionsInUnless do 2 | use Credo.Check, 3 | id: "EX4018", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | Unless blocks should avoid having a negated condition. 8 | 9 | The code in this example ... 10 | 11 | unless !allowed? do 12 | proceed_as_planned() 13 | end 14 | 15 | ... should be refactored to look like this: 16 | 17 | if allowed? do 18 | proceed_as_planned() 19 | end 20 | 21 | The reason for this is not a technical but a human one. It is pretty difficult 22 | to wrap your head around a block of code that is executed if a negated 23 | condition is NOT met. See what I mean? 24 | """ 25 | ] 26 | 27 | @doc false 28 | @impl true 29 | def run(%SourceFile{} = source_file, params) do 30 | issue_meta = IssueMeta.for(source_file, params) 31 | 32 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 33 | end 34 | 35 | defp traverse({:@, _, [{:unless, _, _}]}, issues, _issue_meta) do 36 | {nil, issues} 37 | end 38 | 39 | defp traverse( 40 | {:unless, _meta, [{negator, meta, _arguments} | _]} = ast, 41 | issues, 42 | issue_meta 43 | ) 44 | when negator in [:!, :not] do 45 | issue = issue_for(issue_meta, meta[:line], negator) 46 | 47 | {ast, issues ++ List.wrap(issue)} 48 | end 49 | 50 | defp traverse(ast, issues, _issue_meta) do 51 | {ast, issues} 52 | end 53 | 54 | defp issue_for(issue_meta, line_no, trigger) do 55 | format_issue( 56 | issue_meta, 57 | message: "Avoid negated conditions in unless blocks.", 58 | trigger: trigger, 59 | line_no: line_no 60 | ) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/reject_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.RejectFilter do 2 | use Credo.Check, 3 | id: "EX4025", 4 | tags: [:controversial], 5 | explanations: [ 6 | check: """ 7 | One `Enum.filter/2` is more efficient than `Enum.reject/2 |> Enum.filter/2`. 8 | 9 | This should be refactored: 10 | 11 | ["a", "b", "c"] 12 | |> Enum.reject(&String.contains?(&1, "x")) 13 | |> Enum.filter(&String.contains?(&1, "a")) 14 | 15 | to look like this: 16 | 17 | Enum.filter(["a", "b", "c"], fn letter -> 18 | !String.contains?(letter, "x") && String.contains?(letter, "a") 19 | end) 20 | 21 | The reason for this is performance, because the two calls to 22 | `Enum.reject/2` and `Enum.filter/2` require two iterations whereas 23 | doing the functions in the single `Enum.filter/2` only requires one. 24 | """ 25 | ] 26 | 27 | alias Credo.Check.Refactor.EnumHelpers 28 | 29 | @doc false 30 | def run(source_file, params \\ []) do 31 | issue_meta = IssueMeta.for(source_file, params) 32 | 33 | message = "One `Enum.filter/2` is more efficient than `Enum.reject/2 |> Enum.filter/2`" 34 | trigger = "|>" 35 | 36 | Credo.Code.prewalk( 37 | source_file, 38 | &EnumHelpers.traverse(&1, &2, issue_meta, message, trigger, :reject, :filter, __MODULE__) 39 | ) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/credo/check/refactor/reject_reject.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.RejectReject do 2 | use Credo.Check, 3 | id: "EX4026", 4 | explanations: [ 5 | check: """ 6 | One `Enum.reject/2` is more efficient than `Enum.reject/2 |> Enum.reject/2`. 7 | 8 | This should be refactored: 9 | 10 | ["a", "b", "c"] 11 | |> Enum.reject(&String.contains?(&1, "x")) 12 | |> Enum.reject(&String.contains?(&1, "a")) 13 | 14 | to look like this: 15 | 16 | Enum.reject(["a", "b", "c"], fn letter -> 17 | String.contains?(letter, "x") || String.contains?(letter, "a") 18 | end) 19 | 20 | The reason for this is performance, because the two separate calls 21 | to `Enum.reject/2` require two iterations whereas doing the 22 | functions in the single `Enum.reject/2` only requires one. 23 | """ 24 | ] 25 | 26 | alias Credo.Check.Refactor.EnumHelpers 27 | 28 | @doc false 29 | def run(source_file, params \\ []) do 30 | issue_meta = IssueMeta.for(source_file, params) 31 | 32 | message = "One `Enum.reject/2` is more efficient than `Enum.reject/2 |> Enum.reject/2`" 33 | trigger = "|>" 34 | 35 | Credo.Code.prewalk( 36 | source_file, 37 | &EnumHelpers.traverse(&1, &2, issue_meta, message, trigger, :reject, :reject, __MODULE__) 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/credo/check/warning/iex_pry.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.IExPry do 2 | use Credo.Check, 3 | id: "EX5005", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | While calls to IEx.pry might appear in some parts of production code, 8 | most calls to this function are added during debugging sessions. 9 | 10 | This check warns about those calls, because they might have been committed 11 | in error. 12 | """ 13 | ] 14 | 15 | @call_string "IEx.pry" 16 | 17 | @doc false 18 | @impl true 19 | def run(%SourceFile{} = source_file, params) do 20 | issue_meta = IssueMeta.for(source_file, params) 21 | 22 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 23 | end 24 | 25 | defp traverse( 26 | { 27 | {:., _, [{:__aliases__, meta, [:IEx]}, :pry]}, 28 | _, 29 | _arguments 30 | } = ast, 31 | issues, 32 | issue_meta 33 | ) do 34 | {ast, issues_for_call(meta, issues, issue_meta)} 35 | end 36 | 37 | defp traverse(ast, issues, _issue_meta) do 38 | {ast, issues} 39 | end 40 | 41 | defp issues_for_call(meta, issues, issue_meta) do 42 | new_issue = 43 | format_issue( 44 | issue_meta, 45 | message: "There should be no calls to `IEx.pry/0`.", 46 | trigger: @call_string, 47 | line_no: meta[:line], 48 | column: meta[:column] 49 | ) 50 | 51 | [new_issue | issues] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/credo/check/warning/io_inspect.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.IoInspect do 2 | use Credo.Check, 3 | id: "EX5006", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | While calls to IO.inspect might appear in some parts of production code, 8 | most calls to this function are added during debugging sessions. 9 | 10 | This check warns about those calls, because they might have been committed 11 | in error. 12 | """ 13 | ] 14 | 15 | @doc false 16 | @impl true 17 | def run(%SourceFile{} = source_file, params) do 18 | issue_meta = IssueMeta.for(source_file, params) 19 | Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 20 | end 21 | 22 | defp traverse( 23 | {{:., _, [{:__aliases__, meta, [:"Elixir", :IO]}, :inspect]}, _, args} = ast, 24 | issues, 25 | issue_meta 26 | ) 27 | when length(args) < 3 do 28 | {ast, issues_for_call(meta, "Elixir.IO.inspect", issues, issue_meta)} 29 | end 30 | 31 | defp traverse( 32 | {{:., _, [{:__aliases__, meta, [:IO]}, :inspect]}, _, args} = ast, 33 | issues, 34 | issue_meta 35 | ) 36 | when length(args) < 3 do 37 | {ast, issues_for_call(meta, "IO.inspect", issues, issue_meta)} 38 | end 39 | 40 | defp traverse(ast, issues, _issue_meta) do 41 | {ast, issues} 42 | end 43 | 44 | defp issues_for_call(meta, trigger, issues, issue_meta) do 45 | [issue_for(issue_meta, meta, trigger) | issues] 46 | end 47 | 48 | defp issue_for(issue_meta, meta, trigger) do 49 | format_issue( 50 | issue_meta, 51 | message: "There should be no calls to `IO.inspect/1`.", 52 | trigger: trigger, 53 | line_no: meta[:line], 54 | column: meta[:column] 55 | ) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_file_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedFileOperation do 2 | use Credo.Check, 3 | id: "EX5018", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | The result of a call to the File module's functions has to be used. 8 | 9 | While this is correct ... 10 | 11 | def read_from_cwd(filename) do 12 | # TODO: use Path.join/2 13 | filename = File.cwd!() <> "/" <> filename 14 | 15 | File.read(filename) 16 | end 17 | 18 | ... we forgot to save the result in this example: 19 | 20 | def read_from_cwd(filename) do 21 | File.cwd!() <> "/" <> filename 22 | 23 | File.read(filename) 24 | end 25 | 26 | Since Elixir variables are immutable, many File operations don't work on the 27 | variable you pass in, but return a new variable which has to be used somehow. 28 | """ 29 | ] 30 | 31 | alias Credo.Check.Warning.UnusedOperation 32 | 33 | @checked_module :File 34 | @funs_with_return_value ~w(cwd cwd! dir? exists? read read! regular? stat stat!)a 35 | 36 | @doc false 37 | @impl true 38 | def run(%SourceFile{} = source_file, params) do 39 | UnusedOperation.run( 40 | source_file, 41 | params, 42 | @checked_module, 43 | @funs_with_return_value, 44 | &format_issue/2 45 | ) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_list_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedListOperation do 2 | use Credo.Check, 3 | id: "EX5020", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | The result of a call to the List module's functions has to be used. 8 | 9 | While this is correct ... 10 | 11 | def sort_usernames(usernames) do 12 | usernames = List.flatten(usernames) 13 | 14 | List.sort(usernames) 15 | end 16 | 17 | ... we forgot to save the result in this example: 18 | 19 | def sort_usernames(usernames) do 20 | List.flatten(usernames) 21 | 22 | List.sort(usernames) 23 | end 24 | 25 | List operations never work on the variable you pass in, but return a new 26 | variable which has to be used somehow. 27 | """ 28 | ] 29 | 30 | alias Credo.Check.Warning.UnusedOperation 31 | 32 | @checked_module :List 33 | @funs_with_return_value nil 34 | 35 | @doc false 36 | @impl true 37 | def run(%SourceFile{} = source_file, params) do 38 | UnusedOperation.run( 39 | source_file, 40 | params, 41 | @checked_module, 42 | @funs_with_return_value, 43 | &format_issue/2 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedOperation do 2 | # The result of a call to the provided module's functions has to be used. 3 | 4 | alias Credo.Check.Warning.UnusedFunctionReturnHelper 5 | alias Credo.IssueMeta 6 | 7 | @doc false 8 | def run(source_file, params \\ [], checked_module, funs_with_return_value, format_issue_fun) do 9 | issue_meta = IssueMeta.for(source_file, params) 10 | 11 | relevant_funs = 12 | if params[:ignore] do 13 | ignored_funs = List.wrap(params[:ignore]) 14 | 15 | funs_with_return_value -- ignored_funs 16 | else 17 | funs_with_return_value 18 | end 19 | 20 | all_unused_calls = 21 | UnusedFunctionReturnHelper.find_unused_calls( 22 | source_file, 23 | params, 24 | [checked_module], 25 | relevant_funs 26 | ) 27 | 28 | Enum.reduce(all_unused_calls, [], fn invalid_call, issues -> 29 | {{:., _, [{:__aliases__, meta, _}, _fun_name]}, _, _} = invalid_call 30 | 31 | trigger = 32 | invalid_call 33 | |> Macro.to_string() 34 | |> String.split("(") 35 | |> List.first() 36 | 37 | [issue_for(format_issue_fun, issue_meta, meta, trigger, checked_module) | issues] 38 | end) 39 | end 40 | 41 | defp issue_for(format_issue_fun, issue_meta, meta, trigger, checked_module) do 42 | format_issue_fun.( 43 | issue_meta, 44 | message: "There should be no unused return values for #{checked_module} functions.", 45 | trigger: trigger, 46 | line_no: meta[:line], 47 | column: meta[:column] 48 | ) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_path_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedPathOperation do 2 | use Credo.Check, 3 | id: "EX5021", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | The result of a call to the Path module's functions has to be used. 8 | 9 | While this is correct ... 10 | 11 | def read_from_cwd(filename) do 12 | filename = Path.join(cwd, filename) 13 | 14 | File.read(filename) 15 | end 16 | 17 | ... we forgot to save the result in this example: 18 | 19 | def read_from_cwd(filename) do 20 | Path.join(cwd, filename) 21 | 22 | File.read(filename) 23 | end 24 | 25 | Path operations never work on the variable you pass in, but return a new 26 | variable which has to be used somehow. 27 | """ 28 | ] 29 | 30 | alias Credo.Check.Warning.UnusedOperation 31 | 32 | @checked_module :Path 33 | @funs_with_return_value nil 34 | 35 | @doc false 36 | @impl true 37 | def run(%SourceFile{} = source_file, params) do 38 | UnusedOperation.run( 39 | source_file, 40 | params, 41 | @checked_module, 42 | @funs_with_return_value, 43 | &format_issue/2 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_regex_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedRegexOperation do 2 | use Credo.Check, 3 | id: "EX5022", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | The result of a call to the Regex module's functions has to be used. 8 | 9 | While this is correct ... 10 | 11 | def extract_username_and_salute(regex, string) do 12 | [string] = Regex.run(regex, string) 13 | 14 | "Hi #\{string}" 15 | end 16 | 17 | ... we forgot to save the downcased username in this example: 18 | 19 | def extract_username_and_salute(regex, string) do 20 | Regex.run(regex, string) 21 | 22 | "Hi #\{string}" 23 | end 24 | 25 | Regex operations never work on the variable you pass in, but return a new 26 | variable which has to be used somehow. 27 | """ 28 | ] 29 | 30 | alias Credo.Check.Warning.UnusedOperation 31 | 32 | @checked_module :Regex 33 | @funs_with_return_value nil 34 | 35 | @doc false 36 | @impl true 37 | def run(%SourceFile{} = source_file, params) do 38 | UnusedOperation.run( 39 | source_file, 40 | params, 41 | @checked_module, 42 | @funs_with_return_value, 43 | &format_issue/2 44 | ) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_string_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedStringOperation do 2 | use Credo.Check, 3 | id: "EX5023", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | The result of a call to the String module's functions has to be used. 8 | 9 | While this is correct ... 10 | 11 | def salutation(username) do 12 | username = String.downcase(username) 13 | 14 | "Hi #\{username}" 15 | end 16 | 17 | ... we forgot to save the downcased username in this example: 18 | 19 | # This is bad because it does not modify the username variable! 20 | 21 | def salutation(username) do 22 | String.downcase(username) 23 | 24 | "Hi #\{username}" 25 | end 26 | 27 | Since Elixir variables are immutable, String operations never work on the 28 | variable you pass in, but return a new variable which has to be used somehow. 29 | """ 30 | ] 31 | 32 | alias Credo.Check.Warning.UnusedOperation 33 | 34 | @checked_module :String 35 | @funs_with_return_value nil 36 | 37 | @doc false 38 | @impl true 39 | def run(%SourceFile{} = source_file, params) do 40 | UnusedOperation.run( 41 | source_file, 42 | params, 43 | @checked_module, 44 | @funs_with_return_value, 45 | &format_issue/2 46 | ) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/credo/check/warning/unused_tuple_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnusedTupleOperation do 2 | use Credo.Check, 3 | id: "EX5024", 4 | base_priority: :high, 5 | explanations: [ 6 | check: """ 7 | The result of a call to the Tuple module's functions has to be used. 8 | 9 | While this is correct ... 10 | 11 | def remove_magic_item!(tuple) do 12 | tuple = Tuple.delete_at(tuple, 0) 13 | 14 | if Enum.length(tuple) == 0, do: raise "OMG!!!1" 15 | 16 | tuple 17 | end 18 | 19 | ... we forgot to save the result in this example: 20 | 21 | def remove_magic_item!(tuple) do 22 | Tuple.delete_at(tuple, 0) 23 | 24 | if Enum.length(tuple) == 0, do: raise "OMG!!!1" 25 | 26 | tuple 27 | end 28 | 29 | Tuple operations never work on the variable you pass in, but return a new 30 | variable which has to be used somehow. 31 | """ 32 | ] 33 | 34 | alias Credo.Check.Warning.UnusedOperation 35 | 36 | @checked_module :Tuple 37 | @funs_with_return_value nil 38 | 39 | @doc false 40 | @impl true 41 | def run(%SourceFile{} = source_file, params) do 42 | UnusedOperation.run( 43 | source_file, 44 | params, 45 | @checked_module, 46 | @funs_with_return_value, 47 | &format_issue/2 48 | ) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/credo/check/warning/wrong_test_file_extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.WrongTestFileExtension do 2 | use Credo.Check, 3 | id: "EX5025", 4 | base_priority: :high, 5 | param_defaults: [ 6 | files: %{included: ["test/**/*_test.ex", "apps/**/test/**/*_test.ex"]} 7 | ], 8 | explanations: [ 9 | check: """ 10 | Invoking mix test from the command line will run the tests in each file 11 | matching the pattern `*_test.exs` found in the test directory of your project. 12 | 13 | (from the `ex_unit` docs) 14 | 15 | This check ensures that test files are not ending with `_test.ex` (which would cause them to be skipped). 16 | """ 17 | ] 18 | 19 | alias Credo.SourceFile 20 | 21 | @doc false 22 | def run(%SourceFile{} = source_file, params \\ []) do 23 | source_file 24 | |> IssueMeta.for(params) 25 | |> issue_for() 26 | |> List.wrap() 27 | end 28 | 29 | defp issue_for(issue_meta) do 30 | format_issue( 31 | issue_meta, 32 | message: "Test files should end with `_test.exs`.", 33 | line_no: 1, 34 | trigger: Issue.no_trigger() 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/credo/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI do 2 | @moduledoc """ 3 | `Credo.CLI` is the entrypoint for both the Mix task and the escript. 4 | """ 5 | 6 | alias Credo.Execution 7 | 8 | @doc """ 9 | Runs Credo with the given `argv` and exits the process. 10 | 11 | See `Credo.run/1` if you want to run Credo programmatically. 12 | """ 13 | def main(argv \\ []) do 14 | Credo.Application.start(nil, nil) 15 | 16 | {options, _argv_rest, _errors} = OptionParser.parse(argv, strict: [watch: :boolean]) 17 | 18 | if options[:watch] do 19 | run_to_watch(argv) 20 | else 21 | run_to_halt(argv) 22 | end 23 | end 24 | 25 | @doc false 26 | @deprecated "Use Credo.run/1 instead" 27 | def run(argv) do 28 | Credo.run(argv) 29 | end 30 | 31 | defp run_to_watch(argv) do 32 | Credo.Watcher.run(argv) 33 | 34 | receive do 35 | _ -> nil 36 | end 37 | end 38 | 39 | defp run_to_halt(argv) do 40 | argv 41 | |> Credo.run() 42 | |> halt_if_exit_status_assigned() 43 | end 44 | 45 | defp halt_if_exit_status_assigned(%Execution{mute_exit_status: true}) do 46 | # Skip if exit status is muted 47 | end 48 | 49 | defp halt_if_exit_status_assigned(exec) do 50 | exec 51 | |> Execution.get_exit_status() 52 | |> halt_if_failed() 53 | end 54 | 55 | defp halt_if_failed(0), do: nil 56 | defp halt_if_failed(exit_status), do: exit({:shutdown, exit_status}) 57 | end 58 | -------------------------------------------------------------------------------- /lib/credo/cli/command/categories/categories_output.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Categories.CategoriesOutput do 2 | @moduledoc false 3 | 4 | def print_categories(exec, categories) do 5 | format_mod = format_mod(exec) 6 | 7 | format_mod.print(exec, categories) 8 | end 9 | 10 | defp format_mod(%{format: "json"}), do: Credo.CLI.Command.Categories.Output.Json 11 | defp format_mod(%{format: nil}), do: Credo.CLI.Command.Categories.Output.Default 12 | end 13 | -------------------------------------------------------------------------------- /lib/credo/cli/command/categories/output/default.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Categories.Output.Default do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output 5 | alias Credo.CLI.Output.UI 6 | 7 | def print(_exec, categories) do 8 | Enum.each(categories, &print_category/1) 9 | end 10 | 11 | defp print_category(%{color: color, title: title, description: text}) do 12 | term_width = Output.term_columns() 13 | 14 | UI.puts() 15 | 16 | [ 17 | :bright, 18 | "#{color}_background" |> String.to_atom(), 19 | color, 20 | " ", 21 | Output.foreground_color(color), 22 | :normal, 23 | " #{title}" |> String.pad_trailing(term_width - 1) 24 | ] 25 | |> UI.puts() 26 | 27 | color 28 | |> UI.edge() 29 | |> UI.puts() 30 | 31 | text 32 | |> String.split("\n") 33 | |> Enum.each(&print_line(&1, color)) 34 | end 35 | 36 | defp print_line(line, color) do 37 | [UI.edge(color), " ", :reset, line] 38 | |> UI.puts() 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/credo/cli/command/categories/output/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Categories.Output.Json do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | 6 | def print(_exec, categories) do 7 | JSON.print_map(%{categories: categories}) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/credo/cli/command/diff/output/flycheck.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Diff.Output.FlyCheck do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.Flycheck 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> Flycheck.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/diff/output/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Diff.Output.Json do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | issues = Execution.get_issues(exec) 11 | new_issues = Enum.filter(issues, &(&1.diff_marker == :new)) 12 | fixed_issues = Enum.filter(issues, &(&1.diff_marker == :fixed)) 13 | old_issues = Enum.filter(issues, &(&1.diff_marker == :old)) 14 | 15 | %{ 16 | "diff" => %{ 17 | "new" => Enum.map(new_issues, &JSON.issue_to_json/1), 18 | "fixed" => Enum.map(fixed_issues, &JSON.issue_to_json/1), 19 | "old" => Enum.map(old_issues, &JSON.issue_to_json/1) 20 | } 21 | } 22 | |> JSON.print_map() 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/credo/cli/command/diff/output/oneline.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Diff.Output.Oneline do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.Oneline 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> Oneline.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/diff/task/filter_issues_for_exit_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Diff.Task.FilterIssuesForExitStatus do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | def call(exec, _opts) do 7 | issues = 8 | exec 9 | |> Execution.get_issues() 10 | |> Enum.filter(fn 11 | %Credo.Issue{diff_marker: :new} -> true 12 | _ -> false 13 | end) 14 | 15 | Execution.put_issues(exec, issues) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/credo/cli/command/diff/task/print_before_info.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Diff.Task.PrintBeforeInfo do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Command.Diff.DiffOutput 7 | 8 | def call(exec, _opts) do 9 | source_files = Execution.get_source_files(exec) 10 | 11 | DiffOutput.print_before_info(source_files, exec) 12 | 13 | exec 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/credo/cli/command/diff/task/print_results_and_summary.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Diff.Task.PrintResultsAndSummary do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Command.Diff.DiffOutput 7 | 8 | def call(exec, _opts) do 9 | source_files = Execution.get_source_files(exec) 10 | 11 | time_load = Execution.get_assign(exec, "credo.time.source_files") 12 | time_run = Execution.get_assign(exec, "credo.time.run_checks") 13 | 14 | DiffOutput.print_after_info(source_files, exec, time_load, time_run) 15 | 16 | exec 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/credo/cli/command/explain/explain_output.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Explain.ExplainOutput do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.UI 5 | 6 | use Credo.CLI.Output.FormatDelegator, 7 | default: Credo.CLI.Command.Explain.Output.Default, 8 | json: Credo.CLI.Command.Explain.Output.Json 9 | 10 | def print_help(exec) do 11 | usage = [ 12 | "Usage: ", 13 | :olive, 14 | "mix credo explain [options]" 15 | ] 16 | 17 | description = """ 18 | 19 | Explain the given check or issue. 20 | """ 21 | 22 | example = [ 23 | "Examples:\n", 24 | :olive, 25 | " $ mix credo explain lib/foo/bar.ex:13:6\n", 26 | " $ mix credo explain lib/foo/bar.ex:13:6 --format json\n", 27 | " $ mix credo explain Credo.Check.Refactor.Nesting" 28 | ] 29 | 30 | options = 31 | """ 32 | 33 | Explain options: 34 | --format Display the list in a specific format (json,flycheck,sarif,oneline) 35 | 36 | General options: 37 | --[no-]color Toggle colored output 38 | -v, --version Show version 39 | -h, --help Show this help 40 | 41 | Find advanced usage instructions and more examples here: 42 | https://hexdocs.pm/credo/explain_command.html 43 | 44 | Give feedback and open an issue here: 45 | https://github.com/rrrene/credo/issues 46 | """ 47 | |> String.trim_trailing() 48 | 49 | UI.puts() 50 | UI.puts(usage) 51 | UI.puts(description) 52 | UI.puts(example) 53 | UI.puts(options) 54 | 55 | exec 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/credo/cli/command/explain/output/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Explain.Output.Json do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | 6 | def print_before_info(_source_files, _exec), do: nil 7 | 8 | def print_after_info(explanations, _exec, _, _) do 9 | JSON.print_map(%{explanations: Enum.map(explanations, &cast_to_json/1)}) 10 | end 11 | 12 | defp cast_to_json(%{line_no: _line_no} = explanation) do 13 | related_code = Enum.map(explanation.related_code, &Tuple.to_list/1) 14 | 15 | explanation 16 | |> Map.put(:related_code, related_code) 17 | end 18 | 19 | defp cast_to_json(explanation), do: explanation 20 | end 21 | -------------------------------------------------------------------------------- /lib/credo/cli/command/gen.config.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.GenConfig do 2 | @moduledoc false 3 | 4 | @config_filename ".credo.exs" 5 | @default_config_file File.read!(@config_filename) 6 | 7 | use Credo.CLI.Command, 8 | short_description: "Initialize a new .credo.exs exec file in the current directory" 9 | 10 | alias Credo.CLI.Output.UI 11 | 12 | @doc false 13 | def call(exec, _opts) do 14 | create_config_file(@config_filename) 15 | 16 | exec 17 | end 18 | 19 | defp create_config_file(filename) do 20 | if File.exists?(filename) do 21 | UI.puts([:red, :bright, "File exists: #{filename}, aborted."]) 22 | else 23 | UI.puts([:green, "* creating ", :reset, "#{filename}"]) 24 | write_config_file(filename) 25 | end 26 | end 27 | 28 | defp write_config_file(filename) do 29 | filename 30 | |> Path.dirname() 31 | |> File.mkdir_p!() 32 | 33 | File.write!(filename, @default_config_file) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/credo/cli/command/info/output/default.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Info.Output.Default do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.UI 5 | alias Credo.Execution 6 | 7 | def print(%Execution{verbose: true}, info) do 8 | info 9 | |> verbose_info() 10 | |> UI.puts() 11 | end 12 | 13 | def print(_exec, info) do 14 | info 15 | |> basic_info() 16 | |> UI.puts() 17 | end 18 | 19 | defp basic_info(info) do 20 | """ 21 | System: 22 | Credo: #{info["system"]["credo"]} 23 | Elixir: #{info["system"]["elixir"]} 24 | Erlang: #{info["system"]["erlang"]} 25 | """ 26 | |> String.trim() 27 | end 28 | 29 | defp verbose_info(info) do 30 | """ 31 | #{basic_info(info)} 32 | Configuration: 33 | Files:#{Enum.map(info["config"]["files"], &list_entry/1)} 34 | Checks:#{Enum.map(info["config"]["checks"], &list_entry/1)} 35 | """ 36 | |> String.trim() 37 | end 38 | 39 | defp list_entry(%{"name" => name}) do 40 | "\n - #{name}" 41 | end 42 | 43 | defp list_entry(name) do 44 | "\n - #{name}" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/credo/cli/command/info/output/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Info.Output.Json do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | alias Credo.Execution 6 | 7 | def print(%Execution{verbose: true}, info) do 8 | info 9 | |> verbose_info() 10 | |> JSON.print_map() 11 | end 12 | 13 | def print(_exec, info) do 14 | info 15 | |> basic_info() 16 | |> JSON.print_map() 17 | end 18 | 19 | defp basic_info(info) do 20 | %{ 21 | system: info["system"] 22 | } 23 | end 24 | 25 | defp verbose_info(info) do 26 | info 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/credo/cli/command/list/output/flycheck.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.List.Output.FlyCheck do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.Flycheck 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> Flycheck.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/list/output/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.List.Output.Json do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> JSON.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/list/output/oneline.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.List.Output.Oneline do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.Oneline 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> Oneline.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/list/output/sarif.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.List.Output.Sarif do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.SARIF 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> SARIF.print_issues(exec) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/suggest/output/flycheck.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Suggest.Output.FlyCheck do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.Flycheck 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> Flycheck.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/suggest/output/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Suggest.Output.Json do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> JSON.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/suggest/output/oneline.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Suggest.Output.Oneline do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.Oneline 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> Oneline.print_issues() 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/suggest/output/sarif.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Suggest.Output.Sarif do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.SARIF 5 | alias Credo.Execution 6 | 7 | def print_before_info(_source_files, _exec), do: nil 8 | 9 | def print_after_info(_source_files, exec, _time_load, _time_run) do 10 | exec 11 | |> Execution.get_issues() 12 | |> SARIF.print_issues(exec) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/credo/cli/command/version.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.Version do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | alias Credo.CLI.Output.UI 6 | alias Credo.CLI.Switch 7 | alias Credo.Execution 8 | 9 | use Credo.CLI.Command, 10 | short_description: "Show Credo's version number", 11 | cli_switches: [ 12 | Switch.string("format"), 13 | Switch.boolean("version", alias: :v) 14 | ] 15 | 16 | @doc false 17 | def call(%Execution{format: "json"} = exec, _opts) do 18 | JSON.print_map(%{version: Credo.version()}) 19 | 20 | exec 21 | end 22 | 23 | def call(exec, _opts) do 24 | UI.puts(Credo.version()) 25 | 26 | exec 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/credo/cli/exit_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.ExitStatus do 2 | @moduledoc false 3 | 4 | def generic_error, do: 128 5 | def config_parser_error, do: 129 6 | def config_loaded_but_invalid, do: 130 7 | end 8 | -------------------------------------------------------------------------------- /lib/credo/cli/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Filter do 2 | @moduledoc false 3 | 4 | # TODO: is this the right place for this module? 5 | 6 | alias Credo.Check.ConfigComment 7 | alias Credo.Execution 8 | alias Credo.Issue 9 | alias Credo.SourceFile 10 | 11 | def important(list, exec) when is_list(list) do 12 | Enum.filter(list, &important?(&1, exec)) 13 | end 14 | 15 | def important?(%Issue{} = issue, exec) do 16 | issue.priority >= exec.min_priority 17 | end 18 | 19 | def important?(%SourceFile{filename: filename}, exec) do 20 | exec 21 | |> Execution.get_issues(filename) 22 | |> Enum.any?(&important?(&1, exec)) 23 | end 24 | 25 | def valid_issues(list, exec) when is_list(list) do 26 | Enum.reject(list, fn issue -> 27 | ignored_by_config_comment?(issue, exec) 28 | end) 29 | end 30 | 31 | def ignored_by_config_comment?(%Issue{} = issue, exec) do 32 | case exec.config_comment_map[issue.filename] do 33 | list when is_list(list) -> 34 | Enum.any?(list, &ConfigComment.ignores_issue?(&1, issue)) 35 | 36 | _ -> 37 | false 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/credo/cli/output/formatter/flycheck.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Output.Formatter.Flycheck do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Filename 5 | alias Credo.CLI.Output 6 | alias Credo.CLI.Output.UI 7 | alias Credo.Issue 8 | 9 | def print_issues(issues) do 10 | Enum.each(issues, fn issue -> 11 | issue 12 | |> to_flycheck() 13 | |> UI.puts() 14 | end) 15 | end 16 | 17 | def to_flycheck( 18 | %Issue{ 19 | message: message, 20 | filename: filename, 21 | column: column, 22 | line_no: line_no 23 | } = issue 24 | ) do 25 | pos_suffix = Filename.pos_suffix(line_no, column) 26 | tag = Output.check_tag(issue, false) 27 | 28 | "#{filename}#{pos_suffix}: #{tag}: #{message}" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/credo/cli/output/formatter/oneline.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Output.Formatter.Oneline do 2 | @moduledoc false 3 | 4 | alias Credo.CLI.Filename 5 | alias Credo.CLI.Output 6 | alias Credo.CLI.Output.UI 7 | alias Credo.Issue 8 | 9 | def print_issues(issues) do 10 | issues 11 | |> Enum.sort_by(fn issue -> {issue.filename, issue.line_no, issue.column} end) 12 | |> Enum.each(fn issue -> 13 | UI.puts(to_oneline(issue)) 14 | end) 15 | end 16 | 17 | defp to_oneline( 18 | %Issue{ 19 | check: check, 20 | message: message, 21 | filename: filename, 22 | priority: priority 23 | } = issue 24 | ) do 25 | inner_color = Output.check_color(issue) 26 | message_color = inner_color 27 | filename_color = :default_color 28 | 29 | [ 30 | inner_color, 31 | Output.check_tag(check.category()), 32 | " ", 33 | priority |> Output.priority_arrow(), 34 | " ", 35 | :reset, 36 | filename_color, 37 | :faint, 38 | filename |> to_string, 39 | :default_color, 40 | :faint, 41 | Filename.pos_suffix(issue.line_no, issue.column), 42 | :reset, 43 | message_color, 44 | " ", 45 | message 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/credo/cli/sorter.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Sorter do 2 | @moduledoc false 3 | 4 | # TODO: is this the right place for this module? 5 | 6 | def ensure(list, list_start, list_end \\ []) do 7 | list 8 | |> to_start(list_start) 9 | |> to_end(list_end) 10 | end 11 | 12 | def to_start(list, list_start) do 13 | list_start = Enum.filter(list_start, &Enum.member?(list, &1)) 14 | 15 | list_start ++ (list -- list_start) 16 | end 17 | 18 | def to_end(list, list_end) do 19 | list_end = Enum.filter(list_end, &Enum.member?(list, &1)) 20 | 21 | (list -- list_end) ++ list_end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/credo/cli/switch.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Switch do 2 | defstruct name: nil, 3 | type: :string, 4 | alias: nil 5 | 6 | def boolean(name, keywords \\ []) when is_atom(name) or is_binary(name) do 7 | from_keywords(name, Keyword.put(keywords, :type, :boolean)) 8 | end 9 | 10 | def string(name, keywords \\ []) when is_atom(name) or is_binary(name) do 11 | from_keywords(name, Keyword.put(keywords, :type, :string)) 12 | end 13 | 14 | def keep(name, keywords \\ []) when is_atom(name) or is_binary(name) do 15 | from_keywords(name, Keyword.put(keywords, :type, :keep)) 16 | end 17 | 18 | def ensure(%__MODULE__{} = switch), do: switch 19 | def ensure(%{} = switch), do: from_map(switch) 20 | 21 | def ensure(switch) do 22 | raise("Expected Credo.CLI.Switch, got: #{inspect(switch, pretty: true)}") 23 | end 24 | 25 | defp from_map(%{name: name} = switch) when is_atom(name) or is_binary(name) do 26 | name = String.to_atom(to_string(switch.name)) 27 | 28 | struct(__MODULE__, %{switch | name: name}) 29 | end 30 | 31 | defp from_keywords(name, keywords) when is_atom(name) or is_binary(name) do 32 | name = String.to_atom(to_string(name)) 33 | type = keywords[:type] || :string 34 | 35 | keywords = 36 | keywords 37 | |> Keyword.put(:type, type) 38 | |> Keyword.put(:name, name) 39 | 40 | struct(__MODULE__, keywords) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/credo/cli/task/load_and_validate_source_files.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Task.LoadAndValidateSourceFiles do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Output 7 | alias Credo.Sources 8 | 9 | def call(exec, _opts \\ []) do 10 | {time_load, source_files} = 11 | :timer.tc(fn -> 12 | exec 13 | |> Sources.find() 14 | |> Enum.group_by(& &1.status) 15 | end) 16 | 17 | Output.complain_about_invalid_source_files(Map.get(source_files, :invalid, [])) 18 | Output.complain_about_timed_out_source_files(Map.get(source_files, :timed_out, [])) 19 | 20 | valid_source_files = Map.get(source_files, :valid, []) 21 | 22 | exec 23 | |> put_source_files(valid_source_files) 24 | |> put_assign("credo.time.source_files", time_load) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/credo/cli/task/run_checks.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Task.RunChecks do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.Check.Runner 7 | 8 | def call(exec, _opts \\ []) do 9 | source_files = get_source_files(exec) 10 | 11 | {time_run, :ok} = 12 | :timer.tc(fn -> 13 | Runner.run(source_files, exec) 14 | end) 15 | 16 | put_assign(exec, "credo.time.run_checks", time_run) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/credo/cli/task/set_relevant_issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Task.SetRelevantIssues do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Filter 7 | 8 | def call(exec, _opts \\ []) do 9 | issues = 10 | exec 11 | |> get_issues() 12 | |> Filter.important(exec) 13 | |> Filter.valid_issues(exec) 14 | |> Enum.sort_by(fn issue -> 15 | {issue.check.id(), issue.filename, issue.line_no} 16 | end) 17 | 18 | put_issues(exec, issues) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/credo/code/token_ast_correlation.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Code.TokenAstCorrelation do 2 | # Elixir >= 1.6.0 3 | def find_tokens_in_ast(wanted_token, ast) do 4 | Credo.Code.prewalk(ast, &traverse_ast_for_token(&1, &2, wanted_token)) 5 | end 6 | 7 | defp traverse_ast_for_token({:., meta, arguments} = ast, acc, token) 8 | when is_list(arguments) do 9 | {line_no_start, col_start, _line_no_end, _col_end} = Credo.Code.Token.position(token) 10 | 11 | if meta[:line] == line_no_start && meta[:column] == col_start - 1 do 12 | {nil, acc ++ [ast]} 13 | else 14 | {ast, acc} 15 | end 16 | end 17 | 18 | defp traverse_ast_for_token({_name, meta, _arguments} = ast, acc, token) do 19 | {line_no_start, col_start, _line_no_end, _col_end} = Credo.Code.Token.position(token) 20 | 21 | if meta[:line] == line_no_start && meta[:column] == col_start do 22 | {nil, acc ++ [ast]} 23 | else 24 | {ast, acc} 25 | end 26 | end 27 | 28 | defp traverse_ast_for_token(ast, acc, _token) do 29 | {ast, acc} 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/credo/execution/execution_config_files.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.ExecutionConfigFiles do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | alias Credo.Execution 7 | 8 | def start_server(exec) do 9 | {:ok, pid} = GenServer.start_link(__MODULE__, []) 10 | 11 | %{exec | config_files_pid: pid} 12 | end 13 | 14 | def put(%Execution{config_files_pid: pid}, list) do 15 | GenServer.call(pid, {:put, list}) 16 | end 17 | 18 | def get(%Execution{config_files_pid: pid}) do 19 | GenServer.call(pid, :get) 20 | end 21 | 22 | # callbacks 23 | 24 | def init(_) do 25 | {:ok, []} 26 | end 27 | 28 | def handle_call({:put, new_state}, _from, _current_state) do 29 | {:reply, new_state, new_state} 30 | end 31 | 32 | def handle_call(:get, _from, current_state) do 33 | {:reply, current_state, current_state} 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/credo/execution/execution_source_files.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.ExecutionSourceFiles do 2 | @moduledoc false 3 | 4 | use GenServer 5 | 6 | alias Credo.Execution 7 | 8 | def start_server(exec) do 9 | {:ok, pid} = GenServer.start_link(__MODULE__, []) 10 | 11 | %{exec | source_files_pid: pid} 12 | end 13 | 14 | def put(%Execution{source_files_pid: pid}, list) do 15 | GenServer.call(pid, {:put, list}) 16 | end 17 | 18 | def get(%Execution{source_files_pid: pid}) do 19 | GenServer.call(pid, :get) 20 | end 21 | 22 | # callbacks 23 | 24 | def init(_) do 25 | {:ok, []} 26 | end 27 | 28 | def handle_call({:put, new_state}, _from, _current_state) do 29 | {:reply, new_state, new_state} 30 | end 31 | 32 | def handle_call(:get, _from, current_state) do 33 | {:reply, current_state, current_state} 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/credo/execution/task/append_default_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.AppendDefaultConfig do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.Execution 7 | 8 | @default_config_filename ".credo.exs" 9 | @default_config_file_content File.read!(@default_config_filename) 10 | @origin_credo :credo 11 | 12 | def call(exec, _opts) do 13 | Execution.append_config_file(exec, {@origin_credo, nil, @default_config_file_content}) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/credo/execution/task/append_extra_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.AppendExtraConfig do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.Execution 7 | 8 | @extra_config_env_name "CREDO_EXTRA_CONFIG" 9 | @origin_extra_config :env 10 | 11 | def call(exec, _opts) do 12 | case System.get_env(@extra_config_env_name) do 13 | nil -> exec 14 | "" -> exec 15 | value -> Execution.append_config_file(exec, {@origin_extra_config, nil, value}) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/credo/execution/task/assign_exit_status_for_issues.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.AssignExitStatusForIssues do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | import Bitwise 7 | 8 | def call(exec, _opts) do 9 | exit_status = 10 | exec 11 | |> get_issues() 12 | |> to_exit_status() 13 | 14 | put_exit_status(exec, exit_status) 15 | end 16 | 17 | # Converts the return value of a Command.run() call into an exit_status 18 | defp to_exit_status([]), do: 0 19 | 20 | defp to_exit_status(issues) do 21 | issues 22 | |> Enum.map(& &1.exit_status) 23 | |> Enum.reduce(0, &(&1 ||| &2)) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/credo/execution/task/determine_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.DetermineCommand do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Options 7 | alias Credo.Execution 8 | 9 | def call(exec, _opts) do 10 | determine_command(exec, exec.cli_options) 11 | end 12 | 13 | # `--help` given 14 | defp determine_command( 15 | exec, 16 | %Options{command: nil, switches: %{help: true}} = options 17 | ) do 18 | set_command_and_path(exec, options, "help", options.path) 19 | end 20 | 21 | # `--version` given 22 | defp determine_command( 23 | exec, 24 | %Options{command: nil, switches: %{version: true}} = options 25 | ) do 26 | set_command_and_path(exec, options, "version", options.path) 27 | end 28 | 29 | defp determine_command(exec, options) do 30 | command_name = 31 | case exec.cli_options.args do 32 | [potential_command_name | _] -> 33 | command_names = Execution.get_valid_command_names(exec) 34 | 35 | if Enum.member?(command_names, potential_command_name) do 36 | potential_command_name 37 | end 38 | 39 | _ -> 40 | nil 41 | end 42 | 43 | set_command_and_path(exec, options, command_name, options.path) 44 | end 45 | 46 | defp set_command_and_path(exec, _options, nil, _path), do: exec 47 | 48 | defp set_command_and_path(exec, options, command, path) do 49 | %{ 50 | exec 51 | | cli_options: %{ 52 | options 53 | | command: command, 54 | path: path 55 | } 56 | } 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/credo/execution/task/initialize_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.InitializeCommand do 2 | @moduledoc false 3 | 4 | alias Credo.Execution 5 | 6 | def call(%Execution{} = exec, _opts) do 7 | command_name = Execution.get_command_name(exec) 8 | command_mod = Execution.get_command(exec, command_name) 9 | 10 | exec = 11 | command_mod 12 | |> cli_options_switches() 13 | |> Enum.reduce(exec, fn {switch_name, switch_type}, exec -> 14 | Execution.put_cli_switch(exec, command_mod, switch_name, switch_type) 15 | end) 16 | 17 | command_mod 18 | |> cli_options_aliases() 19 | |> Enum.reduce(exec, fn {switch_alias, switch_name}, exec -> 20 | Execution.put_cli_switch_alias(exec, command_mod, switch_name, switch_alias) 21 | end) 22 | end 23 | 24 | defp cli_options_switches(command_mod) do 25 | command_mod.cli_switches() 26 | |> List.wrap() 27 | |> Enum.map(fn 28 | %{name: name, type: type} when is_binary(name) -> {String.to_atom(name), type} 29 | %{name: name, type: type} when is_atom(name) -> {name, type} 30 | end) 31 | end 32 | 33 | defp cli_options_aliases(command_mod) do 34 | command_mod.cli_switches() 35 | |> List.wrap() 36 | |> Enum.map(fn 37 | %{name: name, alias: alias} when is_binary(name) -> {alias, String.to_atom(name)} 38 | %{name: name, alias: alias} when is_atom(name) -> {alias, name} 39 | _ -> nil 40 | end) 41 | |> Enum.reject(&is_nil/1) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/credo/execution/task/initialize_plugins.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.InitializePlugins do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.Execution 7 | 8 | def call(exec, _opts) do 9 | Enum.reduce(exec.plugins, exec, &init_plugin(&2, &1)) 10 | end 11 | 12 | defp init_plugin(exec, {_mod, false}), do: exec 13 | 14 | defp init_plugin(exec, {mod, _params}) do 15 | if Code.ensure_loaded?(mod) do 16 | if function_exported?(mod, :init, 1) do 17 | exec 18 | |> Execution.set_initializing_plugin(mod) 19 | |> mod.init() 20 | |> Execution.ensure_execution_struct("#{mod}.init/1") 21 | |> Execution.set_initializing_plugin(nil) 22 | else 23 | Execution.halt( 24 | exec, 25 | "Plugin module `#{Credo.Code.Module.name(mod)}` is not a valid plugin: does not implement expected behavior." 26 | ) 27 | end 28 | else 29 | Execution.halt( 30 | exec, 31 | "Plugin module `#{Credo.Code.Module.name(mod)}` is not available and could not be initialized." 32 | ) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/credo/execution/task/parse_options.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.ParseOptions do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Options 7 | alias Credo.CLI.Output.UI 8 | alias Credo.Execution 9 | 10 | def call(exec, opts) do 11 | add_common_aliases? = opts[:parser_mode] == :preliminary 12 | use_strict_parser? = opts[:parser_mode] == :strict 13 | 14 | command_names = Execution.get_valid_command_names(exec) 15 | 16 | given_command_name = 17 | if exec.cli_options do 18 | exec.cli_options.command 19 | end 20 | 21 | cli_aliases = 22 | if add_common_aliases? do 23 | exec.cli_aliases ++ [h: :help, v: :version] 24 | else 25 | exec.cli_aliases 26 | end 27 | 28 | treat_unknown_args_as_files? = 29 | if exec.cli_options && exec.cli_options.command do 30 | command_name = Execution.get_command_name(exec) 31 | command_mod = Execution.get_command(exec, command_name) 32 | 33 | command_mod.treat_unknown_args_as_files?() 34 | else 35 | false 36 | end 37 | 38 | cli_options = 39 | Options.parse( 40 | use_strict_parser?, 41 | exec.argv, 42 | File.cwd!(), 43 | command_names, 44 | given_command_name, 45 | [UI.edge()], 46 | exec.cli_switches, 47 | cli_aliases, 48 | treat_unknown_args_as_files? 49 | ) 50 | 51 | %{exec | cli_options: cli_options} 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/credo/execution/task/require_requires.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.RequireRequires do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.Sources 7 | 8 | def call(%Execution{requires: requires} = exec, _opts) do 9 | requires 10 | |> Sources.find() 11 | |> Enum.each(&Code.require_file/1) 12 | 13 | exec 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/credo/execution/task/run_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.RunCommand do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.Execution 7 | 8 | @exit_status Credo.CLI.ExitStatus.generic_error() 9 | 10 | def call(exec, opts) do 11 | command_name = Execution.get_command_name(exec) 12 | command_mod = Execution.get_command(exec, command_name) 13 | 14 | command_mod.call(exec, opts) 15 | end 16 | 17 | def error(exec, _opts) do 18 | case get_exit_status(exec) do 19 | 0 -> put_exit_status(exec, @exit_status) 20 | _ -> exec 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/credo/execution/task/set_default_command.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.SetDefaultCommand do 2 | @moduledoc false 3 | 4 | @default_command_name "suggest" 5 | @explain_command_name "explain" 6 | 7 | use Credo.Execution.Task 8 | 9 | alias Credo.CLI.Filename 10 | alias Credo.CLI.Options 11 | 12 | def call(exec, _opts) do 13 | determine_command(exec, exec.cli_options) 14 | end 15 | 16 | defp determine_command(exec, %Options{command: nil, args: args} = options) do 17 | potential_path = List.first(args) 18 | 19 | if Filename.contains_line_no?(potential_path) do 20 | set_command_and_path(exec, options, @explain_command_name, potential_path) 21 | else 22 | set_command_and_path(exec, options, @default_command_name, options.path) 23 | end 24 | end 25 | 26 | defp determine_command(exec, _options), do: exec 27 | 28 | defp set_command_and_path(exec, options, command, path) do 29 | %{ 30 | exec 31 | | cli_options: %{ 32 | options 33 | | command: command, 34 | path: path 35 | } 36 | } 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/credo/execution/task/use_colors.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.UseColors do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Output.UI 7 | 8 | def call(exec, _opts) do 9 | UI.use_colors(exec) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/credo/execution/task/validate_options.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Execution.Task.ValidateOptions do 2 | @moduledoc false 3 | 4 | use Credo.Execution.Task 5 | 6 | alias Credo.CLI.Options 7 | alias Credo.CLI.Output.UI 8 | 9 | @exit_status Credo.CLI.ExitStatus.config_loaded_but_invalid() 10 | 11 | def call(exec, _opts) do 12 | case exec.cli_options do 13 | %Options{unknown_args: [], unknown_switches: []} -> 14 | exec 15 | 16 | _ -> 17 | Execution.halt(exec) 18 | end 19 | end 20 | 21 | @spec error(Credo.Execution.t(), keyword()) :: no_return 22 | def error(exec, _opts) do 23 | UI.use_colors(exec) 24 | 25 | Enum.each(exec.cli_options.unknown_args, &print_argument(exec, &1)) 26 | Enum.each(exec.cli_options.unknown_switches, &print_switch(exec, &1)) 27 | 28 | put_exit_status(exec, @exit_status) 29 | end 30 | 31 | defp print_argument(exec, name) do 32 | UI.warn([ 33 | :red, 34 | "** (credo) Unknown argument for `#{exec.cli_options.command}` command: #{name}" 35 | ]) 36 | end 37 | 38 | defp print_switch(exec, {name, _value}), do: print_switch(exec, name) 39 | 40 | defp print_switch(exec, name) do 41 | UI.warn([ 42 | :red, 43 | "** (credo) Unknown switch for `#{exec.cli_options.command}` command: #{name}" 44 | ]) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/credo/issue.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Issue do 2 | @moduledoc """ 3 | `Issue` structs represent all issues found during the code analysis. 4 | """ 5 | 6 | @type t :: %__MODULE__{} 7 | 8 | defstruct check: nil, 9 | category: nil, 10 | priority: 0, 11 | severity: nil, 12 | message: nil, 13 | filename: nil, 14 | line_no: nil, 15 | column: nil, 16 | exit_status: 0, 17 | # optional: the String that triggered the check to fail 18 | trigger: nil, 19 | # optional: whether the issue is old, new or fixed 20 | diff_marker: nil, 21 | # optional: metadata filled in by the check 22 | meta: [], 23 | # optional: the name of the module, macro or 24 | # function where the issue was found 25 | scope: nil 26 | 27 | @doc """ 28 | A value that be assigned to `:trigger` if there is no actual trigger. 29 | """ 30 | def no_trigger, do: {:__no_trigger__} 31 | end 32 | -------------------------------------------------------------------------------- /lib/credo/issue_meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.IssueMeta do 2 | @moduledoc """ 3 | IssueMeta provides helper functions for meta information which a check wants 4 | to pass to the `issue_for(...)` function, i.e. the current SourceFile and check 5 | params (by default). 6 | """ 7 | 8 | @type t :: {__MODULE__, Credo.SourceFile.t(), Keyword.t()} 9 | 10 | alias Credo.SourceFile 11 | 12 | @doc "Returns an issue meta tuple for the given `source_file` and `check_params`." 13 | def for(source_file, check_params) do 14 | {__MODULE__, source_file, check_params} 15 | end 16 | 17 | @doc "Returns the source file for the given `issue_meta`." 18 | def source_file(issue_meta) 19 | 20 | def source_file({__MODULE__, source_file, _params}) do 21 | source_file 22 | end 23 | 24 | def source_file(%SourceFile{} = source_file) do 25 | source_file 26 | end 27 | 28 | @doc "Returns the check params for the given `issue_meta`." 29 | def params(issue_meta) 30 | 31 | def params({__MODULE__, _source_file, check_params}), do: check_params 32 | def params(%SourceFile{}), do: [] 33 | end 34 | -------------------------------------------------------------------------------- /lib/credo/service/config_files.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.ConfigFiles do 2 | @moduledoc false 3 | 4 | use Credo.Service.ETSTableHelper 5 | end 6 | -------------------------------------------------------------------------------- /lib/credo/service/ets_table_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.ETSTableHelper do 2 | @moduledoc false 3 | 4 | defmacro __using__(_opts \\ []) do 5 | quote do 6 | use GenServer 7 | 8 | @timeout 60_000 9 | 10 | alias Credo.Service.ETSTableHelper 11 | 12 | @table_name __MODULE__ 13 | 14 | def start_link(opts \\ []) do 15 | {:ok, _pid} = GenServer.start_link(__MODULE__, opts, name: __MODULE__) 16 | end 17 | 18 | def get(source_file) do 19 | hash = source_file.hash 20 | 21 | case :ets.lookup(@table_name, hash) do 22 | [{^hash, value}] -> 23 | {:ok, value} 24 | 25 | [] -> 26 | :notfound 27 | end 28 | end 29 | 30 | def put(source_file, value) do 31 | GenServer.call(__MODULE__, {:put, source_file.hash, value}, @timeout) 32 | end 33 | 34 | # callbacks 35 | 36 | def init(opts), do: ETSTableHelper.init(@table_name, opts) 37 | 38 | def handle_call(msg, from, current_state), 39 | do: ETSTableHelper.handle_call(@table_name, msg, from, current_state) 40 | end 41 | end 42 | 43 | def init(table_name, _) do 44 | ets = :ets.new(table_name, [:named_table, read_concurrency: true]) 45 | 46 | {:ok, ets} 47 | end 48 | 49 | def handle_call(table_name, {:put, hash, value}, _from, current_state) do 50 | :ets.insert(table_name, {hash, value}) 51 | 52 | {:reply, value, current_state} 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/credo/service/source_file_ast.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.SourceFileAST do 2 | @moduledoc false 3 | 4 | use Credo.Service.ETSTableHelper 5 | end 6 | -------------------------------------------------------------------------------- /lib/credo/service/source_file_lines.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.SourceFileLines do 2 | @moduledoc false 3 | 4 | use Credo.Service.ETSTableHelper 5 | end 6 | -------------------------------------------------------------------------------- /lib/credo/service/source_file_scope_priorities.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.SourceFileScopePriorities do 2 | @moduledoc false 3 | 4 | use Credo.Service.ETSTableHelper 5 | end 6 | -------------------------------------------------------------------------------- /lib/credo/service/source_file_scopes.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.SourceFileScopes do 2 | @moduledoc false 3 | 4 | use Credo.Service.ETSTableHelper 5 | end 6 | -------------------------------------------------------------------------------- /lib/credo/service/source_file_source.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Service.SourceFileSource do 2 | @moduledoc false 3 | 4 | use Credo.Service.ETSTableHelper 5 | end 6 | -------------------------------------------------------------------------------- /lib/credo/severity.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Severity do 2 | @moduledoc false 3 | 4 | def default_value, do: 1 5 | 6 | def compute(_, 0), do: 65_536 7 | def compute(value, max_value), do: value / max_value 8 | end 9 | -------------------------------------------------------------------------------- /lib/credo/test/assertions.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Test.Assertions do 2 | @moduledoc false 3 | 4 | import ExUnit.Assertions 5 | 6 | def assert_trigger(issue, trigger) 7 | 8 | def assert_trigger([issue], trigger), do: [assert_trigger(issue, trigger)] 9 | 10 | def assert_trigger(issue, trigger) do 11 | assert trigger == issue.trigger 12 | 13 | issue 14 | end 15 | 16 | def refute_issues(issues) do 17 | assert [] == issues, 18 | "There should be no issues, got #{Enum.count(issues)}:\n\n#{to_inspected(issues)}" 19 | 20 | issues 21 | end 22 | 23 | def assert_issue(issues, callback \\ nil) do 24 | refute Enum.empty?(issues), "There should be one issue, got none." 25 | 26 | assert Enum.count(issues) == 1, 27 | "There should be only 1 issue, got #{Enum.count(issues)}:\n\n#{to_inspected(issues)}" 28 | 29 | if callback do 30 | issues |> List.first() |> callback.() 31 | end 32 | 33 | issues 34 | end 35 | 36 | def assert_issues(issues, callback \\ nil) do 37 | assert Enum.count(issues) > 0, "There should be multiple issues, got none." 38 | 39 | assert Enum.count(issues) > 1, 40 | "There should be more than one issue, got:\n\n#{to_inspected(issues)}" 41 | 42 | if callback, do: callback.(issues) 43 | 44 | issues 45 | end 46 | 47 | def to_inspected(value) when is_list(value) do 48 | value |> Enum.map(&to_inspected/1) |> Enum.join("\n") 49 | end 50 | 51 | def to_inspected(%Credo.Issue{} = issue) do 52 | inspected = 53 | issue 54 | |> Inspect.Algebra.to_doc(%Inspect.Opts{}) 55 | |> Inspect.Algebra.format(50) 56 | |> Enum.join("") 57 | 58 | if Credo.Test.Case.test_source_files?() do 59 | """ 60 | #{inspected} 61 | 62 | #{Credo.Test.Case.get_issue_inline(issue)} 63 | """ 64 | else 65 | inspected 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/credo/test/check_runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Test.CheckRunner do 2 | alias Credo.Execution 3 | alias Credo.Execution.ExecutionIssues 4 | 5 | def run_check(source_files, check, params \\ []) do 6 | exec = Execution.build() 7 | 8 | source_files 9 | |> List.wrap() 10 | |> issues_for(check, exec, params) 11 | end 12 | 13 | defp issues_for(source_files, check, exec, params) 14 | when is_list(source_files) do 15 | :ok = check.run_on_all_source_files(exec, source_files, params) 16 | 17 | Enum.flat_map(source_files, &get_issues_from_source_file(&1, exec)) 18 | end 19 | 20 | defp get_issues_from_source_file(source_file, exec) do 21 | ExecutionIssues.get(exec, source_file) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/credo/test/source_files.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Test.SourceFiles do 2 | def to_source_file(source) do 3 | filename = "test-untitled#{System.unique_integer()}.ex" 4 | 5 | to_source_file(source, filename) 6 | end 7 | 8 | def to_source_file(source, filename) do 9 | case Credo.SourceFile.parse(source, filename) do 10 | %{status: :valid} = source_file -> 11 | source_file 12 | 13 | _ -> 14 | raise "Source could not be parsed!" 15 | end 16 | end 17 | 18 | def to_source_files(list) do 19 | Enum.map(list, &to_source_file/1) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/credo/watcher.ex: -------------------------------------------------------------------------------- 1 | defmodule Credo.Watcher do 2 | alias Credo.CLI.Output.UI 3 | 4 | @default_watch_path "." 5 | 6 | def run(argv) do 7 | spawn(fn -> 8 | path = get_path(argv) 9 | 10 | UI.puts([ 11 | :bright, 12 | :magenta, 13 | "watch: ", 14 | :reset, 15 | :faint, 16 | "Now watching for changes in '#{path}' ...\n" 17 | ]) 18 | 19 | start_watcher_process(path) 20 | 21 | watch_loop(argv, nil) 22 | end) 23 | end 24 | 25 | defp start_watcher_process(path) do 26 | {:ok, pid} = FileSystem.start_link(dirs: [path]) 27 | 28 | FileSystem.subscribe(pid) 29 | end 30 | 31 | defp watch_loop(argv, exec_from_last_run) do 32 | receive do 33 | {:file_event, _worker_pid, {file_path, events}} -> 34 | elixir_file? = file_path =~ ~r/\.exs?$/ 35 | in_ignored_directory? = file_path =~ ~r/\/deps\/$/ 36 | relative_path = Path.relative_to_cwd(file_path) 37 | 38 | exec = 39 | if Enum.member?(events, :closed) && elixir_file? && !in_ignored_directory? do 40 | UI.puts([:bright, :magenta, "watch: ", :reset, :faint, relative_path, "\n"]) 41 | 42 | file_that_changed = Path.relative_to_cwd(relative_path) 43 | 44 | Credo.run(exec_from_last_run || argv, [file_that_changed]) 45 | else 46 | # data = inspect({System.os_time(:milliseconds), events, relative_path}) 47 | # UI.puts([:bright, :magenta, "changes: ", :reset, :faint, data]) 48 | exec_from_last_run 49 | end 50 | 51 | watch_loop(argv, exec) 52 | end 53 | end 54 | 55 | defp get_path([]), do: @default_watch_path 56 | 57 | defp get_path(argv) do 58 | {_options, argv_rest, _errors} = OptionParser.parse(argv, strict: [watch: :boolean]) 59 | 60 | path = Enum.find(argv_rest, fn path -> File.exists?(path) or path =~ ~r/[\?\*]/ end) 61 | 62 | path || @default_watch_path 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/mix/tasks/credo.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Credo do 2 | use Mix.Task 3 | 4 | @shortdoc "Run code analysis (use `--help` for options)" 5 | @moduledoc @shortdoc 6 | 7 | @doc false 8 | def run(argv) do 9 | Credo.CLI.main(argv) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/mix/tasks/credo.gen.check.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Credo.Gen.Check do 2 | use Mix.Task 3 | 4 | @shortdoc "Generate a new custom check for Credo" 5 | @moduledoc @shortdoc 6 | 7 | @doc false 8 | def run(argv) do 9 | Credo.CLI.main(["gen.check"] ++ argv) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/mix/tasks/credo.gen.config.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Credo.Gen.Config do 2 | use Mix.Task 3 | 4 | @shortdoc "Generate a new config for Credo" 5 | @moduledoc @shortdoc 6 | 7 | @doc false 8 | def run(argv) do 9 | Credo.CLI.main(["gen.config"] ++ argv) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/check_formatted.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $(mix help format) ]]; 4 | then 5 | mix format --check-formatted 6 | fi 7 | -------------------------------------------------------------------------------- /test/credo/check/consistency/line_endings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.LineEndingsTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Consistency.LineEndings 5 | 6 | @unix_line_endings ~S""" 7 | defmodule Credo.Sample do 8 | defmodule InlineModule do 9 | def foobar do 10 | {:ok} = File.read 11 | end 12 | end 13 | end 14 | """ 15 | @unix_line_endings2 ~S""" 16 | defmodule OtherModule do 17 | defmacro foo do 18 | {:ok} = File.read 19 | end 20 | 21 | defp bar do 22 | :ok 23 | end 24 | end 25 | """ 26 | @windows_line_endings ~s""" 27 | defmodule Credo.Sample do\r\n@test_attribute :foo\r\nend 28 | """ 29 | 30 | # 31 | # cases NOT raising issues 32 | # 33 | 34 | test "it should NOT report expected code for linux" do 35 | [@unix_line_endings, @unix_line_endings2] 36 | |> to_source_files 37 | |> run_check(@described_check) 38 | |> refute_issues 39 | end 40 | 41 | test "it should NOT report expected code for windows" do 42 | [@windows_line_endings |> String.trim()] 43 | |> to_source_files 44 | |> run_check(@described_check) 45 | |> refute_issues 46 | end 47 | 48 | # 49 | # cases raising issues 50 | # 51 | 52 | test "it should report an issue here" do 53 | [@unix_line_endings, @windows_line_endings] 54 | |> to_source_files 55 | |> run_check(@described_check) 56 | |> assert_issue(fn issue -> 57 | assert issue.trigger == "\r\n" 58 | end) 59 | end 60 | 61 | test "it should report an issue here when there is no problem, but the :force param specifies the other kind of line ending" do 62 | [@windows_line_endings] 63 | |> to_source_files 64 | |> run_check(@described_check, force: :unix) 65 | |> assert_issue(fn issue -> 66 | assert issue.trigger == "\r\n" 67 | end) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/credo/check/consistency/multi_alias_import_require_use/collector_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.MultiAliasImportRequireUse.CollectorTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Check.Consistency.MultiAliasImportRequireUse.Collector 5 | 6 | @single """ 7 | defmodule Credo.Sample1 do 8 | alias Foo.Bar 9 | import Foo.Bar 10 | alias Foo.Baz # this and the first line counts as 1 11 | import Foo.Baz # this and the second line counts as 1 12 | require Foo.Bar 13 | require Foo.Baz # this and the previous line counts as 1 14 | use Foo.Bar 15 | use Foo.Baz, with_params: true # use with params does not count 16 | use Foo # use with single module alias does not count 17 | end 18 | """ 19 | 20 | @multi """ 21 | defmodule Credo.Sample2 do 22 | alias Foo.{Bar, Baz} 23 | import Foo.{Bar, Baz} 24 | require Foo.{Bar, Baz} 25 | use Foo.{Bar, Baz} 26 | end 27 | """ 28 | 29 | @mixed """ 30 | defmodule Credo.Sample2 do 31 | import Foo.Bar 32 | alias Foo.{Bar, Baz} 33 | import Foo.Baz 34 | import Bar.Baz 35 | end 36 | """ 37 | 38 | test "it should report correct frequencies for single imports" do 39 | result = 40 | @single 41 | |> to_source_file() 42 | |> Collector.collect_matches([]) 43 | 44 | assert %{single: 3} == result 45 | end 46 | 47 | test "it should report correct frequencies for multi imports" do 48 | result = 49 | @multi 50 | |> to_source_file() 51 | |> Collector.collect_matches([]) 52 | 53 | assert %{multi: 4} == result 54 | end 55 | 56 | test "it should report correct frequencies for mixed imports" do 57 | result = 58 | @mixed 59 | |> to_source_file() 60 | |> Collector.collect_matches([]) 61 | 62 | assert %{single: 1, multi: 1} == result 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/credo/check/consistency/parameter_pattern_matching/collector_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Consistency.ParameterPatternMatching.CollectorTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Check.Consistency.ParameterPatternMatching.Collector 5 | 6 | @special_cases """ 7 | defmodule SpecialCases do 8 | def foo([bar, _] = baz), do: :ok 9 | defp foo(baz = [bar, _]), do: :ok 10 | 11 | def special(foo = [bar, _]) when is_list(foo), do: :ok 12 | def special([bar, _] = foo) when is_tuple(bar) do 13 | case bar do 14 | status = {:ok, _} -> status 15 | {:error, _} = status -> status 16 | wat = {:wat, n} when n > 0 -> wat 17 | {:wat, n} = wat when is_tuple(wat) -> wat 18 | end 19 | fn 20 | admin = %User{admin: true} -> admin 21 | %User{} = user -> user 22 | wat = {:wat, n} when is_number(n) -> wat 23 | end 24 | end 25 | end 26 | """ 27 | 28 | @tag :to_be_implemented 29 | test "it should report correct frequencies for special cases" do 30 | result = 31 | @special_cases 32 | |> to_source_file() 33 | |> Collector.collect_matches([]) 34 | 35 | assert %{before: 6, after: 5} == result 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/credo/check/design/skip_test_without_comment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Design.SkipTestWithoutCommentTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Design.SkipTestWithoutComment 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report when comment precedes the tag" do 11 | """ 12 | defmodule CredoSampleModuleTest do 13 | alias ExUnit.Case 14 | 15 | # Happy case: Some comment 16 | @tag :skip 17 | test "foo" do 18 | :ok 19 | end 20 | 21 | # Some comment 22 | @tag :skip 23 | # another comment, shouldn't matter 24 | test "foo2" do 25 | :ok 26 | end 27 | end 28 | """ 29 | |> to_source_file("foo_test.exs") 30 | |> run_check(@described_check) 31 | |> refute_issues() 32 | end 33 | 34 | # 35 | # cases raising issues 36 | # 37 | 38 | # @tag :skip 39 | test "it should report a violation" do 40 | """ 41 | defmodule CredoSampleModuleTest do 42 | alias ExUnit.Case 43 | 44 | @tag :skip 45 | test "foo" do 46 | :ok 47 | end 48 | end 49 | """ 50 | |> to_source_file("foo_test.exs") 51 | |> run_check(@described_check) 52 | |> assert_issue(fn issue -> 53 | assert issue.trigger == "@tag :skip" 54 | end) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/credo/check/housekeeping_heredocs_in_tests.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.HousekeepingHeredocsInTestsTest do 2 | use Credo.Test.Case 3 | 4 | @tag housekeeping: :heredocs 5 | test "find triple-double-quote heredocs in check tests" do 6 | errors = 7 | Path.join(__DIR__, "*/**/*_test.exs") 8 | |> Path.wildcard() 9 | |> Enum.reject(&String.match?(&1, ~r/(collector|helper)/)) 10 | |> Enum.map(&{&1, File.read!(&1)}) 11 | |> Enum.flat_map(fn {filename, source} -> 12 | ast = 13 | source 14 | |> Code.string_to_quoted!( 15 | literal_encoder: &{:ok, {:__literal__, &2, [&1]}}, 16 | token_metadata: true 17 | ) 18 | |> Macro.prewalk(fn 19 | {op, params, args} -> {op, Map.new(params), args} 20 | val -> val 21 | end) 22 | 23 | {_ast, acc} = 24 | Macro.prewalk(ast, [], fn 25 | {:__literal__, %{line: line, delimiter: "\"\"\""}, [val]} = ast, acc 26 | when is_binary(val) -> 27 | {ast, ["#{filename}:#{line}"] ++ acc} 28 | 29 | ast, acc -> 30 | {ast, acc} 31 | end) 32 | 33 | acc 34 | end) 35 | 36 | if errors != [] do 37 | flunk( 38 | "Expected to use ~s or ~S heredocs:\n" <> 39 | Enum.join(errors, "\n") 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/credo/check/params_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.ParamsTest do 2 | use Credo.Test.Case 3 | 4 | # This is defined here so the doctest for `get/3` can use this module 5 | defmodule SamepleCheck do 6 | def param_defaults do 7 | [foo: "bar"] 8 | end 9 | end 10 | 11 | doctest Credo.Check.Params 12 | end 13 | -------------------------------------------------------------------------------- /test/credo/check/readability/alias_as_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.AliasAsTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.AliasAs 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report violation" do 11 | """ 12 | defmodule Test do 13 | alias App.Module1 14 | alias App.Module2 15 | end 16 | """ 17 | |> to_source_file 18 | |> run_check(@described_check) 19 | |> refute_issues() 20 | end 21 | 22 | # 23 | # cases raising issues 24 | # 25 | 26 | test "it should report a violation" do 27 | """ 28 | defmodule Test do 29 | alias App.Module1, as: M1 30 | end 31 | """ 32 | |> to_source_file 33 | |> run_check(@described_check) 34 | |> assert_issue(fn issue -> 35 | assert issue.trigger == "as:" 36 | end) 37 | end 38 | 39 | test "it should report multiple violations" do 40 | [issue1, issue2, issue3] = 41 | """ 42 | defmodule Test do 43 | alias App.Module1, as: M1 44 | alias App.Module2 45 | alias App.Module3, as: M3 46 | alias App.Module4, as: M4 47 | end 48 | """ 49 | |> to_source_file 50 | |> run_check(@described_check) 51 | |> assert_issues() 52 | 53 | assert issue1.trigger == "as:" 54 | assert issue2.trigger == "as:" 55 | assert issue3.trigger == "as:" 56 | end 57 | 58 | test "it should report on alias __MODULE__, as: Foo" do 59 | """ 60 | defmodule Test do 61 | alias __MODULE__, as: Foo 62 | end 63 | """ 64 | |> to_source_file 65 | |> run_check(@described_check) 66 | |> assert_issue() 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/credo/check/readability/impl_true_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.ImplTrueTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.ImplTrue 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report @impl Behaviour" do 11 | """ 12 | defmodule CredoImplTrueTest do 13 | @behaviour MyBehaviour 14 | 15 | @impl MyBehaviour 16 | def foo, do: :bar 17 | end 18 | """ 19 | |> to_source_file() 20 | |> run_check(@described_check) 21 | |> refute_issues() 22 | end 23 | 24 | test "it should NOT report when no @impl found" do 25 | """ 26 | defmodule CredoImplTrueTest do 27 | def foo, do: :bar 28 | end 29 | """ 30 | |> to_source_file() 31 | |> run_check(@described_check) 32 | |> refute_issues() 33 | end 34 | 35 | # 36 | # cases raising issues 37 | # 38 | 39 | test "it should report @impl true" do 40 | """ 41 | defmodule CredoTypespecTest do 42 | @impl true 43 | def foo, do: :bar 44 | end 45 | """ 46 | |> to_source_file() 47 | |> run_check(@described_check) 48 | |> assert_issue(fn issue -> 49 | assert issue.trigger == "@impl" 50 | end) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/credo/check/readability/module_attribute_names_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.ModuleAttributeNamesTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.ModuleAttributeNames 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | defmodule CredoSampleModule do 13 | @some_foobar 14 | end 15 | """ 16 | |> to_source_file 17 | |> run_check(@described_check) 18 | |> refute_issues() 19 | end 20 | 21 | test "it should NOT fail on a dynamic attribute" do 22 | """ 23 | defmodule CredoSampleModule do 24 | defmacro define(key, value) do 25 | quote do 26 | @unquote(key)(unquote(value)) 27 | end 28 | end 29 | end 30 | """ 31 | |> to_source_file 32 | |> run_check(@described_check) 33 | |> refute_issues() 34 | end 35 | 36 | test "it should NOT fail when redefining the @ operator" do 37 | """ 38 | defmodule CredoSampleModule do 39 | defmacro @{_, _, _} do 40 | quote do 41 | # some_code_here 42 | end 43 | end 44 | end 45 | """ 46 | |> to_source_file 47 | |> run_check(@described_check) 48 | |> refute_issues() 49 | end 50 | 51 | # 52 | # cases raising issues 53 | # 54 | 55 | test "it should report a violation" do 56 | """ 57 | defmodule CredoSampleModule do 58 | @someFoobar false 59 | end 60 | """ 61 | |> to_source_file 62 | |> run_check(@described_check) 63 | |> assert_issue(fn issue -> 64 | assert issue.line_no == 2 65 | assert issue.trigger == "@someFoobar" 66 | end) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/credo/check/readability/multi_alias_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.MultiAliasTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.MultiAlias 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report violation" do 11 | """ 12 | defmodule Test do 13 | alias App.Module1 14 | alias App.Module2 15 | end 16 | """ 17 | |> to_source_file 18 | |> run_check(@described_check) 19 | |> refute_issues() 20 | end 21 | 22 | # 23 | # cases raising issues 24 | # 25 | 26 | test "it should report a violation" do 27 | """ 28 | defmodule CredoSampleModule do 29 | alias App.Module2.{Module3} 30 | end 31 | """ 32 | |> to_source_file 33 | |> run_check(@described_check) 34 | |> assert_issue() 35 | end 36 | 37 | test "it should report a violation for multiple expansions" do 38 | """ 39 | defmodule CredoSampleModule do 40 | alias App.{Module1, Module2} 41 | end 42 | """ 43 | |> to_source_file 44 | |> run_check(@described_check) 45 | |> assert_issue() 46 | end 47 | 48 | test "it should report a violation for multiple expansions with __MODULE__ macro" do 49 | """ 50 | defmodule CredoSampleModule do 51 | alias __MODULE__.{Module1, Module2} 52 | end 53 | """ 54 | |> to_source_file 55 | |> run_check(@described_check) 56 | |> assert_issue() 57 | end 58 | 59 | test "it should report a violation for multiple nested expansions" do 60 | """ 61 | defmodule CredoSampleModule do 62 | alias App.{Module1.Submodule1, Module2} 63 | end 64 | """ 65 | |> to_source_file 66 | |> run_check(@described_check) 67 | |> assert_issue(fn issue -> 68 | assert issue.trigger == "Module1.Submodule1" 69 | end) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/credo/check/readability/one_arity_function_in_pipe_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.OneArityFunctionInPipeTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.OneArityFunctionInPipe 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report violation for a valid pipe" do 11 | """ 12 | defmodule Test do 13 | def some_function(arg) do 14 | arg |> foo() |> bar() 15 | end 16 | 17 | def other_function(arg) do 18 | arg 19 | |> foo() 20 | |> bar() 21 | end 22 | end 23 | """ 24 | |> to_source_file 25 | |> run_check(@described_check) 26 | |> refute_issues() 27 | end 28 | 29 | test "it should NOT report violation for a valid pipe with a block" do 30 | """ 31 | defmodule Test do 32 | def other_function(arg) do 33 | arg 34 | |> foo() 35 | |> case do 36 | :x -> :y 37 | :u -> :u 38 | end 39 | |> bar() 40 | end 41 | end 42 | """ 43 | |> to_source_file 44 | |> run_check(@described_check) 45 | |> refute_issues() 46 | end 47 | 48 | # 49 | # cases raising issues 50 | # 51 | 52 | test "it should report a violation for missing parentheses" do 53 | """ 54 | defmodule Test do 55 | def some_function(arg) do 56 | arg 57 | |> foo() 58 | |> bar 59 | |> baz() 60 | end 61 | end 62 | """ 63 | |> to_source_file 64 | |> run_check(@described_check) 65 | |> assert_issue(fn issue -> 66 | assert issue.trigger == "bar" 67 | end) 68 | end 69 | 70 | test "it should report violations for missing parentheses" do 71 | """ 72 | defmodule Test do 73 | def some_function(arg) do 74 | arg 75 | |> foo 76 | |> bar 77 | end 78 | end 79 | """ 80 | |> to_source_file 81 | |> run_check(@described_check) 82 | |> assert_issues() 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/credo/check/readability/one_pipe_per_line_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.OnePipePerLineTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.OnePipePerLine 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report the expected code" do 11 | """ 12 | defmodule CredoSampleModule do 13 | use ExUnit.Case 14 | 15 | def some_fun do 16 | 1 17 | |> Integer.to_string() 18 | |> String.to_integer() 19 | end 20 | end 21 | """ 22 | |> to_source_file 23 | |> run_check(@described_check) 24 | |> refute_issues() 25 | end 26 | 27 | # 28 | # cases raising issues 29 | # 30 | 31 | test "it should report a violation that includes rejected module attrs" do 32 | """ 33 | defmodule CredoSampleModule do 34 | use ExUnit.Case 35 | 36 | def some_fun do 37 | 1 |> Integer.to_string() |> String.to_integer() 38 | end 39 | end 40 | """ 41 | |> to_source_file() 42 | |> run_check(@described_check) 43 | |> assert_issue(fn issue -> 44 | assert issue.trigger == "|>" 45 | end) 46 | end 47 | 48 | test "it should report multiple violations when having multiples pipes" do 49 | """ 50 | defmodule CredoSampleModule do 51 | use ExUnit.Case 52 | 53 | def some_fun do 54 | "1" |> String.to_integer() |> Integer.to_string() 55 | 1 |> Integer.to_string() |> String.to_integer() 56 | end 57 | end 58 | """ 59 | |> to_source_file() 60 | |> run_check(@described_check) 61 | |> assert_issues() 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/credo/check/readability/pipe_into_anonymous_functions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.PipeIntoAnonymousFunctionsTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.PipeIntoAnonymousFunctions 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | defmodule CredoSampleModule do 13 | use ExUnit.Case 14 | 15 | def some_fun do 16 | some_val 17 | |> do_something 18 | |> do_something_else 19 | end 20 | end 21 | """ 22 | |> to_source_file 23 | |> run_check(@described_check) 24 | |> refute_issues() 25 | end 26 | 27 | # 28 | # cases raising issues 29 | # 30 | 31 | test "it should report a violation" do 32 | """ 33 | defmodule CredoSampleModule do 34 | use ExUnit.Case 35 | 36 | def some_fun do 37 | some_val 38 | |> (fn x -> x * 2 end).() 39 | |> do_something 40 | end 41 | end 42 | """ 43 | |> to_source_file 44 | |> run_check(@described_check) 45 | |> assert_issue(fn issue -> 46 | assert issue.trigger == "|>" 47 | end) 48 | end 49 | 50 | test "it should report a violation for multiple violations" do 51 | """ 52 | defmodule CredoSampleModule do 53 | use ExUnit.Case 54 | 55 | def some_fun do 56 | some_val 57 | |> (fn x -> x * 2 end).() 58 | |> (fn x -> x * 2 end).() 59 | end 60 | end 61 | """ 62 | |> to_source_file 63 | |> run_check(@described_check) 64 | |> assert_issues() 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/credo/check/readability/semicolons_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.SemicolonsTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.Semicolons 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | def fun_name do 13 | statement1 14 | statement2 15 | end 16 | """ 17 | |> to_source_file 18 | |> run_check(@described_check) 19 | |> refute_issues() 20 | end 21 | 22 | # 23 | # cases raising issues 24 | # 25 | 26 | test "it should report a violation" do 27 | """ 28 | def fun_name() do 29 | statement1; statement2 30 | end 31 | """ 32 | |> to_source_file 33 | |> run_check(@described_check) 34 | |> assert_issue(fn issue -> 35 | assert issue.trigger == ";" 36 | end) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/credo/check/readability/trailing_blank_line_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.TrailingBlankLineTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.TrailingBlankLine 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | defmodule CredoSampleModule do 13 | end 14 | """ 15 | |> to_source_file 16 | |> run_check(@described_check) 17 | |> refute_issues() 18 | end 19 | 20 | # 21 | # cases raising issues 22 | # 23 | 24 | test "it should report a violation" do 25 | "defmodule CredoSampleModule do\nend" 26 | |> to_source_file 27 | |> run_check(@described_check) 28 | |> assert_issue(fn issue -> 29 | assert issue.trigger == Credo.Issue.no_trigger() 30 | end) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/credo/check/readability/with_custom_tagged_tuple_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Readability.WithCustomTaggedTupleTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Readability.WithCustomTaggedTuple 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report violation" do 11 | """ 12 | defmodule Test do 13 | def run(user, resource) do 14 | with {:ok, resource} <- Resource.fetch(user), 15 | :ok <- Resource.authorize(resource, user), 16 | do: SomeMod.do_something(resource) 17 | end 18 | end 19 | """ 20 | |> to_source_file() 21 | |> run_check(@described_check) 22 | |> refute_issues() 23 | end 24 | 25 | # 26 | # cases raising issues 27 | # 28 | 29 | test "it should report a violation" do 30 | """ 31 | defmodule Test do 32 | def run(user, resource) do 33 | with {:resource, {:ok, resource}} <- {:resource, Resource.fetch(user)}, 34 | {:authz, :ok} <- {:authz, Resource.authorize(resource, user)} do 35 | SomeMod.do_something(resource) 36 | else 37 | {:resource, _} -> {:error, :not_found} 38 | {:authz, _} -> {:error, :unauthorized} 39 | end 40 | end 41 | end 42 | """ 43 | |> to_source_file() 44 | |> run_check(@described_check) 45 | |> assert_issues(fn issues -> 46 | [issue1, issue2] = issues 47 | 48 | assert issue1.message == 49 | "Avoid using tagged tuples as placeholders in `with` (found: `:resource`)." 50 | 51 | assert issue1.trigger == ":resource" 52 | 53 | assert issue2.message == 54 | "Avoid using tagged tuples as placeholders in `with` (found: `:authz`)." 55 | 56 | assert issue2.trigger == ":authz" 57 | end) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/credo/check/refactor/case_trivial_matches_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.CaseTrivialMatchesTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Refactor.CaseTrivialMatches 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | defmodule Credo.Sample.Module do 13 | def some_function(p1, p2, p3, p4, p5) do 14 | case some_value do 15 | true -> :one 16 | false -> :three 17 | nil -> :four 18 | _ -> :something_else 19 | end 20 | end 21 | end 22 | """ 23 | |> to_source_file 24 | |> run_check(@described_check) 25 | |> refute_issues() 26 | end 27 | 28 | test "it should NOT report expected code 2" do 29 | """ 30 | defmodule Credo.Sample.Module do 31 | def some_function(p1, p2, p3, p4, p5) do 32 | case some_value do 33 | ^p1 -> :one 34 | ^p2 -> :two 35 | end 36 | end 37 | end 38 | """ 39 | |> to_source_file 40 | |> run_check(@described_check) 41 | |> refute_issues() 42 | end 43 | 44 | # 45 | # cases raising issues 46 | # 47 | 48 | test "it should report a violation" do 49 | """ 50 | defmodule Credo.Sample.Module do 51 | def some_function(p1, p2, p3, p4, p5, p6) do 52 | case some_value do 53 | true -> :one 54 | false -> :three 55 | end 56 | end 57 | end 58 | """ 59 | |> to_source_file 60 | |> run_check(@described_check) 61 | |> assert_issue() 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/credo/check/refactor/module_size_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.ModuleSizeTest do 2 | use Credo.Test.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/credo/check/refactor/perceived_complexity_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.PerceivedComplexityTest do 2 | use Credo.Test.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/credo/check/refactor/regex_multiple_spaces_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.RegexMultipleSpacesTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Refactor.RegexMultipleSpaces 5 | 6 | @moduletag :to_be_implemented 7 | 8 | # 9 | # cases NOT raising issues 10 | # 11 | 12 | test "it should NOT report expected code" do 13 | """ 14 | defmodule CredoSampleModule do 15 | def my_fun do 16 | regex = ~r/foo {3}bar/ 17 | end 18 | end 19 | """ 20 | |> to_source_file 21 | |> run_check(@described_check) 22 | |> refute_issues() 23 | end 24 | 25 | test "it should NOT report expected code /2" do 26 | """ 27 | defmodule CredoSampleModule do 28 | def some_function(parameter1, parameter2) do 29 | regex = ~r/foo bar/ 30 | end 31 | end 32 | """ 33 | |> to_source_file 34 | |> run_check(@described_check) 35 | |> refute_issues() 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/credo/check/refactor/unless_with_else_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Refactor.UnlessWithElseTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Refactor.UnlessWithElse 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | defmodule CredoSampleModule do 13 | def some_function(parameter1, parameter2) do 14 | unless allowed? do 15 | something 16 | end 17 | if allowed? do 18 | something 19 | else 20 | something_else 21 | end 22 | end 23 | end 24 | """ 25 | |> to_source_file 26 | |> run_check(@described_check) 27 | |> refute_issues() 28 | end 29 | 30 | # 31 | # cases raising issues 32 | # 33 | 34 | test "it should report a violation" do 35 | """ 36 | defmodule CredoSampleModule do 37 | def some_function(parameter1, parameter2) do 38 | unless allowed? do 39 | something 40 | else 41 | something_else 42 | end 43 | end 44 | end 45 | """ 46 | |> to_source_file 47 | |> run_check(@described_check) 48 | |> assert_issue(fn issue -> 49 | assert issue.line_no == 3 50 | assert issue.trigger == "unless" 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/credo/check/runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.RunnerTest do 2 | end 3 | -------------------------------------------------------------------------------- /test/credo/check/warning/iex_pry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.IExPryTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Warning.IExPry 5 | 6 | # 7 | # cases NOT raising issues 8 | # 9 | 10 | test "it should NOT report expected code" do 11 | """ 12 | defmodule CredoSampleModule do 13 | def some_function(parameter1, parameter2) do 14 | parameter1 + parameter2 15 | end 16 | end 17 | """ 18 | |> to_source_file 19 | |> run_check(@described_check) 20 | |> refute_issues() 21 | end 22 | 23 | # 24 | # cases raising issues 25 | # 26 | 27 | test "it should report a violation" do 28 | """ 29 | defmodule CredoSampleModule do 30 | def some_function(parameter1, parameter2) do 31 | x = parameter1 + parameter2 32 | IEx.pry 33 | end 34 | end 35 | """ 36 | |> to_source_file 37 | |> run_check(@described_check) 38 | |> assert_issue(fn issue -> 39 | assert issue.line_no == 4 40 | assert issue.trigger == "IEx.pry" 41 | end) 42 | end 43 | 44 | test "it should report a violation with two on the same line" do 45 | """ 46 | defmodule CredoSampleModule do 47 | def some_function(parameter1, parameter2) do 48 | IEx.pry(); IEx.pry() 49 | end 50 | end 51 | """ 52 | |> to_source_file 53 | |> run_check(@described_check) 54 | |> assert_issues(fn [two, one] -> 55 | assert one.line_no == 3 56 | assert one.column == 5 57 | assert two.line_no == 3 58 | assert two.column == 16 59 | end) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/credo/check/warning/unreachable_code_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.UnreachableCodeTest do 2 | use Credo.Test.Case 3 | 4 | @described_check Credo.Check.Warning.UnreachableCode 5 | 6 | @moduletag :to_be_implemented 7 | 8 | # 9 | # cases NOT raising issues 10 | # 11 | 12 | test "it should NOT report expected code" do 13 | """ 14 | defmodule CredoSampleModule do 15 | def some_function(parameter1, parameter2) do 16 | parameter1 + parameter2 17 | raise "error!!!" 18 | end 19 | end 20 | """ 21 | |> to_source_file 22 | |> run_check(@described_check) 23 | |> refute_issues() 24 | end 25 | 26 | test "it should NOT report expected code /2" do 27 | """ 28 | defmodule CredoSampleModule do 29 | def some_function(parameter1, parameter2) do 30 | if something? do 31 | parameter2 32 | else 33 | parameter1 + parameter2 34 | raise "error!!!" 35 | end 36 | end 37 | end 38 | """ 39 | |> to_source_file 40 | |> run_check(@described_check) 41 | |> refute_issues() 42 | end 43 | 44 | # 45 | # cases raising issues 46 | # 47 | 48 | test "it should report a violation" do 49 | """ 50 | defmodule CredoSampleModule do 51 | def some_function(parameter1, parameter2) do 52 | if something? do 53 | parameter2 54 | else 55 | raise "error!!!" 56 | parameter1 + parameter2 57 | end 58 | end 59 | end 60 | """ 61 | |> to_source_file 62 | |> run_check(@described_check) 63 | |> assert_issue() 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/credo/check/warning/wrong_test_file_extension_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Check.Warning.WrongTestFileExtensionTest do 2 | use Credo.Test.Case 3 | 4 | # we can not test this check, because it uses Credo's `:files`/`:included` 5 | # param to only run on misnamed files :( 6 | end 7 | -------------------------------------------------------------------------------- /test/credo/cli/command/gen.check_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Command.GenCheckTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.CLI.Command.GenCheck 5 | 6 | test "it should work" do 7 | expected = "SomeCustom42Check" 8 | 9 | assert expected == "../ecto/lib/some_custom_42_check.ex" |> GenCheck.check_name_for() 10 | 11 | assert expected == "lib/some_custom_42_check.ex" |> GenCheck.check_name_for() 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/credo/cli/filename_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.FilenameTest do 2 | use ExUnit.Case 3 | doctest Credo.CLI.Filename 4 | 5 | test "the truth" do 6 | assert "C:/Credo/sources.ex" == 7 | Credo.CLI.Filename.remove_line_no_and_column("C:/Credo/sources.ex:39:8") 8 | end 9 | 10 | test "contains line no" do 11 | assert true == Credo.CLI.Filename.contains_line_no?("C:/Credo/sources.ex:39:8") 12 | 13 | assert true == Credo.CLI.Filename.contains_line_no?("C:/Credo/sources.ex:39") 14 | 15 | assert false == Credo.CLI.Filename.contains_line_no?("C:/Credo/sources.ex") 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/credo/cli/output/formatter/json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Output.Formatter.JsonTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.CLI.Output.Formatter.JSON 5 | 6 | test "print_map/1 does not raise when map contains a regex" do 7 | JSON.print_map(%{"option" => ~r/foo/}) 8 | end 9 | 10 | test "prepare_for_json/1 converts keys and values invalid in json" do 11 | assert JSON.prepare_for_json(%{ 12 | "bool" => true, 13 | "list" => ["a", %{"a" => "b", "b" => ~r/foo/}], 14 | "map" => %{"c" => "d", "e" => ["f", 8, ~r/foo/]}, 15 | "number" => 5, 16 | "regex" => ~r/foo/, 17 | "string" => "a", 18 | "tuple" => {"a", 2, ~r/foo/}, 19 | :atom_key => 0, 20 | {"tuple", "key"} => 1, 21 | 0 => 2 22 | }) == %{ 23 | "bool" => true, 24 | "list" => ["a", %{"a" => "b", "b" => "~r/foo/"}], 25 | "map" => %{"c" => "d", "e" => ["f", 8, "~r/foo/"]}, 26 | "number" => 5, 27 | "regex" => "~r/foo/", 28 | "string" => "a", 29 | "tuple" => ["a", 2, "~r/foo/"], 30 | :atom_key => 0, 31 | ~s({"tuple", "key"}) => 1, 32 | 0 => 2 33 | } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/credo/cli/output/output_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.OutputTest do 2 | use Credo.Test.Case 3 | 4 | doctest Credo.CLI.Output 5 | end 6 | -------------------------------------------------------------------------------- /test/credo/cli/output/summary_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.Output.SummaryTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.CLI.Output.Summary 5 | alias Credo.Execution 6 | 7 | doctest Credo.CLI.Output.Summary 8 | 9 | test "print/4 it does not blow up on an empty umbrella project" do 10 | exec = Execution.build() 11 | 12 | Summary.print([], exec, 0, 0) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/credo/cli/sorter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLI.SorterTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.CLI.Sorter 5 | 6 | test "it should work" do 7 | starting_order = [:a, :b] 8 | list = [1, 2, :a, 3, 4, :b, 5, 6] 9 | expected = [:a, :b, 1, 2, 3, 4, 5, 6] 10 | assert expected == list |> Sorter.to_start(starting_order) 11 | end 12 | 13 | test "it should work when items in starting_order are missing in list" do 14 | starting_order = [:a, :b, :i_dont_exists] 15 | list = [1, 2, :a, 3, 4, :b, 5, 6] 16 | expected = [:a, :b, 1, 2, 3, 4, 5, 6] 17 | assert expected == list |> Sorter.to_start(starting_order) 18 | end 19 | 20 | test "it should work with ending_order" do 21 | ending_order = [3, 2, 1] 22 | list = [1, 2, :a, 3, 4, :b, 5, 6] 23 | expected = [:a, 4, :b, 5, 6, 3, 2, 1] 24 | assert expected == list |> Sorter.to_end(ending_order) 25 | end 26 | 27 | test "it should work when items in ending_order are missing in list" do 28 | ending_order = [3, 2, 1, :i_dont_exists] 29 | list = [1, 2, :a, 3, 4, :b, 5, 6] 30 | expected = [:a, 4, :b, 5, 6, 3, 2, 1] 31 | assert expected == list |> Sorter.to_end(ending_order) 32 | end 33 | 34 | test "it should work with start and end" do 35 | starting_order = [:a, :b, :i_dont_exists] 36 | ending_order = [3, 2, 1] 37 | list = [1, 2, :a, 3, 4, :b, 5, 6] 38 | expected = [:a, :b, 4, 5, 6, 3, 2, 1] 39 | assert expected == list |> Sorter.ensure(starting_order, ending_order) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/credo/cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CLITest do 2 | use Credo.Test.Case 3 | 4 | @moduletag slow: :integration 5 | 6 | doctest Credo.CLI 7 | end 8 | -------------------------------------------------------------------------------- /test/credo/config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.ConfigTest do 2 | use ExUnit.Case 3 | 4 | # alias Credo.Execution 5 | end 6 | -------------------------------------------------------------------------------- /test/credo/exs_loader_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.ExsLoaderTest do 2 | use ExUnit.Case 3 | 4 | @exec %Credo.Execution{} 5 | 6 | test "Credo.Execution.parse_exs should work" do 7 | exs_string = """ 8 | %{combine: {:hex, :combine, "0.5.2"}, 9 | cowboy: {:hex, :cowboy, "1.0.2"}, 10 | dirs: ["lib", "src", "test"], 11 | dirs_sigil: ~w(lib src test), 12 | dirs_regex: ~r(lib src test), 13 | checks: [ 14 | {Style.MaxLineLength, max_length: 100}, 15 | {Style.TrailingBlankLine}, 16 | ] 17 | } 18 | """ 19 | 20 | expected = %{ 21 | combine: {:hex, :combine, "0.5.2"}, 22 | cowboy: {:hex, :cowboy, "1.0.2"}, 23 | dirs: ["lib", "src", "test"], 24 | dirs_sigil: ~w(lib src test), 25 | dirs_regex: ~r(lib src test), 26 | checks: [ 27 | {Style.MaxLineLength, max_length: 100}, 28 | {Style.TrailingBlankLine} 29 | ] 30 | } 31 | 32 | assert {:ok, expected} == Credo.ExsLoader.parse(exs_string, "testfile.ex", @exec, true) 33 | assert {:ok, expected} == Credo.ExsLoader.parse(exs_string, "testfile.ex", @exec, false) 34 | end 35 | 36 | test "Credo.Execution.parse_exs should return error tuple" do 37 | exs_string = """ 38 | %{ 39 | configs: [ 40 | %{ 41 | name: "default", 42 | files: %{ 43 | included: ["lib/", "src/", "web/", "apps/"], 44 | excluded: [] 45 | } 46 | checks: [ 47 | {Credo.Check.Readability.ModuleDoc, false} 48 | ] 49 | } 50 | ] 51 | } 52 | """ 53 | 54 | expected = {:error, {9, "syntax error before: ", "checks"}} 55 | 56 | assert expected == Credo.ExsLoader.parse(exs_string, "testfile.ex", @exec, true) 57 | assert expected == Credo.ExsLoader.parse(exs_string, "testfile.ex", @exec, false) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/credo/plugin_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.PluginTest do 2 | use Credo.Test.Case, async: false 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @integration_path "test/fixtures/example_plugin_integration" 7 | 8 | alias Credo.Execution 9 | 10 | Code.require_file("test/fixtures/example_plugin_integration/plugin/example_plugin.ex") 11 | 12 | test "it should use ExamplePlugin's default config" do 13 | File.cd!(@integration_path, fn -> 14 | exec = IntegrationTest.run(["example", "--config-file", ".credo.exs", "-D"]) 15 | 16 | {checks, _only_matching, _ignore_matching} = Execution.checks(exec) 17 | 18 | assert Enum.member?(checks, {Credo.Check.Readability.LargeNumbers, false}) 19 | end) 20 | end 21 | 22 | test "it should run example command when example command is given" do 23 | exec = IntegrationTest.run([@integration_path, "example"]) 24 | 25 | assert "Hello World!" == Execution.get_assign(exec, "example_plugin.hello") 26 | end 27 | 28 | test "it should run example command as default command" do 29 | exec = IntegrationTest.run([@integration_path]) 30 | 31 | assert "Hello World!" == Execution.get_assign(exec, "example_plugin.hello") 32 | end 33 | 34 | test "it should accept --world CLI switch" do 35 | exec = IntegrationTest.run([@integration_path, "--world", "mars"]) 36 | 37 | assert "Hello MARS!" == Execution.get_assign(exec, "example_plugin.hello") 38 | end 39 | 40 | test "it should accept -W CLI switch" do 41 | exec = IntegrationTest.run([@integration_path, "-W", "mars"]) 42 | 43 | assert "Hello MARS!" == Execution.get_assign(exec, "example_plugin.hello") 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/credo/task_group_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.MainProcessTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/credo/test/assertions_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Test.AssertionsTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Credo.Test.Assertions 5 | 6 | alias Credo.Issue 7 | 8 | test "it should assert issues" do 9 | [%Issue{}] 10 | |> assert_issue() 11 | end 12 | 13 | test "it should not assert an empty list of issues" do 14 | assert_raise ExUnit.AssertionError, fn -> 15 | [] 16 | |> assert_issue() 17 | end 18 | end 19 | 20 | test "it should assert a list of issues" do 21 | [%Issue{}, %Issue{}] 22 | |> assert_issues() 23 | end 24 | 25 | test "it should not assert a list of one issue as assert_issues/1" do 26 | assert_raise ExUnit.AssertionError, fn -> 27 | [] 28 | |> assert_issues() 29 | end 30 | end 31 | 32 | test "it should refute issues" do 33 | [] 34 | |> refute_issues() 35 | end 36 | 37 | test "it should not refute a list of issues" do 38 | assert_raise ExUnit.AssertionError, fn -> 39 | [%Issue{}] 40 | |> refute_issues() 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/credo/test/check_runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.Test.CheckRunnerTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Credo.Test.CheckRunner 5 | 6 | alias Credo.Issue 7 | alias Credo.SourceFile 8 | 9 | @source_file %SourceFile{filename: "x"} 10 | 11 | defmodule FakeTestCheck do 12 | use Credo.Check 13 | 14 | def run(%SourceFile{} = source_file, params \\ []) do 15 | if params[:issue_count] do 16 | issue_count = params[:issue_count] || 0 17 | 18 | replicate(issue_count, %Issue{filename: source_file.filename}) 19 | else 20 | [] 21 | end 22 | end 23 | 24 | defp replicate(count, item) do 25 | for(_ <- 1..count, do: item) 26 | end 27 | end 28 | 29 | test "it should run the check" do 30 | issues = run_check(@source_file, FakeTestCheck, []) 31 | 32 | assert issues == [] 33 | end 34 | 35 | test "it should run the check /2" do 36 | issues = run_check(@source_file, FakeTestCheck, issue_count: 1) 37 | 38 | assert Enum.count(issues) == 1 39 | end 40 | 41 | test "it should run the check /3" do 42 | issues = run_check(@source_file, FakeTestCheck, issue_count: 3) 43 | 44 | assert Enum.count(issues) == 3 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/error_if_warnings.sh: -------------------------------------------------------------------------------- 1 | grep -E "^warning: .+" --after-context=6 <&0 2 | 3 | if [ $? -eq 0 ] 4 | then 5 | echo "error: Runtime warnings detected. Exiting ..." 6 | exit 255 7 | else 8 | exit 0 9 | fi 10 | -------------------------------------------------------------------------------- /test/fixtures/custom-config.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "src/", "web/", "apps/"], 7 | excluded: [] 8 | }, 9 | checks: [ 10 | {Credo.Check.Readability.ModuleDoc, false} 11 | ] 12 | }, 13 | %{ 14 | name: "empty-config" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/custom-config.exs.malformed: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "src/", "web/", "apps/"], 7 | excluded: [] 8 | } 9 | checks: [ 10 | {Credo.Check.Readability.ModuleDoc, false} 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/example_code/clean_redux.ex: -------------------------------------------------------------------------------- 1 | defmodule X do 2 | defp escape_charlist(reversed_result, [?" | remainder], needs_quote?), 3 | do: escape_charlist(~c"\"\\" ++ reversed_result, remainder, needs_quote?) 4 | 5 | @doc ~S""" 6 | Escape a subsection name before saving. 7 | """ 8 | def escape_subsection(""), do: "\"\"" 9 | 10 | def escape_subsection(x) when is_binary(x) do 11 | x 12 | |> String.to_charlist() 13 | |> escape_subsection_impl([]) 14 | |> Enum.reverse() 15 | |> to_quoted_string() 16 | end 17 | 18 | defp to_quoted_string(s), do: ~s["#{s}"] 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/example_plugin_integration/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | plugins: [ 6 | {ExamplePlugin, [world: "World"]} 7 | ] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/example_plugin_integration/plugin/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | checks: [ 6 | {Credo.Check.Readability.LargeNumbers, false} 7 | ] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/example_plugin_integration/plugin/example_plugin.ex: -------------------------------------------------------------------------------- 1 | defmodule ExamplePlugin do 2 | @config_file File.read!(Path.join(__DIR__, ".credo.exs")) 3 | 4 | import Credo.Plugin 5 | 6 | def init(exec) do 7 | exec 8 | |> register_default_config(@config_file) 9 | |> register_command("example", ExamplePlugin.ExampleCommand) 10 | |> register_cli_switch(:world, :string, :W, fn switch_value -> 11 | {:world, String.upcase(switch_value)} 12 | end) 13 | |> prepend_task(:set_default_command, ExamplePlugin.SetExampleAsDefaultCommand) 14 | end 15 | end 16 | 17 | defmodule ExamplePlugin.ExampleCommand do 18 | @moduledoc false 19 | 20 | use Credo.CLI.Command 21 | 22 | alias Credo.Execution 23 | 24 | def call(exec, _) do 25 | world = Execution.get_plugin_param(exec, ExamplePlugin, :world) 26 | 27 | Execution.put_assign(exec, "example_plugin.hello", "Hello #{world}!") 28 | end 29 | end 30 | 31 | defmodule ExamplePlugin.SetExampleAsDefaultCommand do 32 | use Credo.Execution.Task 33 | 34 | def call(exec, _opts) do 35 | set_command(exec, exec.cli_options.command || "example") 36 | end 37 | 38 | defp set_command(exec, command) do 39 | %{exec | cli_options: %{exec.cli_options | command: command}} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/fixtures/file_exclusion/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | excluded: ["foo.ex"] 7 | } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/file_exclusion/bar.ex: -------------------------------------------------------------------------------- 1 | # bar 2 | -------------------------------------------------------------------------------- /test/fixtures/file_exclusion/foo.ex: -------------------------------------------------------------------------------- 1 | # foo 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | commitmsg="$(git rev-parse --git-path hooks/commit-msg)" 14 | test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | precommit="$(git rev-parse --git-path hooks/pre-commit)" 13 | test -x "$precommit" && exec "$precommit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=$(git hash-object -t tree /dev/null) 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | exec git diff-index --check --cached $against -- 50 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | while read local_ref local_sha remote_ref remote_sha 28 | do 29 | if [ "$local_sha" = $z40 ] 30 | then 31 | # Handle delete 32 | : 33 | else 34 | if [ "$remote_sha" = $z40 ] 35 | then 36 | # New branch, examine all commits 37 | range="$local_sha" 38 | else 39 | # Update to existing branch, examine new commits 40 | range="$remote_sha..$local_sha" 41 | fi 42 | 43 | # Check for WIP commit 44 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 45 | if [ -n "$commit" ] 46 | then 47 | echo >&2 "Found WIP commit in $local_ref, not pushing" 48 | exit 1 49 | fi 50 | fi 51 | done 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/pre-receive.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to make use of push options. 4 | # The example simply echoes all push options that start with 'echoback=' 5 | # and rejects all pushes when the "reject" push option is used. 6 | # 7 | # To enable this hook, rename this file to "pre-receive". 8 | 9 | if test -n "$GIT_PUSH_OPTION_COUNT" 10 | then 11 | i=0 12 | while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" 13 | do 14 | eval "value=\$GIT_PUSH_OPTION_$i" 15 | case "$value" in 16 | echoback=*) 17 | echo "echo from the pre-receive-hook: ${value#*=}" >&2 18 | ;; 19 | reject) 20 | exit 1 21 | esac 22 | i=$((i + 1)) 23 | done 24 | fi 25 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first one removes the 13 | # "# Please enter the commit message..." help message. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | COMMIT_MSG_FILE=$1 24 | COMMIT_SOURCE=$2 25 | SHA1=$3 26 | 27 | /usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" 28 | 29 | # case "$COMMIT_SOURCE,$SHA1" in 30 | # ,|template,) 31 | # /usr/bin/perl -i.bak -pe ' 32 | # print "\n" . `git diff --cached --name-status -r` 33 | # if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; 34 | # *) ;; 35 | # esac 36 | 37 | # SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 38 | # git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" 39 | # if test -z "$COMMIT_SOURCE" 40 | # then 41 | # /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" 42 | # fi 43 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/09/02a2715e3db943e9fffc8ed626025a136dbc4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/09/02a2715e3db943e9fffc8ed626025a136dbc4a -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/10/df72ae94e2a666e0a7cbe5048e1833a357cfe5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/10/df72ae94e2a666e0a7cbe5048e1833a357cfe5 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/11/6272d9408606a4b8f165fb149af23d60be026d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/11/6272d9408606a4b8f165fb149af23d60be026d -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/17/8f9317eb57724fccf09c74e0baa197bb12f7f2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/17/8f9317eb57724fccf09c74e0baa197bb12f7f2 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/2b/3a6a1db133fc0f2960bac3983164d4f9a9c0c5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/2b/3a6a1db133fc0f2960bac3983164d4f9a9c0c5 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/2f/4df54614af76459e53a38d823f427cf63e2a18: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/2f/4df54614af76459e53a38d823f427cf63e2a18 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/34/210f45e4d1b0acdcc84355e2b97a58283000dc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/34/210f45e4d1b0acdcc84355e2b97a58283000dc -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/44/f8a14333a7420fe874edc720f4bfcc860c3ed6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/44/f8a14333a7420fe874edc720f4bfcc860c3ed6 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/47/73d949779e8dddc38d6a83c9e61783bd339db8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/47/73d949779e8dddc38d6a83c9e61783bd339db8 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/48/3d58ae9dc2f1605d42941db6b4758a04b17ac6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/48/3d58ae9dc2f1605d42941db6b4758a04b17ac6 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/5a/96e335044fa25ce25014326955c8bd29af5ec4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/5a/96e335044fa25ce25014326955c8bd29af5ec4 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/69/98e81bb3bc22abdda5761f14be6629685665bc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/69/98e81bb3bc22abdda5761f14be6629685665bc -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/77/3fd798caaad39039c5f91b1cb31bc22ba15fc9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/77/3fd798caaad39039c5f91b1cb31bc22ba15fc9 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/77/f632647a8fdfe6b315ab8b57f7c4dc313913dc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/77/f632647a8fdfe6b315ab8b57f7c4dc313913dc -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/7c/9eeb29209cf8ffc7bcf8218a21ba2f342567ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/7c/9eeb29209cf8ffc7bcf8218a21ba2f342567ec -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/8e/2b266f96f0e56d55ded352b6d31182e3097752: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/8e/2b266f96f0e56d55ded352b6d31182e3097752 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/9b/86f40b6685ac801fc8689ed2f03d0738c3ea1c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/9b/86f40b6685ac801fc8689ed2f03d0738c3ea1c -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/9e/033f62a8a2c66e6f145544391645398e7c5611: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/9e/033f62a8a2c66e6f145544391645398e7c5611 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/ab/03fb0cb38ef86c7970f21850fa0b10d80f8980: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/ab/03fb0cb38ef86c7970f21850fa0b10d80f8980 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/b3/13235066408ce34cc5e63b4e12394752608c99: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/b3/13235066408ce34cc5e63b4e12394752608c99 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/b8/263030c824a096f058e1f1f79902b7b93a609d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/b8/263030c824a096f058e1f1f79902b7b93a609d -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/b9/0909b828423fb723dddd82a97e35d3a325f6e5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/b9/0909b828423fb723dddd82a97e35d3a325f6e5 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/c9/2b67021398e462ff5639754c90402551c26263: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/c9/2b67021398e462ff5639754c90402551c26263 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/cc/9d83be165d09ae5e5ebc1ad4af03bea02da2a8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/cc/9d83be165d09ae5e5ebc1ad4af03bea02da2a8 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/da/72fa8ddba1fc83545a0b438b9c5529d2293d88: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/da/72fa8ddba1fc83545a0b438b9c5529d2293d88 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/dd/5c65c9db1b5e530170e59660e39e2628fb2a1e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/dd/5c65c9db1b5e530170e59660e39e2628fb2a1e -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/e5/5746a6c642f990aaa909a1486cd10af696e240: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/e5/5746a6c642f990aaa909a1486cd10af696e240 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/e7/0168dfb1ba5f51a80373b6af7fc4c342340c09: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/e7/0168dfb1ba5f51a80373b6af7fc4c342340c09 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/ed/634b278ad31b9c327f6666e084779827ab2f59: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/ed/634b278ad31b9c327f6666e084779827ab2f59 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/ef/e7406369ccdc9262379450dd0a0540dd6627bd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/ef/e7406369ccdc9262379450dd0a0540dd6627bd -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/objects/f2/dc5fdb387767bef8f24a34f68ee593b84de434: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_diff_bare_repo/objects/f2/dc5fdb387767bef8f24a34f68ee593b84de434 -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/refs/heads/master: -------------------------------------------------------------------------------- 1 | 116272d9408606a4b8f165fb149af23d60be026d 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/refs/tags/add-doc-fun-with-cond-issue: -------------------------------------------------------------------------------- 1 | b8263030c824a096f058e1f1f79902b7b93a609d 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/refs/tags/add-fun-with-cond-issue: -------------------------------------------------------------------------------- 1 | 34210f45e4d1b0acdcc84355e2b97a58283000dc 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/refs/tags/add-two-todo-comments: -------------------------------------------------------------------------------- 1 | b313235066408ce34cc5e63b4e12394752608c99 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_diff_bare_repo/refs/tags/move-todo-comments: -------------------------------------------------------------------------------- 1 | 116272d9408606a4b8f165fb149af23d60be026d 2 | -------------------------------------------------------------------------------- /test/fixtures/integration_test_config/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["**/*.ex"] 7 | }, 8 | plugins: [], 9 | requires: [], 10 | strict: false, 11 | parse_timeout: 5000, 12 | color: false, 13 | checks: [ 14 | { 15 | Credo.Check.Consistency.ExceptionNames, 16 | files: %{included: "**/*.ex", excluded: "**/clean.ex"}, tags: [:__initial__, :my_tag] 17 | }, 18 | { 19 | Credo.Check.Readability.ModuleDoc, 20 | files: %{included: "**/*.ex", excluded: "**/*_redux.ex"} 21 | }, 22 | {Credo.Check.Readability.TrailingWhiteSpace, 23 | [ 24 | category: :warning, 25 | exit_status: 23, 26 | files: %{excluded: "**/*.ex"}, 27 | priority: :high, 28 | tags: [:__initial__, :my_tag] 29 | ]} 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/integration_test_config/.sarif.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | plugins: [], 6 | requires: [], 7 | strict: false, 8 | parse_timeout: 5000, 9 | color: false, 10 | checks: %{ 11 | enabled: [ 12 | { 13 | Credo.Check.Consistency.ExceptionNames, 14 | files: %{included: "**/*.ex", excluded: "**/clean.ex"}, tags: [:__initial__, :my_tag] 15 | }, 16 | { 17 | Credo.Check.Readability.ModuleDoc, 18 | files: %{included: "**/*.ex", excluded: "**/*_redux.ex"} 19 | }, 20 | {Credo.Check.Readability.TrailingWhiteSpace, 21 | [ 22 | category: :warning, 23 | exit_status: 23, 24 | files: %{excluded: "**/*.ex"}, 25 | priority: :high, 26 | tags: [:__initial__, :my_tag] 27 | ]} 28 | ] 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test/fixtures/integration_test_config/lib/clean.ex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/integration_test_config/lib/clean.ex -------------------------------------------------------------------------------- /test/fixtures/integration_test_config/lib/clean/clean_redux.ex: -------------------------------------------------------------------------------- 1 | defmodule X do 2 | defp escape_charlist(reversed_result, [?" | remainder], needs_quote?), 3 | do: escape_charlist(~c"\"\\" ++ reversed_result, remainder, needs_quote?) 4 | 5 | @doc ~S""" 6 | Escape a subsection name before saving. 7 | """ 8 | def escape_subsection(""), do: "\"\"" 9 | 10 | def escape_subsection(x) when is_binary(x) do 11 | x 12 | |> String.to_charlist() 13 | |> escape_subsection_impl([]) 14 | |> Enum.reverse() 15 | |> to_quoted_string() 16 | end 17 | 18 | defp to_quoted_string(s), do: ~s["#{s}"] 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/options/cmd1/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/options/cmd1/.keep -------------------------------------------------------------------------------- /test/fixtures/options/foo.ex: -------------------------------------------------------------------------------- 1 | # foo 2 | -------------------------------------------------------------------------------- /test/fixtures/options/src/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrene/credo/f731459d4fb5c3359303e99fde9fa1e51d6fbea9/test/fixtures/options/src/.keep -------------------------------------------------------------------------------- /test/integration/categories_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.CategoriesTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | 8 | test "it should NOT report issues on categories" do 9 | exec = IntegrationTest.run(["categories"]) 10 | issues = Credo.Execution.get_issues(exec) 11 | 12 | assert exec.cli_options.command == "categories" 13 | assert issues == [] 14 | end 15 | 16 | test "it should NOT report issues on categories (using --format json)" do 17 | exec = IntegrationTest.run(["categories", "--format", "json"]) 18 | issues = Credo.Execution.get_issues(exec) 19 | 20 | assert exec.cli_options.command == "categories" 21 | assert issues == [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/integration/explain_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.ExplainTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | @moduletag timeout: 300_000 8 | 9 | @fixture_integration_test_config_with_location "test/credo/code/interpolation_helper_test.exs:167" 10 | 11 | test "it should explain an issue using a filename with location" do 12 | exec = IntegrationTest.run([@fixture_integration_test_config_with_location]) 13 | 14 | assert exec.cli_options.command == "explain" 15 | end 16 | 17 | test "it should explain an issue using a filename with location (using --format json)" do 18 | exec = 19 | IntegrationTest.run([@fixture_integration_test_config_with_location, "--format", "json"]) 20 | 21 | assert exec.cli_options.command == "explain" 22 | end 23 | 24 | test "it should explain an issue using explain command (using --help)" do 25 | exec = IntegrationTest.run(["explain", "--help"]) 26 | 27 | assert exec.cli_options.command == "explain" 28 | end 29 | 30 | test "it should explain an issue using explain command" do 31 | exec = IntegrationTest.run(["explain", @fixture_integration_test_config_with_location]) 32 | 33 | assert exec.cli_options.command == "explain" 34 | end 35 | 36 | test "it should explain a check using explain command" do 37 | exec = IntegrationTest.run(["explain", "Credo.Check.Readability.ModuleDoc"]) 38 | _issues = Credo.Execution.get_issues(exec) 39 | 40 | assert exec.cli_options.command == "explain" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/integration/gen_check_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.GenCheckTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | 8 | test "it should generate a new check" do 9 | exec = IntegrationTest.run(["gen.check", "tmp/lib/my_first_credo_check.ex"]) 10 | 11 | assert exec.cli_options.command == "gen.check" 12 | end 13 | 14 | test "it should show help on generating a new check" do 15 | exec = IntegrationTest.run(["gen.check", "--help"]) 16 | 17 | assert exec.cli_options.command == "gen.check" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/integration/gen_config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.GenConfigTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | 8 | test "it should generate a new config" do 9 | exec = IntegrationTest.run(["gen.config", "tmp/lib/my_first_credo_config.ex"]) 10 | 11 | assert exec.cli_options.command == "gen.config" 12 | end 13 | 14 | test "it should show help on generating a new config" do 15 | exec = IntegrationTest.run(["gen.config", "--help"]) 16 | 17 | assert exec.cli_options.command == "gen.config" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/integration/help_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.HelpTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | 8 | test "it should NOT report issues on --help" do 9 | exec = IntegrationTest.run(["--help"]) 10 | issues = Credo.Execution.get_issues(exec) 11 | 12 | assert exec.cli_options.command == "help" 13 | assert issues == [] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/integration/info_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.InfoTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | 8 | test "it should NOT report issues for info" do 9 | exec = IntegrationTest.run(["info"]) 10 | issues = Credo.Execution.get_issues(exec) 11 | 12 | assert exec.cli_options.command == "info" 13 | assert issues == [] 14 | end 15 | 16 | test "it should NOT report issues for info --help" do 17 | exec = IntegrationTest.run(["info", "--help"]) 18 | issues = Credo.Execution.get_issues(exec) 19 | 20 | assert exec.cli_options.command == "info" 21 | assert issues == [] 22 | end 23 | 24 | test "it should NOT report issues for info --verbose" do 25 | exec = IntegrationTest.run(["info", "--verbose"]) 26 | issues = Credo.Execution.get_issues(exec) 27 | 28 | assert exec.cli_options.command == "info" 29 | assert issues == [] 30 | end 31 | 32 | test "it should NOT report issues for info --verbose (using --format json)" do 33 | exec = IntegrationTest.run(["info", "--verbose", "--format", "json"]) 34 | issues = Credo.Execution.get_issues(exec) 35 | 36 | assert exec.cli_options.command == "info" 37 | assert issues == [] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/integration/sarif_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.SarifTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag timeout: 300_000 7 | 8 | test "it should report issues using suggest command (using --format sarif)" do 9 | exec = 10 | IntegrationTest.run( 11 | ~w[suggest --config-file test/fixtures/integration_test_config/.sarif.exs --format sarif] 12 | ) 13 | 14 | issues = Credo.Execution.get_issues(exec) 15 | 16 | assert exec.cli_options.command == "suggest" 17 | assert issues != [] 18 | 19 | sarif = 20 | ExUnit.CaptureIO.capture_io(fn -> 21 | Credo.CLI.Output.Formatter.SARIF.print_issues(issues, exec) 22 | end) 23 | 24 | sarif_map = Jason.decode!(sarif) 25 | 26 | assert sarif_map["version"] == "2.1.0" 27 | 28 | first_run = List.first(sarif_map["runs"]) 29 | 30 | assert first_run["tool"]["driver"]["name"] == "Credo" 31 | 32 | rules = first_run["tool"]["driver"]["rules"] 33 | results = first_run["results"] 34 | 35 | assert rules != [] 36 | assert results != [] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/integration/version_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Credo.VersionTest do 2 | use Credo.Test.Case 3 | 4 | alias Credo.Test.IntegrationTest 5 | 6 | @moduletag slow: :integration 7 | 8 | test "it should NOT report issues on integration_test_config fixture" do 9 | exec = IntegrationTest.run(["--version"]) 10 | issues = Credo.Execution.get_issues(exec) 11 | 12 | assert exec.cli_options.command == "version" 13 | assert issues == [] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/old_credo.exs: -------------------------------------------------------------------------------- 1 | Mix.install([{:credo, "~> 1.7"}]) 2 | 3 | Credo.run(~w"--mute-exit-status --strict --enable-disabled-checks .+ --no-color --format oneline") 4 | -------------------------------------------------------------------------------- /test/regression/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # common setup 4 | 5 | set -e 6 | 7 | DIRNAME=$( cd "$( dirname "$0" )" && pwd ) 8 | CREDO_ROOT=$( cd "$DIRNAME/../.." && pwd ) 9 | 10 | # script specific sources, variables and function definitions 11 | 12 | OLD_CREDO_VERSION=$1 13 | GIT_REPO=$2 14 | PROJECT_NAME=$3 15 | 16 | mkdir -p tmp 17 | 18 | WORKING_DIR=$( cd "$CREDO_ROOT/tmp/$PROJECT_NAME" && pwd ) 19 | 20 | cd tmp 21 | 22 | rm -fr $PROJECT_NAME 23 | git clone $GIT_REPO $PROJECT_NAME --depth=1 --quiet 24 | 25 | cd $CREDO_ROOT 26 | 27 | # TODO: add --enable-disabled-checks . 28 | 29 | mix credo --working-dir $WORKING_DIR \ 30 | --strict --format oneline --mute-exit-status | sort | sed -e 's/tmp\///g' > tmp/results_current.txt 31 | 32 | cp test/regression/run_older_credo_version.exs tmp/ 33 | cd tmp 34 | 35 | mix run --no-mix-exs run_older_credo_version.exs $OLD_CREDO_VERSION --working-dir $WORKING_DIR \ 36 | --strict --format oneline --mute-exit-status | sort | sed -e 's/tmp\///g' > results_old.txt 37 | 38 | diff results_old.txt results_current.txt && echo "No differences in issues." -------------------------------------------------------------------------------- /test/regression/run_older_credo_version.exs: -------------------------------------------------------------------------------- 1 | [credo_version | rest] = System.argv() 2 | 3 | Mix.install([ 4 | {:credo, credo_version} 5 | ]) 6 | 7 | Credo.run(rest) 8 | -------------------------------------------------------------------------------- /test/run_on_project.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # common setup 4 | 5 | set -e 6 | 7 | DIRNAME=$( cd "$( dirname "$0" )" && pwd ) 8 | CREDO_ROOT=$( cd "$DIRNAME/.." && pwd ) 9 | 10 | # script specific sources, variables and function definitions 11 | 12 | GIT_REPO=$1 13 | PROJECT_NAME=$2 14 | CREDO_ARG1=$3 15 | CREDO_ARG2=$4 16 | CREDO_ARG3=$5 17 | CREDO_ARG4=$6 18 | CREDO_ARG5=$7 19 | 20 | # setup 21 | 22 | cd $CREDO_ROOT 23 | 24 | mkdir -p tmp 25 | 26 | echo "" 27 | echo "--> Cloning $PROJECT_NAME ..." 28 | 29 | PROJECT_DIRNAME=tmp/$PROJECT_NAME 30 | 31 | rm -fr $PROJECT_DIRNAME || true 32 | git clone $GIT_REPO $PROJECT_DIRNAME --depth=1 --quiet 33 | 34 | echo "--> Analysing $PROJECT_NAME ..." 35 | echo "" 36 | 37 | CMD="mix credo $PROJECT_DIRNAME --mute-exit-status --format json --strict $CREDO_ARG1 $CREDO_ARG2 $CREDO_ARG3 $CREDO_ARG4 $CREDO_ARG5" 38 | 39 | bash -c "time $CMD" 40 | -------------------------------------------------------------------------------- /test/runtime_warnings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | mix compile --force --warnings-as-errors 6 | 7 | 8 | mix credo --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 9 | 10 | mix credo --strict --enable-disabled-checks . --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 11 | 12 | mix credo --strict --enable-disabled-checks . --mute-exit-status --format=json 2>&1 | ./test/error_if_warnings.sh 13 | 14 | mix credo lib/credo.ex --read-from-stdin --strict < lib/credo.ex 2>&1 | ./test/error_if_warnings.sh 15 | 16 | mix credo list --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 17 | 18 | mix credo suggest --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 19 | 20 | mix credo diff HEAD^ --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 21 | 22 | mix credo diff v1.4.0 --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 23 | 24 | mix credo explain test/fixtures/example_code/clean_redux.ex:1:11 --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 25 | 26 | mix credo explain Credo.Check.Refactor.Nesting --mute-exit-status 2>&1 | ./test/error_if_warnings.sh 27 | 28 | mix credo categories 2>&1 | ./test/error_if_warnings.sh 29 | 30 | mix credo info --verbose 2>&1 | ./test/error_if_warnings.sh 31 | 32 | mix credo version 2>&1 | ./test/error_if_warnings.sh 33 | 34 | mix credo help 2>&1 | ./test/error_if_warnings.sh 35 | 36 | 37 | echo "" 38 | echo "Smoke test for runtime warnings successful." 39 | -------------------------------------------------------------------------------- /test/smoke_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | GENEREATE_CREDO_CHECK="lib/my_first_credo_check.ex" 6 | 7 | mix compile --force --warnings-as-errors 8 | 9 | mix credo --mute-exit-status 10 | mix credo --strict --mute-exit-status 11 | mix credo --strict --enable-disabled-checks . --mute-exit-status 12 | mix credo --debug --mute-exit-status 13 | mix credo --strict --format=sarif --mute-exit-status 14 | 15 | # repro for editor integrations 16 | # should not find any issues, therefore exit status is not muted 17 | mix credo lib/credo.ex --read-from-stdin --strict < lib/credo.ex 18 | 19 | mix credo list --mute-exit-status 20 | mix credo suggest --mute-exit-status 21 | mix credo diff HEAD^ --mute-exit-status 22 | mix credo diff v1.4.0 --mute-exit-status 23 | 24 | # explain issues 25 | mix credo test/fixtures/example_code/clean_redux.ex:1:11 --mute-exit-status 26 | mix credo explain test/fixtures/example_code/clean_redux.ex:1:11 --mute-exit-status 27 | mix credo test/fixtures/example_code/clean_redux.ex:1:11 --mute-exit-status --format=json 28 | mix credo explain test/fixtures/example_code/clean_redux.ex:1:11 --mute-exit-status --format=json 29 | # explain check 30 | mix credo explain Credo.Check.Refactor.Nesting --mute-exit-status 31 | mix credo explain Credo.Check.Refactor.Nesting --mute-exit-status --format=json 32 | 33 | mix credo.gen.check $GENEREATE_CREDO_CHECK 34 | rm $GENEREATE_CREDO_CHECK 35 | 36 | mix credo.gen.config || true 37 | 38 | mix credo categories 39 | mix credo categories --format=json 40 | 41 | mix credo info 42 | mix credo info --verbose 43 | mix credo info --format=json 44 | mix credo info --verbose --format=json 45 | 46 | mix credo version 47 | mix credo help 48 | 49 | mix credo -v 50 | mix credo -h 51 | 52 | 53 | echo "" 54 | echo "Smoke test successful." 55 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | check_version = 4 | ~w(1.6.5 1.7.0 1.9.0) 5 | |> Enum.reduce([], fn version, acc -> 6 | # allow -dev versions so we can test before the Elixir release. 7 | if System.version() |> Version.match?("< #{version}-dev") do 8 | acc ++ [needs_elixir: version] 9 | else 10 | acc 11 | end 12 | end) 13 | 14 | exclude = Keyword.merge([to_be_implemented: true, housekeeping: true], check_version) 15 | 16 | ExUnit.configure(exclude: exclude) 17 | 18 | defmodule Credo.Test.IntegrationTest do 19 | def run(argv) do 20 | parent = self() 21 | 22 | spawn(fn -> 23 | doit = fn -> 24 | exec = Credo.run(argv) 25 | send(parent, {:exec, exec}) 26 | end 27 | 28 | if System.get_env("DEBUG") do 29 | doit.() 30 | else 31 | ExUnit.CaptureLog.capture_log(fn -> 32 | ExUnit.CaptureIO.capture_io(fn -> 33 | doit.() 34 | end) 35 | end) 36 | end 37 | end) 38 | 39 | receive do 40 | {:exec, exec} -> exec 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/test_if_tests_fail_after_resetting_lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # common setup 4 | 5 | set -e 6 | 7 | DIRNAME=$( cd "$( dirname "$0" )" && pwd ) 8 | CREDO_ROOT=$( cd "$DIRNAME/.." && pwd ) 9 | 10 | # execution 11 | 12 | cd $CREDO_ROOT 13 | 14 | git checkout master lib/ 15 | 16 | if mix test ; then 17 | echo "" 18 | echo "------------------------------------------------------------------" 19 | echo "" 20 | echo "There are changes to both lib/ and test/ which can indicate" 21 | echo "a bugfix with a corresponding test that reproduces the fixed bug" 22 | echo "" 23 | echo "(if this is not a bugfix PR, please ignore the following error)" 24 | echo "" 25 | echo "\e[31mAfter resetting changes in lib/, mix test should have failed" 26 | echo "" 27 | echo "------------------------------------------------------------------" 28 | echo "" 29 | exit 1 30 | else 31 | exit 0 32 | fi 33 | -------------------------------------------------------------------------------- /test/test_phoenix_compatibility.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | ## Ensures that a fresh Phoenix project does not trigger any issue in "normal" mode 5 | ## 6 | 7 | # common setup 8 | 9 | set -e 10 | 11 | DIRNAME=$( cd "$( dirname "$0" )" && pwd ) 12 | CREDO_ROOT=$( cd "$DIRNAME/.." && pwd ) 13 | 14 | # script specific sources, variables and function definitions 15 | 16 | PROJECT_NAME=phx_credo_tester 17 | PROJECT_DIRNAME=tmp/$PROJECT_NAME 18 | 19 | # setup 20 | 21 | yes | mix archive.install hex phx_new 22 | 23 | cd $CREDO_ROOT 24 | 25 | mkdir -p tmp 26 | 27 | echo "" 28 | echo "--> Creating $PROJECT_NAME ..." 29 | echo "" 30 | 31 | rm -fr $PROJECT_DIRNAME || true 32 | 33 | cd tmp 34 | yes n | mix phx.new $PROJECT_NAME 35 | 36 | # execution 37 | 38 | echo "" 39 | echo "--> Running Credo ..." 40 | echo "" 41 | 42 | cd $CREDO_ROOT 43 | 44 | mix credo $PROJECT_DIRNAME 45 | --------------------------------------------------------------------------------