sonicop 26.8.111 → 26.8.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CONFORMANCE.md +64 -31
- data/Cargo.lock +94 -19
- data/Cargo.toml +13 -8
- data/NOTICE +15 -3
- data/README.ja.md +145 -57
- data/README.md +140 -48
- data/build.rs +155 -0
- data/lib/sonicop/runner.rb +7 -1
- data/lib/sonicop/version.rb +1 -1
- data/src/cli.rs +216 -45
- data/src/config/inheritance.rs +637 -64
- data/src/config/mod.rs +130 -1
- data/src/config/paths.rs +550 -48
- data/src/cop_name.rs +18 -1
- data/src/diagnostic.rs +168 -3
- data/src/directives.rs +398 -77
- data/src/display_width.rs +82 -42
- data/src/display_width_table.rs +506 -0
- data/src/engine.rs +1532 -51
- data/src/formatter.rs +10 -1
- data/src/lib.rs +2 -0
- data/src/main.rs +112 -1
- data/src/nul_bytes.rs +245 -47
- data/src/parser.rs +37 -0
- data/src/profile.rs +174 -24
- data/src/ruby_version.rs +167 -21
- data/src/rules/bundler/duplicated_group.rs +3 -2
- data/src/rules/bundler/gem_comment.rs +5 -4
- data/src/rules/gemspec/attribute_assignment.rs +4 -3
- data/src/rules/gemspec/deprecated_attribute_assignment.rs +7 -6
- data/src/rules/gemspec/duplicated_assignment.rs +4 -3
- data/src/rules/gemspec/require_mfa.rs +10 -9
- data/src/rules/gemspec/required_ruby_version.rs +3 -2
- data/src/rules/gemspec/support.rs +28 -8
- data/src/rules/layout/access_modifier_indentation.rs +13 -6
- data/src/rules/layout/argument_alignment.rs +19 -7
- data/src/rules/layout/array_alignment.rs +1 -2
- data/src/rules/layout/block_alignment.rs +74 -30
- data/src/rules/layout/block_end_newline.rs +5 -5
- data/src/rules/layout/class_structure.rs +13 -4
- data/src/rules/layout/closing_heredoc_indentation.rs +24 -5
- data/src/rules/layout/closing_parenthesis_indentation.rs +3 -3
- data/src/rules/layout/comment_indentation.rs +2 -1
- data/src/rules/layout/condition_position.rs +3 -2
- data/src/rules/layout/def_end_alignment.rs +10 -6
- data/src/rules/layout/dot_position.rs +1 -2
- data/src/rules/layout/else_alignment.rs +24 -14
- data/src/rules/layout/empty_line_after_guard_clause.rs +8 -7
- data/src/rules/layout/empty_line_after_multiline_condition.rs +5 -5
- data/src/rules/layout/empty_line_between_defs.rs +8 -6
- data/src/rules/layout/empty_lines_around_access_modifier.rs +22 -10
- data/src/rules/layout/empty_lines_around_arguments.rs +4 -4
- data/src/rules/layout/empty_lines_around_body.rs +8 -2
- data/src/rules/layout/empty_lines_around_exception_handling_keywords.rs +1 -2
- data/src/rules/layout/empty_lines_around_method_body.rs +3 -2
- data/src/rules/layout/end_alignment.rs +32 -7
- data/src/rules/layout/end_of_line.rs +14 -1
- data/src/rules/layout/extra_spacing.rs +9 -11
- data/src/rules/layout/first_array_element_indentation.rs +1 -2
- data/src/rules/layout/first_hash_element_indentation.rs +8 -6
- data/src/rules/layout/hash_alignment.rs +113 -19
- data/src/rules/layout/heredoc_argument_closing_parenthesis.rs +6 -5
- data/src/rules/layout/heredoc_indentation.rs +4 -3
- data/src/rules/layout/indentation_style.rs +6 -4
- data/src/rules/layout/indentation_width.rs +76 -16
- data/src/rules/layout/initial_indentation.rs +18 -5
- data/src/rules/layout/line_continuation_leading_space.rs +4 -3
- data/src/rules/layout/line_continuation_spacing.rs +4 -3
- data/src/rules/layout/line_length.rs +417 -43
- data/src/rules/layout/mod.rs +1 -1
- data/src/rules/layout/multiline_assignment_layout.rs +20 -3
- data/src/rules/layout/multiline_block_layout.rs +1 -2
- data/src/rules/layout/multiline_expression.rs +60 -31
- data/src/rules/layout/multiline_hash_brace_layout.rs +4 -3
- data/src/rules/layout/multiline_method_call_indentation.rs +1 -1
- data/src/rules/layout/multiline_operation_indentation.rs +3 -3
- data/src/rules/layout/redundant_line_break.rs +16 -5
- data/src/rules/layout/rescue_ensure_alignment.rs +20 -12
- data/src/rules/layout/single_line_block_chain.rs +13 -4
- data/src/rules/layout/space_around_block_parameters.rs +16 -2
- data/src/rules/layout/space_around_equals_in_parameter_default.rs +1 -2
- data/src/rules/layout/space_around_keyword.rs +22 -4
- data/src/rules/layout/space_around_operators.rs +34 -2
- data/src/rules/layout/space_before_brackets.rs +13 -6
- data/src/rules/layout/space_before_comma.rs +3 -2
- data/src/rules/layout/space_inside_array_literal_brackets.rs +25 -10
- data/src/rules/layout/space_inside_array_percent_literal.rs +7 -1
- data/src/rules/layout/space_inside_parens.rs +170 -87
- data/src/rules/layout/space_inside_range_literal.rs +53 -19
- data/src/rules/layout/space_inside_reference_brackets.rs +65 -3
- data/src/rules/layout/space_inside_string_interpolation.rs +147 -48
- data/src/rules/layout/support.rs +172 -26
- data/src/rules/layout/tokens.rs +47 -7
- data/src/rules/layout/trailing_whitespace.rs +5 -2
- data/src/rules/lint/access_modifier.rs +50 -5
- data/src/rules/lint/ambiguity.rs +97 -7
- data/src/rules/lint/ambiguous_block_association.rs +29 -12
- data/src/rules/lint/ambiguous_operator.rs +8 -0
- data/src/rules/lint/ambiguous_operator_precedence.rs +18 -1
- data/src/rules/lint/ambiguous_range.rs +15 -1
- data/src/rules/lint/ambiguous_regexp_literal.rs +40 -1
- data/src/rules/lint/array_literal_in_regexp.rs +13 -5
- data/src/rules/lint/assignment_in_condition.rs +3 -2
- data/src/rules/lint/blocks.rs +2 -1
- data/src/rules/lint/boolean_symbol.rs +4 -4
- data/src/rules/lint/constant_definition_in_block.rs +1 -2
- data/src/rules/lint/constant_reassignment.rs +9 -13
- data/src/rules/lint/constant_resolution.rs +8 -0
- data/src/rules/lint/cop_directives.rs +28 -4
- data/src/rules/lint/debugger.rs +59 -9
- data/src/rules/lint/deprecated_open_ssl_constant.rs +1 -2
- data/src/rules/lint/disjunctive_assignment_in_constructor.rs +2 -2
- data/src/rules/lint/duplicate_branch.rs +16 -6
- data/src/rules/lint/duplicate_case_condition.rs +5 -5
- data/src/rules/lint/duplicate_hash_key.rs +8 -4
- data/src/rules/lint/duplicate_match_pattern.rs +7 -5
- data/src/rules/lint/duplicate_methods.rs +133 -54
- data/src/rules/lint/duplicate_require.rs +1 -2
- data/src/rules/lint/duplicate_set_element.rs +19 -5
- data/src/rules/lint/empty_conditional_body.rs +19 -3
- data/src/rules/lint/empty_expression.rs +16 -1
- data/src/rules/lint/empty_in_pattern.rs +3 -2
- data/src/rules/lint/empty_when.rs +4 -4
- data/src/rules/lint/ensure_return.rs +3 -2
- data/src/rules/lint/float_comparison.rs +8 -6
- data/src/rules/lint/flow.rs +8 -5
- data/src/rules/lint/format_parameter_mismatch.rs +4 -3
- data/src/rules/lint/hash_compare_by_identity.rs +5 -4
- data/src/rules/lint/heredoc_method_call_position.rs +3 -2
- data/src/rules/lint/implicit_string_concatenation.rs +33 -3
- data/src/rules/lint/ineffective_access_modifier.rs +5 -4
- data/src/rules/lint/inherit_exception.rs +4 -3
- data/src/rules/lint/literal_as_condition.rs +51 -31
- data/src/rules/lint/literal_assignment_in_condition.rs +4 -3
- data/src/rules/lint/literal_in_interpolation.rs +66 -5
- data/src/rules/lint/literals.rs +10 -8
- data/src/rules/lint/missing_cop_enable_directive.rs +95 -18
- data/src/rules/lint/mod.rs +1 -0
- data/src/rules/lint/nested_method_definition.rs +36 -11
- data/src/rules/lint/nil_receiver.rs +420 -0
- data/src/rules/lint/no_return_in_begin_end_blocks.rs +2 -2
- data/src/rules/lint/node_equality.rs +8 -5
- data/src/rules/lint/non_atomic_file_operation.rs +9 -3
- data/src/rules/lint/non_deterministic_require_order.rs +5 -3
- data/src/rules/lint/non_local_exit_from_iterator.rs +22 -9
- data/src/rules/lint/number_conversion.rs +1 -1
- data/src/rules/lint/out_of_range_regexp_ref.rs +6 -4
- data/src/rules/lint/parentheses_as_grouped_expression.rs +5 -0
- data/src/rules/lint/redundant_cop_enable_directive.rs +3 -1
- data/src/rules/lint/redundant_dir_glob_sort.rs +1 -2
- data/src/rules/lint/redundant_safe_navigation.rs +47 -22
- data/src/rules/lint/redundant_splat_expansion.rs +4 -2
- data/src/rules/lint/redundant_string_coercion.rs +3 -2
- data/src/rules/lint/redundant_type_conversion.rs +16 -4
- data/src/rules/lint/regexp.rs +1 -3
- data/src/rules/lint/regexp_source.rs +2 -2
- data/src/rules/lint/regexp_tree.rs +38 -5
- data/src/rules/lint/require_range_parentheses.rs +5 -5
- data/src/rules/lint/rescue_exception.rs +4 -3
- data/src/rules/lint/rescue_type.rs +3 -2
- data/src/rules/lint/return_in_void_context.rs +8 -3
- data/src/rules/lint/safe_navigation_chain.rs +47 -11
- data/src/rules/lint/self_assignment.rs +20 -2
- data/src/rules/lint/shadowed_argument.rs +2 -2
- data/src/rules/lint/shadowed_exception.rs +2 -2
- data/src/rules/lint/suppressed_exception.rs +21 -3
- data/src/rules/lint/suppressed_exception_in_number_conversion.rs +4 -3
- data/src/rules/lint/symbol_conversion.rs +56 -8
- data/src/rules/lint/syntax.rs +195 -5
- data/src/rules/lint/top_level_return_with_argument.rs +11 -1
- data/src/rules/lint/triple_quotes.rs +3 -2
- data/src/rules/lint/unescaped_bracket_in_regexp.rs +1 -1
- data/src/rules/lint/unexpected_block_arity.rs +23 -5
- data/src/rules/lint/unified_integer.rs +18 -12
- data/src/rules/lint/unmodified_reduce_accumulator.rs +36 -7
- data/src/rules/lint/unreachable_code.rs +4 -4
- data/src/rules/lint/unreachable_loop.rs +26 -10
- data/src/rules/lint/unreachable_pattern_branch.rs +7 -5
- data/src/rules/lint/unused_block_argument.rs +2 -2
- data/src/rules/lint/unused_method_argument.rs +1 -2
- data/src/rules/lint/useless_assignment.rs +3 -2
- data/src/rules/lint/useless_constant_scoping.rs +3 -2
- data/src/rules/lint/useless_else_without_rescue.rs +4 -3
- data/src/rules/lint/useless_rescue.rs +5 -4
- data/src/rules/lint/useless_ruby2_keywords.rs +2 -1
- data/src/rules/lint/useless_setter_call.rs +5 -4
- data/src/rules/lint/useless_times.rs +4 -3
- data/src/rules/lint/variable_force.rs +307 -79
- data/src/rules/lint/void.rs +32 -20
- data/src/rules/metrics/abc_size.rs +197 -7
- data/src/rules/metrics/block_length.rs +19 -10
- data/src/rules/metrics/block_nesting.rs +1 -2
- data/src/rules/metrics/class_length.rs +16 -4
- data/src/rules/metrics/collection_literal_length.rs +2 -1
- data/src/rules/metrics/complexity.rs +52 -14
- data/src/rules/metrics/fragments.rs +10 -14
- data/src/rules/metrics/locals.rs +35 -14
- data/src/rules/metrics/method_length.rs +14 -6
- data/src/rules/metrics/parameter_lists.rs +7 -6
- data/src/rules/metrics/perceived_complexity.rs +1 -3
- data/src/rules/metrics/support.rs +94 -4
- data/src/rules/mod.rs +525 -80
- data/src/rules/naming/ascii_identifiers.rs +3 -3
- data/src/rules/naming/binary_operator_parameter_name.rs +5 -4
- data/src/rules/naming/block_forwarding.rs +9 -8
- data/src/rules/naming/constant_name.rs +56 -18
- data/src/rules/naming/file_name.rs +174 -43
- data/src/rules/naming/method_name.rs +8 -1
- data/src/rules/naming/predicate_method.rs +42 -14
- data/src/rules/naming/predicate_prefix.rs +70 -4
- data/src/rules/naming/rescued_exceptions_variable_name.rs +14 -11
- data/src/rules/naming/support.rs +54 -295
- data/src/rules/naming/variable_name.rs +17 -4
- data/src/rules/naming/variable_number.rs +9 -10
- data/src/rules/node_ext.rs +1 -1
- data/src/rules/ruby_literal.rs +8 -3
- data/src/rules/security/compound_hash.rs +3 -2
- data/src/rules/security/open.rs +5 -4
- data/src/rules/send_node.rs +134 -0
- data/src/rules/single_line.rs +4 -4
- data/src/rules/style/access_modifier_declarations.rs +76 -22
- data/src/rules/style/accessor_grouping.rs +44 -6
- data/src/rules/style/alias.rs +4 -3
- data/src/rules/style/and_or.rs +2 -2
- data/src/rules/style/arguments_forwarding.rs +5 -5
- data/src/rules/style/array_coercion.rs +5 -5
- data/src/rules/style/array_first_last.rs +19 -4
- data/src/rules/style/array_intersect.rs +26 -8
- data/src/rules/style/array_intersect_with_single_element.rs +1 -1
- data/src/rules/style/ascii_comments.rs +2 -0
- data/src/rules/style/attr.rs +1 -1
- data/src/rules/style/auto_resource_cleanup.rs +1 -1
- data/src/rules/style/bitwise_predicate.rs +2 -2
- data/src/rules/style/block_args.rs +1 -1
- data/src/rules/style/block_comments.rs +11 -2
- data/src/rules/style/block_delimiters.rs +154 -10
- data/src/rules/style/case_equality.rs +7 -2
- data/src/rules/style/case_like_if.rs +26 -2
- data/src/rules/style/class_and_module_children.rs +162 -10
- data/src/rules/style/class_methods.rs +1 -1
- data/src/rules/style/class_methods_definitions.rs +9 -0
- data/src/rules/style/collection_compact.rs +4 -4
- data/src/rules/style/combinable_defined.rs +2 -2
- data/src/rules/style/combinable_loops.rs +9 -11
- data/src/rules/style/comments.rs +6 -3
- data/src/rules/style/comparable_clamp.rs +2 -2
- data/src/rules/style/concat_array_literals.rs +3 -3
- data/src/rules/style/conditional.rs +12 -1
- data/src/rules/style/conditional_assignment.rs +133 -4
- data/src/rules/style/constant_visibility.rs +3 -3
- data/src/rules/style/copyright.rs +64 -3
- data/src/rules/style/data_inheritance.rs +1 -1
- data/src/rules/style/def_with_parentheses.rs +1 -1
- data/src/rules/style/document_dynamic_eval_definition.rs +50 -12
- data/src/rules/style/documentation.rs +1 -1
- data/src/rules/style/each_for_simple_loop.rs +1 -1
- data/src/rules/style/each_with_object.rs +4 -4
- data/src/rules/style/empty_block_parameter.rs +1 -1
- data/src/rules/style/empty_case_condition.rs +6 -6
- data/src/rules/style/empty_class_definition.rs +1 -1
- data/src/rules/style/empty_else.rs +1 -1
- data/src/rules/style/empty_heredoc.rs +14 -1
- data/src/rules/style/empty_lambda_parameter.rs +1 -1
- data/src/rules/style/empty_literal.rs +34 -2
- data/src/rules/style/empty_method.rs +1 -1
- data/src/rules/style/empty_string_inside_interpolation.rs +1 -1
- data/src/rules/style/endless.rs +11 -3
- data/src/rules/style/env_home.rs +1 -1
- data/src/rules/style/eval_with_location.rs +6 -0
- data/src/rules/style/even_odd.rs +1 -1
- data/src/rules/style/exact_regexp_match.rs +1 -1
- data/src/rules/style/expand_path_arguments.rs +3 -3
- data/src/rules/style/explicit_block_argument.rs +3 -3
- data/src/rules/style/file_open.rs +5 -0
- data/src/rules/style/file_open_call.rs +3 -3
- data/src/rules/style/file_write.rs +1 -1
- data/src/rules/style/for.rs +1 -1
- data/src/rules/style/format_sequences.rs +6 -0
- data/src/rules/style/format_string.rs +75 -11
- data/src/rules/style/format_string_token.rs +61 -12
- data/src/rules/style/frozen_string_literal_comment.rs +62 -4
- data/src/rules/style/guard_clause.rs +8 -7
- data/src/rules/style/hash_as_last_array_item.rs +4 -4
- data/src/rules/style/hash_each_methods.rs +1 -1
- data/src/rules/style/hash_like_case.rs +8 -8
- data/src/rules/style/hash_lookup_method.rs +43 -3
- data/src/rules/style/hash_subset.rs +16 -10
- data/src/rules/style/hash_syntax.rs +609 -71
- data/src/rules/style/hash_transform.rs +20 -18
- data/src/rules/style/hash_transform_keys.rs +7 -0
- data/src/rules/style/hash_transform_values.rs +7 -0
- data/src/rules/style/identical_conditional_branches.rs +54 -5
- data/src/rules/style/if_inside_else.rs +19 -3
- data/src/rules/style/if_unless_modifier.rs +13 -14
- data/src/rules/style/if_with_boolean_literal_branches.rs +1 -1
- data/src/rules/style/if_with_semicolon.rs +7 -1
- data/src/rules/style/implicit_runtime_error.rs +7 -0
- data/src/rules/style/in_pattern_then.rs +2 -2
- data/src/rules/style/infinite_loop.rs +6 -0
- data/src/rules/style/inline_comment.rs +4 -1
- data/src/rules/style/inverse_methods.rs +12 -2
- data/src/rules/style/invertible_unless_condition.rs +12 -12
- data/src/rules/style/it_block_parameter.rs +5 -2
- data/src/rules/style/keyword_arguments_merging.rs +8 -2
- data/src/rules/style/keyword_parameters_order.rs +1 -1
- data/src/rules/style/line_end_concatenation.rs +1 -1
- data/src/rules/style/magic_comment_format.rs +14 -6
- data/src/rules/style/map_chain.rs +1 -1
- data/src/rules/style/map_compact_with_conditional_block.rs +3 -3
- data/src/rules/style/map_into_array.rs +7 -7
- data/src/rules/style/map_join.rs +3 -3
- data/src/rules/style/method_call_with_args_parentheses.rs +7 -7
- data/src/rules/style/method_call_without_args_parentheses.rs +2 -2
- data/src/rules/style/method_def_parentheses.rs +2 -3
- data/src/rules/style/min_max.rs +3 -3
- data/src/rules/style/min_max_comparison.rs +2 -2
- data/src/rules/style/missing_else.rs +1 -1
- data/src/rules/style/missing_respond_to_missing.rs +1 -1
- data/src/rules/style/mixin_grouping.rs +6 -7
- data/src/rules/style/multiline_block_chain.rs +6 -8
- data/src/rules/style/multiline_if_then.rs +1 -1
- data/src/rules/style/multiline_in_pattern_then.rs +16 -3
- data/src/rules/style/multiline_method_signature.rs +1 -1
- data/src/rules/style/multiline_when_then.rs +2 -2
- data/src/rules/style/mutable_constant.rs +31 -35
- data/src/rules/style/negated_if.rs +2 -2
- data/src/rules/style/negated_if_else_condition.rs +5 -3
- data/src/rules/style/negated_while.rs +1 -1
- data/src/rules/style/negative_array_index.rs +25 -3
- data/src/rules/style/nested_modifier.rs +1 -1
- data/src/rules/style/nested_parenthesized_calls.rs +2 -2
- data/src/rules/style/nested_ternary_operator.rs +2 -2
- data/src/rules/style/next.rs +12 -12
- data/src/rules/style/nil_lambda.rs +2 -2
- data/src/rules/style/nodes.rs +21 -0
- data/src/rules/style/non_nil_check.rs +82 -2
- data/src/rules/style/numbered_parameters.rs +4 -1
- data/src/rules/style/numbered_parameters_limit.rs +2 -2
- data/src/rules/style/numeric_literals.rs +17 -1
- data/src/rules/style/numeric_predicate.rs +25 -6
- data/src/rules/style/one_line_conditional.rs +24 -7
- data/src/rules/style/operator_method_call.rs +5 -0
- data/src/rules/style/optional_arguments.rs +1 -1
- data/src/rules/style/or_assignment.rs +1 -1
- data/src/rules/style/parallel_assignment.rs +20 -9
- data/src/rules/style/parens.rs +1 -1
- data/src/rules/style/parentheses_around_condition.rs +3 -3
- data/src/rules/style/partition_instead_of_double_select.rs +3 -3
- data/src/rules/style/percent.rs +18 -9
- data/src/rules/style/percent_q_literals.rs +40 -7
- data/src/rules/style/preferred_hash_methods.rs +1 -1
- data/src/rules/style/quoted_symbols.rs +6 -0
- data/src/rules/style/raise_args.rs +26 -20
- data/src/rules/style/random_with_offset.rs +1 -1
- data/src/rules/style/reduce_to_hash.rs +6 -6
- data/src/rules/style/redundant_argument.rs +1 -1
- data/src/rules/style/redundant_assignment.rs +5 -5
- data/src/rules/style/redundant_begin.rs +5 -3
- data/src/rules/style/redundant_capital_w.rs +3 -2
- data/src/rules/style/redundant_condition.rs +17 -8
- data/src/rules/style/redundant_current_directory_in_path.rs +2 -2
- data/src/rules/style/redundant_double_splat_hash_braces.rs +15 -5
- data/src/rules/style/redundant_exception.rs +3 -1
- data/src/rules/style/redundant_file_extension_in_require.rs +2 -2
- data/src/rules/style/redundant_format.rs +98 -20
- data/src/rules/style/redundant_freeze.rs +1 -1
- data/src/rules/style/redundant_heredoc_delimiter_quotes.rs +1 -1
- data/src/rules/style/redundant_initialize.rs +6 -3
- data/src/rules/style/redundant_interpolation.rs +8 -8
- data/src/rules/style/redundant_interpolation_unfreeze.rs +10 -3
- data/src/rules/style/redundant_line_continuation.rs +10 -7
- data/src/rules/style/redundant_parentheses.rs +96 -19
- data/src/rules/style/redundant_regexp_argument.rs +1 -1
- data/src/rules/style/redundant_regexp_character_class.rs +4 -3
- data/src/rules/style/redundant_regexp_escape.rs +3 -2
- data/src/rules/style/redundant_return.rs +1 -2
- data/src/rules/style/redundant_self.rs +28 -16
- data/src/rules/style/redundant_sort.rs +1 -1
- data/src/rules/style/redundant_sort_by.rs +1 -1
- data/src/rules/style/redundant_string_escape.rs +6 -5
- data/src/rules/style/regexp_literal.rs +3 -2
- data/src/rules/style/require_order.rs +2 -2
- data/src/rules/style/rescue_modifier.rs +9 -4
- data/src/rules/style/rescue_standard_error.rs +1 -1
- data/src/rules/style/return_nil.rs +2 -2
- data/src/rules/style/return_nil_in_predicate_method_definition.rs +2 -2
- data/src/rules/style/safe_navigation.rs +2 -2
- data/src/rules/style/safe_navigation_chain_length.rs +8 -1
- data/src/rules/style/sample.rs +3 -3
- data/src/rules/style/select_by.rs +6 -2
- data/src/rules/style/select_by_range.rs +1 -1
- data/src/rules/style/semicolon.rs +77 -4
- data/src/rules/style/signal_exception.rs +60 -3
- data/src/rules/style/single_argument_dig.rs +10 -2
- data/src/rules/style/single_line_block_params.rs +14 -4
- data/src/rules/style/single_line_do_end_block.rs +13 -12
- data/src/rules/style/single_line_methods.rs +2 -2
- data/src/rules/style/slicing_with_range.rs +2 -2
- data/src/rules/style/sole_nested_conditional.rs +4 -4
- data/src/rules/style/special_global_vars.rs +2 -2
- data/src/rules/style/stabby_lambda_parentheses.rs +1 -1
- data/src/rules/style/statement_modifier.rs +3 -2
- data/src/rules/style/static_class.rs +7 -1
- data/src/rules/style/string_concatenation.rs +12 -7
- data/src/rules/style/string_hash_keys.rs +3 -2
- data/src/rules/style/string_literals.rs +3 -3
- data/src/rules/style/string_literals_in_interpolation.rs +4 -1
- data/src/rules/style/struct_inheritance.rs +3 -3
- data/src/rules/style/super_arguments.rs +15 -7
- data/src/rules/style/super_with_args_parentheses.rs +49 -12
- data/src/rules/style/swap_values.rs +1 -1
- data/src/rules/style/symbol_array.rs +9 -2
- data/src/rules/style/symbol_proc.rs +3 -3
- data/src/rules/style/tally_method.rs +4 -4
- data/src/rules/style/ternary_parentheses.rs +30 -15
- data/src/rules/style/top_level_method_definition.rs +10 -5
- data/src/rules/style/trailing_body.rs +1 -1
- data/src/rules/style/trailing_comma.rs +206 -10
- data/src/rules/style/trailing_comma_in_arguments.rs +9 -10
- data/src/rules/style/trailing_comma_in_array_literal.rs +2 -1
- data/src/rules/style/trailing_comma_in_hash_literal.rs +2 -1
- data/src/rules/style/trailing_method_end_statement.rs +6 -1
- data/src/rules/style/trailing_underscore_variable.rs +4 -5
- data/src/rules/style/trivial_accessors.rs +2 -2
- data/src/rules/style/unless_logical_operators.rs +1 -1
- data/src/rules/style/unpack_first.rs +11 -2
- data/src/rules/style/when_then.rs +4 -4
- data/src/rules/style/while_until_do.rs +1 -1
- data/src/rules/style/while_until_modifier.rs +3 -3
- data/src/rules/style/word_array.rs +9 -7
- data/src/rules/style/yoda_condition.rs +2 -2
- data/src/rules/style/zero_length_predicate.rs +5 -0
- data/src/rules/support.rs +128 -11
- data/src/source.rs +18 -15
- metadata +5 -1
data/src/config/inheritance.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
use std::collections::HashSet;
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use std::fmt::Display;
|
|
2
3
|
use std::fs;
|
|
3
4
|
use std::path::{Path, PathBuf};
|
|
4
5
|
use std::process::Command;
|
|
@@ -7,17 +8,55 @@ use std::time::Duration;
|
|
|
7
8
|
use anyhow::{Context, Result, bail};
|
|
8
9
|
use serde_yaml_ng::{Mapping, Value};
|
|
9
10
|
|
|
11
|
+
const MAX_REMOTE_CONFIG_BYTES: u64 = 5 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
/// Configuration files whose inheritance has already been resolved, keyed by canonical path for
|
|
14
|
+
/// local files and by URL for remote ones.
|
|
15
|
+
///
|
|
16
|
+
/// `visited` cannot double as this memo: it is a recursion *stack*, popped again on the way out so
|
|
17
|
+
/// that a file legitimately reachable through two different branches still resolves. Without a
|
|
18
|
+
/// separate memo every additional mention of a file re-reads, re-parses and re-merges its whole
|
|
19
|
+
/// subtree, so a chain in which each level names the next one twice costs 2^depth file reads --
|
|
20
|
+
/// or, through `load_remote_with_inheritance`, 2^depth HTTP requests.
|
|
21
|
+
#[derive(Default)]
|
|
22
|
+
struct ResolvedConfigs {
|
|
23
|
+
local: HashMap<PathBuf, Value>,
|
|
24
|
+
remote: HashMap<String, Value>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[cfg(test)]
|
|
28
|
+
thread_local! {
|
|
29
|
+
/// Counts the configuration documents actually read and parsed, so the memoisation tests can
|
|
30
|
+
/// assert on reads rather than on wall-clock time. Thread-local because the test harness gives
|
|
31
|
+
/// every test its own thread.
|
|
32
|
+
static PARSED_CONFIG_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>) -> Result<Value> {
|
|
36
|
+
load_local_with_inheritance(path, visited, &mut ResolvedConfigs::default())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn load_local_with_inheritance(
|
|
40
|
+
path: &Path,
|
|
41
|
+
visited: &mut HashSet<PathBuf>,
|
|
42
|
+
resolved: &mut ResolvedConfigs,
|
|
43
|
+
) -> Result<Value> {
|
|
11
44
|
let canonical = fs::canonicalize(path)
|
|
12
45
|
.with_context(|| format!("configuration file not found: {}", path.display()))?;
|
|
46
|
+
// Only fully resolved files land in the memo, so a hit can never be a file that is still on
|
|
47
|
+
// the recursion stack -- genuine cycles keep reaching the `visited` check below.
|
|
48
|
+
if let Some(cached) = resolved.local.get(&canonical) {
|
|
49
|
+
return Ok(cached.clone());
|
|
50
|
+
}
|
|
13
51
|
if !visited.insert(canonical.clone()) {
|
|
14
52
|
bail!("circular inherit_from detected at {}", canonical.display());
|
|
15
53
|
}
|
|
16
54
|
|
|
17
55
|
let contents = fs::read_to_string(&canonical)
|
|
18
56
|
.with_context(|| format!("failed to read configuration: {}", canonical.display()))?;
|
|
19
|
-
|
|
20
|
-
|
|
57
|
+
#[cfg(test)]
|
|
58
|
+
PARSED_CONFIG_COUNT.with(|count| count.set(count.get() + 1));
|
|
59
|
+
let mut current = parse_yaml_configuration(&contents, canonical.display())?;
|
|
21
60
|
let inherit = take_mapping_key(&mut current, "inherit_from");
|
|
22
61
|
let inherit_gem = take_mapping_key(&mut current, "inherit_gem");
|
|
23
62
|
let parent = canonical.parent().unwrap_or(Path::new("."));
|
|
@@ -28,9 +67,9 @@ pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>)
|
|
|
28
67
|
for inherited in inherited_paths {
|
|
29
68
|
if inherited.starts_with("http://") || inherited.starts_with("https://") {
|
|
30
69
|
let mut remote_visited = HashSet::new();
|
|
31
|
-
merged =
|
|
70
|
+
merged = merge_inherited_config(
|
|
32
71
|
merged,
|
|
33
|
-
load_remote_with_inheritance(&inherited, &mut remote_visited)?,
|
|
72
|
+
load_remote_with_inheritance(&inherited, &mut remote_visited, resolved)?,
|
|
34
73
|
);
|
|
35
74
|
continue;
|
|
36
75
|
}
|
|
@@ -40,20 +79,32 @@ pub(super) fn load_with_inheritance(path: &Path, visited: &mut HashSet<PathBuf>)
|
|
|
40
79
|
} else {
|
|
41
80
|
parent.join(inherited)
|
|
42
81
|
};
|
|
43
|
-
merged =
|
|
82
|
+
merged = merge_inherited_config(
|
|
83
|
+
merged,
|
|
84
|
+
load_local_with_inheritance(&inherited, visited, resolved)?,
|
|
85
|
+
);
|
|
44
86
|
}
|
|
45
87
|
|
|
46
88
|
visited.remove(&canonical);
|
|
47
|
-
|
|
89
|
+
let config = merge_inherited_config(merged, current);
|
|
90
|
+
resolved.local.insert(canonical, config.clone());
|
|
91
|
+
Ok(config)
|
|
48
92
|
}
|
|
49
93
|
|
|
50
|
-
fn load_remote_with_inheritance(
|
|
94
|
+
fn load_remote_with_inheritance(
|
|
95
|
+
url: &str,
|
|
96
|
+
visited: &mut HashSet<String>,
|
|
97
|
+
resolved: &mut ResolvedConfigs,
|
|
98
|
+
) -> Result<Value> {
|
|
99
|
+
// Same reasoning as the local memo, except that every avoided lookup is an avoided HTTP GET.
|
|
100
|
+
if let Some(cached) = resolved.remote.get(url) {
|
|
101
|
+
return Ok(cached.clone());
|
|
102
|
+
}
|
|
51
103
|
if !visited.insert(url.to_owned()) {
|
|
52
104
|
bail!("circular remote inherit_from detected at {url}");
|
|
53
105
|
}
|
|
54
106
|
let contents = fetch_remote_config(url)?;
|
|
55
|
-
let mut current
|
|
56
|
-
serde_yaml_ng::from_str(&contents).with_context(|| format!("invalid YAML from {url}"))?;
|
|
107
|
+
let mut current = parse_yaml_configuration(&contents, url)?;
|
|
57
108
|
let inherit = take_mapping_key(&mut current, "inherit_from");
|
|
58
109
|
let mut merged = Value::Mapping(Mapping::new());
|
|
59
110
|
for inherited in parse_inherit_paths(inherit)? {
|
|
@@ -63,13 +114,36 @@ fn load_remote_with_inheritance(url: &str, visited: &mut HashSet<String>) -> Res
|
|
|
63
114
|
} else {
|
|
64
115
|
join_remote_url(url, &inherited)?
|
|
65
116
|
};
|
|
66
|
-
merged =
|
|
117
|
+
merged = merge_inherited_config(
|
|
67
118
|
merged,
|
|
68
|
-
load_remote_with_inheritance(&inherited_url, visited)?,
|
|
119
|
+
load_remote_with_inheritance(&inherited_url, visited, resolved)?,
|
|
69
120
|
);
|
|
70
121
|
}
|
|
71
122
|
visited.remove(url);
|
|
72
|
-
|
|
123
|
+
let config = merge_inherited_config(merged, current);
|
|
124
|
+
resolved.remote.insert(url.to_owned(), config.clone());
|
|
125
|
+
Ok(config)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Parses one configuration document the way `ConfigLoader#load_yaml_configuration` does.
|
|
129
|
+
///
|
|
130
|
+
/// Upstream normalises the parse result with `hash = yaml_tree_to_hash(yaml_tree) || {}` and then
|
|
131
|
+
/// raises `ValidationError, "Malformed configuration in <path>"` unless what is left is a `Hash`.
|
|
132
|
+
/// Both halves matter. An empty `.rubocop.yml` -- the most common way of saying "the defaults are
|
|
133
|
+
/// fine" -- parses to `Value::Null`, and a stray scalar to `Value::String`; either one would then
|
|
134
|
+
/// win the non-mapping arm of `merge_config` and *replace* the whole `default.yml`-derived
|
|
135
|
+
/// configuration, silently re-enabling every cop and dropping `AllCops/Include` and
|
|
136
|
+
/// `AllCops/Exclude` (so `node_modules` and friends start getting linted).
|
|
137
|
+
fn parse_yaml_configuration(contents: &str, origin: impl Display) -> Result<Value> {
|
|
138
|
+
let value: Value =
|
|
139
|
+
serde_yaml_ng::from_str(contents).with_context(|| format!("invalid YAML in {origin}"))?;
|
|
140
|
+
match value {
|
|
141
|
+
// `nil` and `false` are Ruby's only falsy values, so they are exactly what `|| {}` swaps
|
|
142
|
+
// out for an empty hash.
|
|
143
|
+
Value::Null | Value::Bool(false) => Ok(Value::Mapping(Mapping::new())),
|
|
144
|
+
Value::Mapping(_) => Ok(value),
|
|
145
|
+
_ => bail!("Malformed configuration in {origin}"),
|
|
146
|
+
}
|
|
73
147
|
}
|
|
74
148
|
|
|
75
149
|
fn fetch_remote_config(url: &str) -> Result<String> {
|
|
@@ -92,27 +166,42 @@ fn fetch_remote_config(url: &str) -> Result<String> {
|
|
|
92
166
|
response
|
|
93
167
|
.body_mut()
|
|
94
168
|
.with_config()
|
|
95
|
-
|
|
169
|
+
// ureq は指定値と同じ長さでも上限超過にするため、論理上限を含む 1 バイト先を渡す。
|
|
170
|
+
.limit(MAX_REMOTE_CONFIG_BYTES + 1)
|
|
96
171
|
.read_to_string()
|
|
97
172
|
.with_context(|| format!("failed to read remote configuration: {url}"))
|
|
98
173
|
}
|
|
99
174
|
|
|
175
|
+
/// Resolves an `inherit_from` entry of a remote configuration against the URL it was found in,
|
|
176
|
+
/// the way RFC 3986 resolves a relative reference: the last path segment of the base is replaced,
|
|
177
|
+
/// and a query string or fragment of the base is dropped rather than carried over.
|
|
178
|
+
///
|
|
179
|
+
/// Everything after the authority has to be located explicitly. Searching the whole URL for the
|
|
180
|
+
/// last `/` mistakes the second slash of `https://` for a directory separator when the base
|
|
181
|
+
/// carries no path at all -- turning `https://example.com` + `shared.yml` into
|
|
182
|
+
/// `https://shared.yml` -- and mistakes a slash inside a query string for one as well.
|
|
100
183
|
fn join_remote_url(base: &str, relative: &str) -> Result<String> {
|
|
184
|
+
let scheme_end = base
|
|
185
|
+
.find("://")
|
|
186
|
+
.context("remote configuration URL has no scheme")?
|
|
187
|
+
+ 3;
|
|
188
|
+
let hierarchical_end = base[scheme_end..]
|
|
189
|
+
.find(['?', '#'])
|
|
190
|
+
.map_or(base.len(), |offset| scheme_end + offset);
|
|
191
|
+
let hierarchical = &base[..hierarchical_end];
|
|
192
|
+
let authority_end = hierarchical[scheme_end..]
|
|
193
|
+
.find('/')
|
|
194
|
+
.map_or(hierarchical.len(), |offset| scheme_end + offset);
|
|
101
195
|
if relative.starts_with('/') {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
let directory_end = base
|
|
112
|
-
.rfind('/')
|
|
113
|
-
.context("remote configuration URL has no directory")?
|
|
114
|
-
+ 1;
|
|
115
|
-
Ok(format!("{}{}", &base[..directory_end], relative))
|
|
196
|
+
return Ok(format!("{}{relative}", &hierarchical[..authority_end]));
|
|
197
|
+
}
|
|
198
|
+
match hierarchical[authority_end..].rfind('/') {
|
|
199
|
+
Some(offset) => Ok(format!(
|
|
200
|
+
"{}{relative}",
|
|
201
|
+
&hierarchical[..authority_end + offset + 1]
|
|
202
|
+
)),
|
|
203
|
+
None => Ok(format!("{hierarchical}/{relative}")),
|
|
204
|
+
}
|
|
116
205
|
}
|
|
117
206
|
|
|
118
207
|
fn parse_inherit_paths(value: Option<Value>) -> Result<Vec<String>> {
|
|
@@ -166,21 +255,47 @@ fn take_mapping_key(value: &mut Value, key: &str) -> Option<Value> {
|
|
|
166
255
|
.remove(Value::String(key.to_owned()))
|
|
167
256
|
}
|
|
168
257
|
|
|
258
|
+
/// Where `should_union?` gets its "root mode" from.
|
|
259
|
+
///
|
|
260
|
+
/// RuboCop merges configurations through two entry points that answer this differently, and
|
|
261
|
+
/// nothing else about the two merges differs, so the whole difference lives in this choice.
|
|
262
|
+
enum RootMode {
|
|
263
|
+
/// `ConfigLoaderResolver#merge_with_default` passes `config['inherit_mode'] || {}` -- the
|
|
264
|
+
/// file-level directive -- unchanged for every key it merges.
|
|
265
|
+
File,
|
|
266
|
+
/// `ConfigLoaderResolver#resolve_inheritance` passes `determine_inherit_mode(hash, cop)`,
|
|
267
|
+
/// which is `local_inherit || hash['inherit_mode'] || {}`: a cop's own directive *replaces*
|
|
268
|
+
/// the file-level one, and since an empty Hash is truthy in Ruby it replaces it even when it
|
|
269
|
+
/// selects nothing.
|
|
270
|
+
PerCop,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Merges a user configuration over the `default.yml`-derived one, mirroring
|
|
274
|
+
/// `ConfigLoaderResolver#merge_with_default`.
|
|
169
275
|
pub(super) fn merge_config(base: Value, overlay: Value) -> Value {
|
|
170
|
-
|
|
276
|
+
merge_mappings(base, overlay, &RootMode::File)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Merges an inheriting configuration over the one it inherits from, mirroring
|
|
280
|
+
/// `ConfigLoaderResolver#resolve_inheritance`.
|
|
281
|
+
fn merge_inherited_config(base: Value, overlay: Value) -> Value {
|
|
282
|
+
merge_mappings(base, overlay, &RootMode::PerCop)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn merge_mappings(base: Value, overlay: Value, root: &RootMode) -> Value {
|
|
171
286
|
match (base, overlay) {
|
|
172
287
|
(Value::Mapping(mut base), Value::Mapping(overlay)) => {
|
|
288
|
+
let file_mode = inherit_mode(&overlay).unwrap_or_default();
|
|
173
289
|
for (key, value) in overlay {
|
|
174
|
-
let
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} else {
|
|
178
|
-
&local_merge
|
|
290
|
+
let cop_mode = match root {
|
|
291
|
+
RootMode::File => None,
|
|
292
|
+
RootMode::PerCop => value.as_mapping().and_then(inherit_mode),
|
|
179
293
|
};
|
|
294
|
+
let mode = cop_mode.as_ref().unwrap_or(&file_mode);
|
|
180
295
|
// `map_or` evaluates its default eagerly, so every key paid for a deep
|
|
181
296
|
// clone even though most keys are absent from `base`.
|
|
182
297
|
let merged = match base.remove(&key) {
|
|
183
|
-
Some(old) => deep_merge(old, value,
|
|
298
|
+
Some(old) => deep_merge(old, value, mode),
|
|
184
299
|
None => value,
|
|
185
300
|
};
|
|
186
301
|
base.insert(key, merged);
|
|
@@ -191,21 +306,25 @@ pub(super) fn merge_config(base: Value, overlay: Value) -> Value {
|
|
|
191
306
|
}
|
|
192
307
|
}
|
|
193
308
|
|
|
194
|
-
fn deep_merge(base: Value, overlay: Value,
|
|
309
|
+
fn deep_merge(base: Value, overlay: Value, root_mode: &InheritMode) -> Value {
|
|
195
310
|
match (base, overlay) {
|
|
196
311
|
(Value::Mapping(mut base), Value::Mapping(overlay)) => {
|
|
312
|
+
// `should_union?` reads `inherit_mode` out of *both* hashes at the level it is
|
|
313
|
+
// merging, so an inherited configuration can opt its own parameters into unioning
|
|
314
|
+
// without the inheriting file knowing about it.
|
|
315
|
+
let base_mode = inherit_mode(&base);
|
|
316
|
+
let overlay_mode = inherit_mode(&overlay);
|
|
197
317
|
for (key, value) in overlay {
|
|
198
|
-
let
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
(None, new) => new,
|
|
318
|
+
let merged = match base.remove(&key) {
|
|
319
|
+
Some(old) => merge_entry(
|
|
320
|
+
key.as_str(),
|
|
321
|
+
old,
|
|
322
|
+
value,
|
|
323
|
+
overlay_mode.as_ref(),
|
|
324
|
+
base_mode.as_ref(),
|
|
325
|
+
root_mode,
|
|
326
|
+
),
|
|
327
|
+
None => value,
|
|
209
328
|
};
|
|
210
329
|
base.insert(key, merged);
|
|
211
330
|
}
|
|
@@ -215,42 +334,227 @@ fn deep_merge(base: Value, overlay: Value, merge_keys: &HashSet<String>) -> Valu
|
|
|
215
334
|
}
|
|
216
335
|
}
|
|
217
336
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
337
|
+
/// Merges one key of a mapping, mirroring the `merge_hashes?` / `should_union?` / plain-overwrite
|
|
338
|
+
/// ladder of `ConfigLoaderResolver#merge`.
|
|
339
|
+
fn merge_entry(
|
|
340
|
+
key: Option<&str>,
|
|
341
|
+
base: Value,
|
|
342
|
+
overlay: Value,
|
|
343
|
+
overlay_mode: Option<&InheritMode>,
|
|
344
|
+
base_mode: Option<&InheritMode>,
|
|
345
|
+
root_mode: &InheritMode,
|
|
346
|
+
) -> Value {
|
|
347
|
+
// `merge_hashes?` is tested before `should_union?`, so two mappings always recurse.
|
|
348
|
+
if base.is_mapping() && overlay.is_mapping() {
|
|
349
|
+
return deep_merge(base, overlay, root_mode);
|
|
350
|
+
}
|
|
351
|
+
if should_union(key, &base, &overlay, overlay_mode, base_mode, root_mode) {
|
|
352
|
+
return union_sequences(base, overlay);
|
|
353
|
+
}
|
|
354
|
+
overlay
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// Mirrors `ConfigLoaderResolver#should_union?`.
|
|
358
|
+
///
|
|
359
|
+
/// The precedence chain is what makes `override` usable at all: the derived side is consulted
|
|
360
|
+
/// first and an `override` entry there stops the search with "replace", which is the only way to
|
|
361
|
+
/// cancel a root-level `merge`. The base side gets its turn next, so an inherited file can request
|
|
362
|
+
/// unioning for its own parameters, and only then does the root mode decide.
|
|
363
|
+
fn should_union(
|
|
364
|
+
key: Option<&str>,
|
|
365
|
+
base: &Value,
|
|
366
|
+
derived: &Value,
|
|
367
|
+
derived_mode: Option<&InheritMode>,
|
|
368
|
+
base_mode: Option<&InheritMode>,
|
|
369
|
+
root_mode: &InheritMode,
|
|
370
|
+
) -> bool {
|
|
371
|
+
if !base.is_sequence() && !derived.is_sequence() {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
let Some(key) = key else {
|
|
375
|
+
return false;
|
|
227
376
|
};
|
|
228
|
-
mode.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
377
|
+
if let Some(decision) = derived_mode.and_then(|mode| mode.decide(key)) {
|
|
378
|
+
return decision;
|
|
379
|
+
}
|
|
380
|
+
if let Some(decision) = base_mode.and_then(|mode| mode.decide(key)) {
|
|
381
|
+
return decision;
|
|
382
|
+
}
|
|
383
|
+
root_mode.merge_keys.contains(key)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// Mirrors `Array(base_hash[key]) | Array(derived_hash[key])`, the union `ConfigLoaderResolver#merge`
|
|
387
|
+
/// performs once `should_union?` agrees. Ruby's `|` keeps the first occurrence of each value, so a
|
|
388
|
+
/// path listed on both sides ends up in the result once.
|
|
389
|
+
fn union_sequences(base: Value, derived: Value) -> Value {
|
|
390
|
+
let mut union: Vec<Value> = Vec::new();
|
|
391
|
+
for item in to_sequence(base).into_iter().chain(to_sequence(derived)) {
|
|
392
|
+
if !union.contains(&item) {
|
|
393
|
+
union.push(item);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
Value::Sequence(union)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// Mirrors Ruby's `Array()`, which turns `nil` into `[]` and wraps anything that is not already an
|
|
400
|
+
/// array. `should_union?` only requires *one* of the two sides to be an array, so the other one is
|
|
401
|
+
/// allowed to be a bare scalar or absent.
|
|
402
|
+
fn to_sequence(value: Value) -> Vec<Value> {
|
|
403
|
+
match value {
|
|
404
|
+
Value::Null => Vec::new(),
|
|
405
|
+
Value::Sequence(items) => items,
|
|
406
|
+
other => vec![other],
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// The `merge` and `override` lists of one `inherit_mode` directive.
|
|
411
|
+
#[derive(Debug, Default)]
|
|
412
|
+
struct InheritMode {
|
|
413
|
+
merge_keys: HashSet<String>,
|
|
414
|
+
override_keys: HashSet<String>,
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
impl InheritMode {
|
|
418
|
+
/// `should_override?` and `should_merge?` collapsed into one lookup: `Some(false)` for
|
|
419
|
+
/// "override wins", `Some(true)` for "merge wins", and `None` when this directive says nothing
|
|
420
|
+
/// about the key and the next mode in `should_union?`'s chain gets its turn.
|
|
421
|
+
fn decide(&self, key: &str) -> Option<bool> {
|
|
422
|
+
if self.override_keys.contains(key) {
|
|
423
|
+
Some(false)
|
|
424
|
+
} else if self.merge_keys.contains(key) {
|
|
425
|
+
Some(true)
|
|
426
|
+
} else {
|
|
427
|
+
None
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Reads the `inherit_mode` directive of a single configuration level.
|
|
433
|
+
///
|
|
434
|
+
/// Returning `None` only for a missing (or explicitly null) key is what lets `RootMode::PerCop`
|
|
435
|
+
/// implement `local_inherit || hash['inherit_mode'] || {}` faithfully: a present but empty
|
|
436
|
+
/// directive is truthy in Ruby and therefore still replaces the file-level mode.
|
|
437
|
+
fn inherit_mode(mapping: &Mapping) -> Option<InheritMode> {
|
|
438
|
+
let mode = mapping.get("inherit_mode")?;
|
|
439
|
+
if mode.is_null() {
|
|
440
|
+
return None;
|
|
441
|
+
}
|
|
442
|
+
Some(InheritMode {
|
|
443
|
+
merge_keys: mode_keys(mode, "merge"),
|
|
444
|
+
override_keys: mode_keys(mode, "override"),
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
fn mode_keys(mode: &Value, name: &str) -> HashSet<String> {
|
|
449
|
+
mode.as_mapping()
|
|
450
|
+
.and_then(|mapping| mapping.get(name))
|
|
451
|
+
.and_then(Value::as_sequence)
|
|
452
|
+
.map(|keys| {
|
|
453
|
+
keys.iter()
|
|
454
|
+
.filter_map(Value::as_str)
|
|
455
|
+
.map(ToOwned::to_owned)
|
|
456
|
+
.collect()
|
|
457
|
+
})
|
|
458
|
+
.unwrap_or_default()
|
|
232
459
|
}
|
|
233
460
|
|
|
234
461
|
#[cfg(test)]
|
|
235
462
|
mod tests {
|
|
463
|
+
use std::collections::HashSet;
|
|
236
464
|
use std::fs;
|
|
465
|
+
use std::io::{Read, Write};
|
|
466
|
+
use std::net::TcpListener;
|
|
467
|
+
use std::path::Path;
|
|
468
|
+
use std::thread;
|
|
237
469
|
|
|
238
470
|
use serde_yaml_ng::Value;
|
|
239
471
|
use tempfile::tempdir;
|
|
240
472
|
|
|
241
|
-
use super::{
|
|
473
|
+
use super::{
|
|
474
|
+
InheritMode, MAX_REMOTE_CONFIG_BYTES, PARSED_CONFIG_COUNT, deep_merge, fetch_remote_config,
|
|
475
|
+
join_remote_url, load_with_inheritance, merge_config, merge_inherited_config,
|
|
476
|
+
};
|
|
242
477
|
use crate::config::Config;
|
|
243
478
|
|
|
244
479
|
fn yaml(text: &str) -> Value {
|
|
245
480
|
serde_yaml_ng::from_str(text).unwrap()
|
|
246
481
|
}
|
|
247
482
|
|
|
483
|
+
/// Number of configuration documents parsed since the last call, on this thread.
|
|
484
|
+
fn taken_parse_count() -> usize {
|
|
485
|
+
PARSED_CONFIG_COUNT.with(|count| count.replace(0))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
fn resolve(path: &Path) -> anyhow::Result<Value> {
|
|
489
|
+
load_with_inheritance(path, &mut HashSet::new())
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fn serve_once(body: Vec<u8>) -> (String, thread::JoinHandle<()>) {
|
|
493
|
+
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
|
|
494
|
+
let address = listener.local_addr().unwrap();
|
|
495
|
+
let server = thread::spawn(move || {
|
|
496
|
+
let (mut stream, _) = listener.accept().unwrap();
|
|
497
|
+
let mut request = [0; 1024];
|
|
498
|
+
assert_ne!(stream.read(&mut request).unwrap(), 0);
|
|
499
|
+
write!(
|
|
500
|
+
stream,
|
|
501
|
+
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
502
|
+
body.len()
|
|
503
|
+
)
|
|
504
|
+
.unwrap();
|
|
505
|
+
stream.write_all(&body).unwrap();
|
|
506
|
+
});
|
|
507
|
+
(format!("http://{address}/config.yml"), server)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fn cop_exclude(merged: &Value, cop: &str) -> Value {
|
|
511
|
+
merged
|
|
512
|
+
.as_mapping()
|
|
513
|
+
.unwrap()
|
|
514
|
+
.get(cop)
|
|
515
|
+
.unwrap()
|
|
516
|
+
.as_mapping()
|
|
517
|
+
.unwrap()
|
|
518
|
+
.get("Exclude")
|
|
519
|
+
.unwrap()
|
|
520
|
+
.clone()
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/// Writes `.rubocop.yml` plus `f0.yml`..`f{depth}.yml`, where every level names the next one
|
|
524
|
+
/// twice. Resolving that costs 2^depth reads without memoisation and depth + 2 with it.
|
|
525
|
+
fn write_diamond_chain(directory: &Path, depth: usize) {
|
|
526
|
+
fs::write(
|
|
527
|
+
directory.join(".rubocop.yml"),
|
|
528
|
+
"inherit_from:\n - f0.yml\n",
|
|
529
|
+
)
|
|
530
|
+
.unwrap();
|
|
531
|
+
for level in 0..depth {
|
|
532
|
+
fs::write(
|
|
533
|
+
directory.join(format!("f{level}.yml")),
|
|
534
|
+
format!(
|
|
535
|
+
"inherit_from:\n - f{next}.yml\n - f{next}.yml\n",
|
|
536
|
+
next = level + 1
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
.unwrap();
|
|
540
|
+
}
|
|
541
|
+
fs::write(
|
|
542
|
+
directory.join(format!("f{depth}.yml")),
|
|
543
|
+
"Layout/LineLength:\n Max: 42\n",
|
|
544
|
+
)
|
|
545
|
+
.unwrap();
|
|
546
|
+
}
|
|
547
|
+
|
|
248
548
|
#[test]
|
|
249
549
|
fn joins_remote_inherit_urls() {
|
|
250
550
|
assert_eq!(
|
|
251
551
|
join_remote_url("https://example.com/team/base.yml", "shared.yml").unwrap(),
|
|
252
552
|
"https://example.com/team/shared.yml"
|
|
253
553
|
);
|
|
554
|
+
assert_eq!(
|
|
555
|
+
join_remote_url("https://example.com/team/nested/base.yml", "shared.yml").unwrap(),
|
|
556
|
+
"https://example.com/team/nested/shared.yml"
|
|
557
|
+
);
|
|
254
558
|
assert_eq!(
|
|
255
559
|
join_remote_url("https://example.com/team/base.yml", "/root.yml").unwrap(),
|
|
256
560
|
"https://example.com/root.yml"
|
|
@@ -259,10 +563,62 @@ mod tests {
|
|
|
259
563
|
join_remote_url("https://example.com", "/root.yml").unwrap(),
|
|
260
564
|
"https://example.com/root.yml"
|
|
261
565
|
);
|
|
566
|
+
// A base without a path must not have the `//` of its scheme mistaken for a directory.
|
|
567
|
+
assert_eq!(
|
|
568
|
+
join_remote_url("https://example.com", "shared.yml").unwrap(),
|
|
569
|
+
"https://example.com/shared.yml"
|
|
570
|
+
);
|
|
571
|
+
assert_eq!(
|
|
572
|
+
join_remote_url("https://example.com/", "shared.yml").unwrap(),
|
|
573
|
+
"https://example.com/shared.yml"
|
|
574
|
+
);
|
|
575
|
+
assert_eq!(
|
|
576
|
+
join_remote_url("https://example.com/team/", "shared.yml").unwrap(),
|
|
577
|
+
"https://example.com/team/shared.yml"
|
|
578
|
+
);
|
|
579
|
+
// A `/` inside a query string is not a directory separator either.
|
|
580
|
+
assert_eq!(
|
|
581
|
+
join_remote_url(
|
|
582
|
+
"https://example.com/team/base.yml?ref=heads/main",
|
|
583
|
+
"shared.yml"
|
|
584
|
+
)
|
|
585
|
+
.unwrap(),
|
|
586
|
+
"https://example.com/team/shared.yml"
|
|
587
|
+
);
|
|
588
|
+
assert_eq!(
|
|
589
|
+
join_remote_url("https://example.com?ref=heads/main", "shared.yml").unwrap(),
|
|
590
|
+
"https://example.com/shared.yml"
|
|
591
|
+
);
|
|
592
|
+
assert_eq!(
|
|
593
|
+
join_remote_url(
|
|
594
|
+
"https://example.com/team/base.yml?ref=heads/main",
|
|
595
|
+
"/root.yml"
|
|
596
|
+
)
|
|
597
|
+
.unwrap(),
|
|
598
|
+
"https://example.com/root.yml"
|
|
599
|
+
);
|
|
262
600
|
assert!(join_remote_url("example.com/base.yml", "/root.yml").is_err());
|
|
263
601
|
assert!(join_remote_url("no-directory", "shared.yml").is_err());
|
|
264
602
|
}
|
|
265
603
|
|
|
604
|
+
#[test]
|
|
605
|
+
fn remote_configuration_enforces_the_response_size_limit() {
|
|
606
|
+
let limit = MAX_REMOTE_CONFIG_BYTES as usize;
|
|
607
|
+
|
|
608
|
+
let (url, server) = serve_once(vec![b'a'; limit]);
|
|
609
|
+
let accepted = fetch_remote_config(&url).unwrap();
|
|
610
|
+
server.join().unwrap();
|
|
611
|
+
assert_eq!(accepted.len(), limit);
|
|
612
|
+
|
|
613
|
+
let (url, server) = serve_once(vec![b'a'; limit + 1]);
|
|
614
|
+
let error = fetch_remote_config(&url).unwrap_err().to_string();
|
|
615
|
+
server.join().unwrap();
|
|
616
|
+
assert!(
|
|
617
|
+
error.starts_with("failed to read remote configuration:"),
|
|
618
|
+
"unexpected error: {error}"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
266
622
|
#[test]
|
|
267
623
|
fn merges_configurations_without_losing_untouched_keys() {
|
|
268
624
|
let merged = merge_config(
|
|
@@ -296,14 +652,63 @@ mod tests {
|
|
|
296
652
|
#[test]
|
|
297
653
|
fn merge_prefers_scalar_overlays_over_mappings() {
|
|
298
654
|
assert_eq!(merge_config(yaml("A:\n x: 1\n"), yaml("3")), yaml("3"));
|
|
299
|
-
let
|
|
300
|
-
assert_eq!(deep_merge(yaml("[1, 2]"), yaml("[3]"), &
|
|
655
|
+
let mode = InheritMode::default();
|
|
656
|
+
assert_eq!(deep_merge(yaml("[1, 2]"), yaml("[3]"), &mode), yaml("[3]"));
|
|
301
657
|
assert_eq!(
|
|
302
|
-
deep_merge(yaml("a: 1\n"), yaml("b: 2\n"), &
|
|
658
|
+
deep_merge(yaml("a: 1\n"), yaml("b: 2\n"), &mode),
|
|
303
659
|
yaml("a: 1\nb: 2\n")
|
|
304
660
|
);
|
|
305
661
|
}
|
|
306
662
|
|
|
663
|
+
#[test]
|
|
664
|
+
fn per_cop_override_mode_beats_the_root_merge_mode() {
|
|
665
|
+
let merged = merge_inherited_config(
|
|
666
|
+
yaml("Style/A:\n Exclude: [a.rb]\n"),
|
|
667
|
+
yaml(concat!(
|
|
668
|
+
"inherit_mode:\n merge:\n - Exclude\n",
|
|
669
|
+
"Style/A:\n",
|
|
670
|
+
" inherit_mode:\n override:\n - Exclude\n",
|
|
671
|
+
" Exclude: [b.rb]\n",
|
|
672
|
+
)),
|
|
673
|
+
);
|
|
674
|
+
assert_eq!(cop_exclude(&merged, "Style/A"), yaml("[b.rb]"));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#[test]
|
|
678
|
+
fn base_side_per_cop_merge_mode_unions_sequences() {
|
|
679
|
+
let merged = merge_inherited_config(
|
|
680
|
+
yaml("Style/A:\n inherit_mode:\n merge:\n - Exclude\n Exclude: [a.rb]\n"),
|
|
681
|
+
yaml("Style/A:\n Exclude: [b.rb]\n"),
|
|
682
|
+
);
|
|
683
|
+
assert_eq!(cop_exclude(&merged, "Style/A"), yaml("[a.rb, b.rb]"));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/// The two upstream entry points read a present-but-empty per-cop `inherit_mode` differently,
|
|
687
|
+
/// and both readings are load-bearing: `resolve_inheritance` lets it cancel the file-level
|
|
688
|
+
/// directive, while `merge_with_default` never consults it for the root mode at all.
|
|
689
|
+
#[test]
|
|
690
|
+
fn empty_per_cop_inherit_mode_replaces_the_root_mode_only_when_inheriting() {
|
|
691
|
+
let overlay = concat!(
|
|
692
|
+
"inherit_mode:\n merge:\n - Exclude\n",
|
|
693
|
+
"Style/A:\n inherit_mode: {}\n Exclude: [b.rb]\n",
|
|
694
|
+
);
|
|
695
|
+
let inherited =
|
|
696
|
+
merge_inherited_config(yaml("Style/A:\n Exclude: [a.rb]\n"), yaml(overlay));
|
|
697
|
+
assert_eq!(cop_exclude(&inherited, "Style/A"), yaml("[b.rb]"));
|
|
698
|
+
|
|
699
|
+
let defaulted = merge_config(yaml("Style/A:\n Exclude: [a.rb]\n"), yaml(overlay));
|
|
700
|
+
assert_eq!(cop_exclude(&defaulted, "Style/A"), yaml("[a.rb, b.rb]"));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#[test]
|
|
704
|
+
fn union_keeps_each_value_once() {
|
|
705
|
+
let merged = merge_config(
|
|
706
|
+
yaml("Style/A:\n Exclude: [a.rb, b.rb]\n"),
|
|
707
|
+
yaml("inherit_mode:\n merge:\n - Exclude\nStyle/A:\n Exclude: [b.rb, c.rb]\n"),
|
|
708
|
+
);
|
|
709
|
+
assert_eq!(cop_exclude(&merged, "Style/A"), yaml("[a.rb, b.rb, c.rb]"));
|
|
710
|
+
}
|
|
711
|
+
|
|
307
712
|
#[test]
|
|
308
713
|
fn loads_inherited_configuration_and_merges_selected_arrays() {
|
|
309
714
|
let directory = tempdir().unwrap();
|
|
@@ -329,4 +734,172 @@ mod tests {
|
|
|
329
734
|
);
|
|
330
735
|
assert!(!config.rule_enabled("Layout/LineLength"));
|
|
331
736
|
}
|
|
737
|
+
|
|
738
|
+
#[test]
|
|
739
|
+
fn empty_configuration_parses_as_an_empty_mapping() {
|
|
740
|
+
let directory = tempdir().unwrap();
|
|
741
|
+
let path = directory.path().join(".rubocop.yml");
|
|
742
|
+
fs::write(&path, "").unwrap();
|
|
743
|
+
assert_eq!(resolve(&path).unwrap(), yaml("{}"));
|
|
744
|
+
|
|
745
|
+
// A comment-only file is the same story once the comment is stripped.
|
|
746
|
+
fs::write(&path, "# nothing to configure yet\n").unwrap();
|
|
747
|
+
assert_eq!(resolve(&path).unwrap(), yaml("{}"));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
#[test]
|
|
751
|
+
fn empty_configuration_keeps_the_default_configuration() {
|
|
752
|
+
let directory = tempdir().unwrap();
|
|
753
|
+
fs::write(directory.path().join("Gemfile"), "").unwrap();
|
|
754
|
+
fs::write(directory.path().join(".rubocop.yml"), "").unwrap();
|
|
755
|
+
fs::write(directory.path().join("a.rb"), "x = 1\n").unwrap();
|
|
756
|
+
fs::create_dir_all(directory.path().join("node_modules/pkg")).unwrap();
|
|
757
|
+
fs::write(directory.path().join("node_modules/pkg/v.rb"), "y = 2\n").unwrap();
|
|
758
|
+
let config = Config::load(None, directory.path()).unwrap();
|
|
759
|
+
// `AllCops/Exclude` from `default.yml` has to survive, or `node_modules` gets linted.
|
|
760
|
+
assert!(config.path_excluded(&directory.path().join("node_modules/pkg/v.rb")));
|
|
761
|
+
assert!(!config.path_excluded(&directory.path().join("a.rb")));
|
|
762
|
+
// And so do the per-cop defaults.
|
|
763
|
+
assert_eq!(
|
|
764
|
+
config.cop_value::<usize>("Layout/LineLength", "Max"),
|
|
765
|
+
Some(120)
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
#[test]
|
|
770
|
+
fn non_mapping_configuration_is_rejected_as_malformed() {
|
|
771
|
+
let directory = tempdir().unwrap();
|
|
772
|
+
let path = directory.path().join(".rubocop.yml");
|
|
773
|
+
for contents in ["hello\n", "- Style/A\n", "42\n"] {
|
|
774
|
+
fs::write(&path, contents).unwrap();
|
|
775
|
+
let error = resolve(&path).unwrap_err().to_string();
|
|
776
|
+
assert!(
|
|
777
|
+
error.starts_with("Malformed configuration in"),
|
|
778
|
+
"unexpected error for {contents:?}: {error}"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#[test]
|
|
784
|
+
fn diamond_inheritance_reads_every_file_once() {
|
|
785
|
+
let directory = tempdir().unwrap();
|
|
786
|
+
write_diamond_chain(directory.path(), 8);
|
|
787
|
+
taken_parse_count();
|
|
788
|
+
|
|
789
|
+
let resolved = resolve(&directory.path().join(".rubocop.yml")).unwrap();
|
|
790
|
+
|
|
791
|
+
// `.rubocop.yml` plus `f0.yml`..`f8.yml`; without memoisation this would be 2^9 - 1 + 1.
|
|
792
|
+
assert_eq!(taken_parse_count(), 10);
|
|
793
|
+
assert_eq!(
|
|
794
|
+
resolved
|
|
795
|
+
.as_mapping()
|
|
796
|
+
.unwrap()
|
|
797
|
+
.get("Layout/LineLength")
|
|
798
|
+
.unwrap(),
|
|
799
|
+
&yaml("Max: 42\n")
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
#[test]
|
|
804
|
+
fn detects_circular_inheritance() {
|
|
805
|
+
let directory = tempdir().unwrap();
|
|
806
|
+
fs::write(
|
|
807
|
+
directory.path().join(".rubocop.yml"),
|
|
808
|
+
"inherit_from: a.yml\n",
|
|
809
|
+
)
|
|
810
|
+
.unwrap();
|
|
811
|
+
fs::write(directory.path().join("a.yml"), "inherit_from: b.yml\n").unwrap();
|
|
812
|
+
fs::write(directory.path().join("b.yml"), "inherit_from: a.yml\n").unwrap();
|
|
813
|
+
let error = resolve(&directory.path().join(".rubocop.yml"))
|
|
814
|
+
.unwrap_err()
|
|
815
|
+
.to_string();
|
|
816
|
+
assert!(
|
|
817
|
+
error.starts_with("circular inherit_from detected at"),
|
|
818
|
+
"unexpected error: {error}"
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// A file inheriting from itself is the degenerate case of the same check.
|
|
822
|
+
fs::write(directory.path().join("b.yml"), "inherit_from: b.yml\n").unwrap();
|
|
823
|
+
let error = resolve(&directory.path().join(".rubocop.yml"))
|
|
824
|
+
.unwrap_err()
|
|
825
|
+
.to_string();
|
|
826
|
+
assert!(
|
|
827
|
+
error.starts_with("circular inherit_from detected at"),
|
|
828
|
+
"unexpected error: {error}"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
#[test]
|
|
833
|
+
fn override_mode_wins_over_an_inherited_merge_mode_end_to_end() {
|
|
834
|
+
let directory = tempdir().unwrap();
|
|
835
|
+
fs::write(directory.path().join("Gemfile"), "").unwrap();
|
|
836
|
+
fs::write(
|
|
837
|
+
directory.path().join("base.yml"),
|
|
838
|
+
"Layout/LineLength:\n Exclude: [a.rb]\n",
|
|
839
|
+
)
|
|
840
|
+
.unwrap();
|
|
841
|
+
fs::write(
|
|
842
|
+
directory.path().join(".rubocop.yml"),
|
|
843
|
+
concat!(
|
|
844
|
+
"inherit_from: base.yml\n",
|
|
845
|
+
"inherit_mode:\n merge:\n - Exclude\n",
|
|
846
|
+
"Layout/LineLength:\n",
|
|
847
|
+
" inherit_mode:\n override:\n - Exclude\n",
|
|
848
|
+
" Exclude: [b.rb]\n",
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
.unwrap();
|
|
852
|
+
let config = Config::load(None, directory.path()).unwrap();
|
|
853
|
+
assert_eq!(
|
|
854
|
+
config.cop_value::<Vec<String>>("Layout/LineLength", "Exclude"),
|
|
855
|
+
Some(vec!["b.rb".to_owned()])
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
#[test]
|
|
860
|
+
fn inherited_per_cop_merge_mode_wins_end_to_end() {
|
|
861
|
+
let directory = tempdir().unwrap();
|
|
862
|
+
fs::write(directory.path().join("Gemfile"), "").unwrap();
|
|
863
|
+
fs::write(
|
|
864
|
+
directory.path().join("base.yml"),
|
|
865
|
+
"Layout/LineLength:\n inherit_mode:\n merge:\n - Exclude\n Exclude: [a.rb]\n",
|
|
866
|
+
)
|
|
867
|
+
.unwrap();
|
|
868
|
+
fs::write(
|
|
869
|
+
directory.path().join(".rubocop.yml"),
|
|
870
|
+
"inherit_from: base.yml\nLayout/LineLength:\n Exclude: [b.rb]\n",
|
|
871
|
+
)
|
|
872
|
+
.unwrap();
|
|
873
|
+
let config = Config::load(None, directory.path()).unwrap();
|
|
874
|
+
assert_eq!(
|
|
875
|
+
config.cop_value::<Vec<String>>("Layout/LineLength", "Exclude"),
|
|
876
|
+
Some(vec!["a.rb".to_owned(), "b.rb".to_owned()])
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/// `merge_with_default` never derives its root mode per cop, so an empty per-cop directive
|
|
881
|
+
/// does not stop a file-level `merge` from unioning with `default.yml` -- and `node_modules`
|
|
882
|
+
/// stays excluded.
|
|
883
|
+
#[test]
|
|
884
|
+
fn empty_per_cop_inherit_mode_still_unions_with_the_default_configuration() {
|
|
885
|
+
let directory = tempdir().unwrap();
|
|
886
|
+
fs::write(directory.path().join("Gemfile"), "").unwrap();
|
|
887
|
+
fs::write(
|
|
888
|
+
directory.path().join(".rubocop.yml"),
|
|
889
|
+
concat!(
|
|
890
|
+
"inherit_mode:\n merge:\n - Exclude\n",
|
|
891
|
+
"AllCops:\n inherit_mode: {}\n Exclude:\n - custom/**/*\n",
|
|
892
|
+
),
|
|
893
|
+
)
|
|
894
|
+
.unwrap();
|
|
895
|
+
fs::create_dir_all(directory.path().join("node_modules/pkg")).unwrap();
|
|
896
|
+
fs::write(directory.path().join("node_modules/pkg/v.rb"), "y = 2\n").unwrap();
|
|
897
|
+
fs::create_dir_all(directory.path().join("custom")).unwrap();
|
|
898
|
+
fs::write(directory.path().join("custom/c.rb"), "z = 3\n").unwrap();
|
|
899
|
+
fs::write(directory.path().join("a.rb"), "x = 1\n").unwrap();
|
|
900
|
+
let config = Config::load(None, directory.path()).unwrap();
|
|
901
|
+
assert!(config.path_excluded(&directory.path().join("node_modules/pkg/v.rb")));
|
|
902
|
+
assert!(config.path_excluded(&directory.path().join("custom/c.rb")));
|
|
903
|
+
assert!(!config.path_excluded(&directory.path().join("a.rb")));
|
|
904
|
+
}
|
|
332
905
|
}
|