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/paths.rs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
use std::borrow::Cow;
|
|
1
2
|
use std::collections::HashMap;
|
|
2
3
|
use std::fs;
|
|
3
|
-
use std::path::{Path, PathBuf};
|
|
4
|
+
use std::path::{Component, Path, PathBuf};
|
|
5
|
+
use std::sync::OnceLock;
|
|
4
6
|
|
|
5
|
-
use globset::{Glob, GlobMatcher};
|
|
7
|
+
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
|
6
8
|
use serde_yaml_ng::{Mapping, Value};
|
|
7
9
|
|
|
8
10
|
pub(super) fn compile_excludes(raw: &Value) -> HashMap<String, PathPatterns> {
|
|
@@ -43,7 +45,19 @@ pub(super) fn cop_patterns(raw: &Value, name: &str, key: &str) -> Option<PathPat
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
fn mapping_patterns(mapping: &Mapping, key: &str) -> Option<PathPatterns> {
|
|
46
|
-
let
|
|
48
|
+
let value = mapping.get(key)?;
|
|
49
|
+
// `Array(...)`: a lone scalar counts as a one-element list. `Exclude: tmp/**/*`, written
|
|
50
|
+
// without the list dashes, is a common enough slip that it has to mean something, and Ruby's
|
|
51
|
+
// own idiom for reading these lists says what. Upstream does not survive the shape at all --
|
|
52
|
+
// `Config#make_excludes_absolute` raises `undefined method 'map!' for an instance of String` --
|
|
53
|
+
// and this project answers a malformed configuration rather than reproducing a crash. What it
|
|
54
|
+
// must not do is the third thing, which is what deserializing straight into `Vec<String>` and
|
|
55
|
+
// dropping the error did: forgetting the entry leaves an `Exclude` silently unenforced, so the
|
|
56
|
+
// files the user ruled out get inspected and nothing says why.
|
|
57
|
+
let patterns: Vec<String> = match value.as_str() {
|
|
58
|
+
Some(pattern) => vec![pattern.to_owned()],
|
|
59
|
+
None => serde_yaml_ng::from_value(value.clone()).ok()?,
|
|
60
|
+
};
|
|
47
61
|
Some(PathPatterns::compile(&patterns))
|
|
48
62
|
}
|
|
49
63
|
|
|
@@ -58,6 +72,91 @@ pub(super) struct PathPatterns {
|
|
|
58
72
|
/// degrading into "no list configured".
|
|
59
73
|
configured: usize,
|
|
60
74
|
patterns: Vec<CompiledPattern>,
|
|
75
|
+
/// Every pattern, asked in one go.
|
|
76
|
+
all: PatternSet,
|
|
77
|
+
/// The patterns written with a capital, which is the subset
|
|
78
|
+
/// `matches_uppercase_includes` walks.
|
|
79
|
+
uppercase: PatternSet,
|
|
80
|
+
/// Whether any pattern is absolute. Only those are ever held against the absolute spelling of a
|
|
81
|
+
/// path, so a list without one -- which is every list RuboCop ships and nearly every list a
|
|
82
|
+
/// project writes -- skips that second pass and the `PathBuf` it would build per file.
|
|
83
|
+
has_absolute: bool,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// One list of globs folded into `globset`'s set form so a path can be tested against all of them
|
|
87
|
+
/// at once.
|
|
88
|
+
///
|
|
89
|
+
/// `globset` sorts a set into literal, basename, extension and prefix tables and only falls back to
|
|
90
|
+
/// a regex for the patterns none of those cover, so asking the 46 default `Include` globs together
|
|
91
|
+
/// costs about what asking one of them separately did. Walking `patterns` one at a time is kept for
|
|
92
|
+
/// the cases the sets cannot answer -- a hidden path, where each glob has its own say about whether
|
|
93
|
+
/// it reaches the dot -- so the sets are an accelerator, never the authority.
|
|
94
|
+
#[derive(Clone, Debug, Default)]
|
|
95
|
+
struct PatternSet {
|
|
96
|
+
/// Matched against the project-relative path.
|
|
97
|
+
full: GlobSet,
|
|
98
|
+
/// The separator-free patterns, matched against the basename as well.
|
|
99
|
+
basename: GlobSet,
|
|
100
|
+
/// The `dir` half of a `dir/**/*` pattern, matched against each ancestor of the path.
|
|
101
|
+
ancestor: GlobSet,
|
|
102
|
+
/// Whether `ancestor` holds anything, so the ancestor walk is skipped when it does not.
|
|
103
|
+
has_ancestor: bool,
|
|
104
|
+
/// Whether every builder produced a set. A failure only costs speed: the caller falls back to
|
|
105
|
+
/// walking the patterns one at a time, which is what it did before the sets existed.
|
|
106
|
+
usable: bool,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl PatternSet {
|
|
110
|
+
fn build<'a>(patterns: impl Iterator<Item = &'a CompiledPattern>) -> Self {
|
|
111
|
+
let mut full = GlobSetBuilder::new();
|
|
112
|
+
let mut basename = GlobSetBuilder::new();
|
|
113
|
+
let mut ancestor = GlobSetBuilder::new();
|
|
114
|
+
let mut has_ancestor = false;
|
|
115
|
+
for pattern in patterns {
|
|
116
|
+
full.add(pattern.matcher.glob().clone());
|
|
117
|
+
if pattern.basename {
|
|
118
|
+
basename.add(pattern.matcher.glob().clone());
|
|
119
|
+
}
|
|
120
|
+
if let Some(prefix) = pattern.ancestor.as_ref() {
|
|
121
|
+
ancestor.add(prefix.glob().clone());
|
|
122
|
+
has_ancestor = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
match (full.build(), basename.build(), ancestor.build()) {
|
|
126
|
+
(Ok(full), Ok(basename), Ok(ancestor)) => Self {
|
|
127
|
+
full,
|
|
128
|
+
basename,
|
|
129
|
+
ancestor,
|
|
130
|
+
has_ancestor,
|
|
131
|
+
usable: true,
|
|
132
|
+
},
|
|
133
|
+
_ => Self::default(),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// The same question `CompiledPattern::matches` answers for one pattern, asked of the whole set.
|
|
138
|
+
/// Only reachable where every pattern would have been asked with `hidden` false, so the
|
|
139
|
+
/// `skips_hidden` shortcut has no say here.
|
|
140
|
+
fn matches(&self, normalized: &str, basename: &str) -> bool {
|
|
141
|
+
self.full.is_match(normalized)
|
|
142
|
+
|| (self.has_ancestor
|
|
143
|
+
&& Path::new(normalized)
|
|
144
|
+
.ancestors()
|
|
145
|
+
.skip(1)
|
|
146
|
+
.any(|path| self.ancestor.is_match(slashed(path).as_ref())))
|
|
147
|
+
|| self.basename.is_match(basename)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// `path` as text with Windows separators folded to `/`, borrowing whenever there is nothing to
|
|
152
|
+
/// fold -- which is every path on a Unix checkout.
|
|
153
|
+
fn slashed(path: &Path) -> Cow<'_, str> {
|
|
154
|
+
match path.to_string_lossy() {
|
|
155
|
+
Cow::Borrowed(text) if !text.contains('\\') => Cow::Borrowed(text),
|
|
156
|
+
Cow::Borrowed(text) => Cow::Owned(text.replace('\\', "/")),
|
|
157
|
+
Cow::Owned(text) if !text.contains('\\') => Cow::Owned(text),
|
|
158
|
+
Cow::Owned(text) => Cow::Owned(text.replace('\\', "/")),
|
|
159
|
+
}
|
|
61
160
|
}
|
|
62
161
|
|
|
63
162
|
#[derive(Clone, Debug)]
|
|
@@ -73,16 +172,26 @@ struct CompiledPattern {
|
|
|
73
172
|
/// The pattern's dot-prefixed segments, such as `.git` in `.git/**/*`, compiled on their own so
|
|
74
173
|
/// they can be asked whether they reach a hidden part of a path.
|
|
75
174
|
dot_segments: Vec<GlobMatcher>,
|
|
175
|
+
/// `/[A-Z]/.match?(pattern)`: whether the pattern names a file by a capitalised name of its
|
|
176
|
+
/// own, which is what `allowed_camel_case_file?` selects on.
|
|
177
|
+
has_uppercase: bool,
|
|
76
178
|
}
|
|
77
179
|
|
|
78
180
|
impl PathPatterns {
|
|
79
181
|
fn compile(patterns: &[String]) -> Self {
|
|
182
|
+
let compiled: Vec<CompiledPattern> = patterns
|
|
183
|
+
.iter()
|
|
184
|
+
.filter_map(|pattern| CompiledPattern::compile(pattern))
|
|
185
|
+
.collect();
|
|
186
|
+
let all = PatternSet::build(compiled.iter());
|
|
187
|
+
let uppercase = PatternSet::build(compiled.iter().filter(|pattern| pattern.has_uppercase));
|
|
188
|
+
let has_absolute = compiled.iter().any(|pattern| pattern.absolute);
|
|
80
189
|
Self {
|
|
81
190
|
configured: patterns.len(),
|
|
82
|
-
patterns:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
191
|
+
patterns: compiled,
|
|
192
|
+
all,
|
|
193
|
+
uppercase,
|
|
194
|
+
has_absolute,
|
|
86
195
|
}
|
|
87
196
|
}
|
|
88
197
|
|
|
@@ -97,28 +206,90 @@ impl PathPatterns {
|
|
|
97
206
|
}
|
|
98
207
|
|
|
99
208
|
pub(super) fn matches_includes(&self, path: &Path, root: &Path) -> bool {
|
|
209
|
+
self.matches_includes_inner(path, root, false)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// `file_to_include?(file) { |pattern| /[A-Z]/.match?(pattern.to_s) }`: the same walk over the
|
|
213
|
+
/// `Include` list, restricted to the patterns written with a capital. A file that only reaches
|
|
214
|
+
/// the run through one of those -- `Rakefile`, `Gemfile` -- is named after the pattern rather
|
|
215
|
+
/// than after Ruby's conventions, so `Naming/FileName` lets it be.
|
|
216
|
+
pub(super) fn matches_uppercase_includes(&self, path: &Path, root: &Path) -> bool {
|
|
217
|
+
self.matches_includes_inner(path, root, true)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn matches_includes_inner(&self, path: &Path, root: &Path, uppercase_only: bool) -> bool {
|
|
100
221
|
if self.patterns.is_empty() {
|
|
101
222
|
return false;
|
|
102
223
|
}
|
|
103
224
|
let relative = project_relative(path, root).unwrap_or_else(|| path.to_path_buf());
|
|
104
225
|
let relative = relative.to_string_lossy().replace('\\', "/");
|
|
105
|
-
let normalized = relative.trim_start_matches("./");
|
|
106
226
|
let basename = path
|
|
107
227
|
.file_name()
|
|
108
228
|
.and_then(|name| name.to_str())
|
|
109
229
|
.unwrap_or("");
|
|
230
|
+
if self.matches_includes_spelling(
|
|
231
|
+
relative.trim_start_matches("./"),
|
|
232
|
+
basename,
|
|
233
|
+
uppercase_only,
|
|
234
|
+
false,
|
|
235
|
+
) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
// `match_relative_or_absolute_path?`: an `Include` pattern written absolutely is held
|
|
239
|
+
// against the absolute spelling of the file, which is the only spelling it can ever match.
|
|
240
|
+
// Only the absolute patterns are asked -- upstream picks the spelling per pattern, and
|
|
241
|
+
// handing a relative pattern the absolute path would let `**/vendor/**/*` match by way of a
|
|
242
|
+
// directory above the project root that the user never named.
|
|
243
|
+
if !self.has_absolute {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
let Some(absolute) = absolute_form(path) else {
|
|
247
|
+
return false;
|
|
248
|
+
};
|
|
249
|
+
self.matches_includes_spelling(slashed(&absolute).as_ref(), basename, uppercase_only, true)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// One spelling of a path put to the `Include` list. `absolute_only` restricts the question to
|
|
253
|
+
/// the absolute patterns and takes the slow walk, because the sets are built over the whole
|
|
254
|
+
/// list and there is no set for that subset.
|
|
255
|
+
fn matches_includes_spelling(
|
|
256
|
+
&self,
|
|
257
|
+
normalized: &str,
|
|
258
|
+
basename: &str,
|
|
259
|
+
uppercase_only: bool,
|
|
260
|
+
absolute_only: bool,
|
|
261
|
+
) -> bool {
|
|
110
262
|
// RuboCop's `match_path?` matches without `FNM_DOTMATCH`, so a wildcard never reaches a dot
|
|
111
263
|
// component -- only a segment the pattern spells out literally does. On top of that,
|
|
112
264
|
// `hidden_file_in_not_hidden_dir?` lets a dot *file* through as long as it sits in a real,
|
|
113
265
|
// non-hidden directory. So a dot directory is invisible unless named, and a top-level dot
|
|
114
266
|
// file needs naming too.
|
|
115
267
|
let (directories, file) = normalized.rsplit_once('/').unwrap_or(("", normalized));
|
|
268
|
+
let hidden_file = hidden_segment(file);
|
|
269
|
+
let hidden_directory = directories.split('/').any(hidden_segment);
|
|
270
|
+
// With nothing hidden about the path, every pattern would clear the two dot tests below on
|
|
271
|
+
// its own, and what is left is the plain glob question the set can answer in one go.
|
|
272
|
+
if !absolute_only {
|
|
273
|
+
let set = if uppercase_only {
|
|
274
|
+
&self.uppercase
|
|
275
|
+
} else {
|
|
276
|
+
&self.all
|
|
277
|
+
};
|
|
278
|
+
if set.usable && !hidden_directory && !(hidden_file && directories.is_empty()) {
|
|
279
|
+
return set.matches(normalized, basename);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
116
282
|
let hidden_directories: Vec<&str> = directories
|
|
117
283
|
.split('/')
|
|
118
|
-
.filter(|part| part
|
|
284
|
+
.filter(|part| hidden_segment(part))
|
|
119
285
|
.collect();
|
|
120
|
-
let hidden_file = file.starts_with('.');
|
|
121
286
|
self.patterns.iter().any(|pattern| {
|
|
287
|
+
if uppercase_only && !pattern.has_uppercase {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if absolute_only && !pattern.absolute {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
122
293
|
if !hidden_directories
|
|
123
294
|
.iter()
|
|
124
295
|
.all(|part| pattern.reaches_hidden(part))
|
|
@@ -144,10 +315,34 @@ impl PathPatterns {
|
|
|
144
315
|
.and_then(|name| name.to_str())
|
|
145
316
|
.unwrap_or("");
|
|
146
317
|
let hidden = respect_hidden && has_hidden_component(Path::new(normalized));
|
|
318
|
+
// `hidden` and `absolute_only` are the only things that put a pattern out of the running on
|
|
319
|
+
// its own; without them the set answers for the whole list at once.
|
|
320
|
+
let matched = if self.all.usable && !hidden && !absolute_only {
|
|
321
|
+
self.all.matches(normalized, basename)
|
|
322
|
+
} else {
|
|
323
|
+
self.patterns
|
|
324
|
+
.iter()
|
|
325
|
+
.filter(|pattern| !absolute_only || pattern.absolute)
|
|
326
|
+
.any(|pattern| pattern.matches(normalized, basename, hidden))
|
|
327
|
+
};
|
|
328
|
+
if matched {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
// `file_to_exclude?` expands the file before matching, so an `Exclude` written absolutely
|
|
332
|
+
// reaches project files upstream. The same restriction as on the `Include` side applies:
|
|
333
|
+
// only the absolute patterns get to see the absolute spelling.
|
|
334
|
+
if !self.has_absolute {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
let Some(absolute) = absolute_form(path) else {
|
|
338
|
+
return false;
|
|
339
|
+
};
|
|
340
|
+
let absolute = slashed(&absolute);
|
|
341
|
+
let hidden = respect_hidden && has_hidden_component(Path::new(absolute.as_ref()));
|
|
147
342
|
self.patterns
|
|
148
343
|
.iter()
|
|
149
|
-
.filter(|pattern|
|
|
150
|
-
.any(|pattern| pattern.matches(
|
|
344
|
+
.filter(|pattern| pattern.absolute)
|
|
345
|
+
.any(|pattern| pattern.matches(absolute.as_ref(), basename, hidden))
|
|
151
346
|
}
|
|
152
347
|
}
|
|
153
348
|
|
|
@@ -170,6 +365,9 @@ impl CompiledPattern {
|
|
|
170
365
|
.filter_map(|segment| Glob::new(segment).ok())
|
|
171
366
|
.map(|glob| glob.compile_matcher())
|
|
172
367
|
.collect(),
|
|
368
|
+
has_uppercase: pattern
|
|
369
|
+
.chars()
|
|
370
|
+
.any(|character| character.is_ascii_uppercase()),
|
|
173
371
|
})
|
|
174
372
|
}
|
|
175
373
|
|
|
@@ -191,7 +389,7 @@ impl CompiledPattern {
|
|
|
191
389
|
Path::new(normalized)
|
|
192
390
|
.ancestors()
|
|
193
391
|
.skip(1)
|
|
194
|
-
.any(|path| ancestor.is_match(path
|
|
392
|
+
.any(|path| ancestor.is_match(slashed(path).as_ref()))
|
|
195
393
|
})
|
|
196
394
|
|| (self.basename && self.matcher.is_match(basename))
|
|
197
395
|
}
|
|
@@ -208,12 +406,38 @@ impl CompiledPattern {
|
|
|
208
406
|
/// treated as hidden. The text comparisons are tried first because they cover every normal case;
|
|
209
407
|
/// only when they disagree is it worth asking the filesystem.
|
|
210
408
|
pub(super) fn project_relative(path: &Path, root: &Path) -> Option<PathBuf> {
|
|
409
|
+
// `strip_prefix` compares component by component, so a `..` left in the path survives into the
|
|
410
|
+
// result: `sonicop ../lib` run from a subdirectory produced `sub/../lib/a.rb` where upstream
|
|
411
|
+
// produces `lib/a.rb`, and every wildcard pattern then refused the file over the dot segment.
|
|
412
|
+
// Upstream never sees one because `PathUtil.relative_path` runs the path through
|
|
413
|
+
// `File.expand_path` first, so the fold happens here too.
|
|
414
|
+
let folded = without_parent_segments(path);
|
|
415
|
+
let path = folded.as_ref();
|
|
211
416
|
if let Ok(relative) = path.strip_prefix(root) {
|
|
212
417
|
return Some(relative.to_path_buf());
|
|
213
418
|
}
|
|
214
419
|
if let Ok(relative) = strip_verbatim(path).strip_prefix(strip_verbatim(root)) {
|
|
215
420
|
return Some(relative.to_path_buf());
|
|
216
421
|
}
|
|
422
|
+
// A path spelled relative to the working directory -- which is every path a run started with
|
|
423
|
+
// `sonicop .` walks -- shares no text with an absolute root, so both comparisons above miss and
|
|
424
|
+
// the file falls through to `canonicalize`. That is two syscalls per question and three
|
|
425
|
+
// questions per file, which cost more than inspecting the file did. Joining the working
|
|
426
|
+
// directory answers the same question without asking the filesystem anything; `Components`
|
|
427
|
+
// drops the `.` segments a walk introduces, and the join is folded again because a leading `..`
|
|
428
|
+
// only has something to cancel once the working directory is in front of it.
|
|
429
|
+
if path.is_relative() {
|
|
430
|
+
if let Some(joined) = working_directory().map(|cwd| cwd.join(path)) {
|
|
431
|
+
let joined = without_parent_segments(&joined);
|
|
432
|
+
let joined = joined.as_ref();
|
|
433
|
+
if let Ok(relative) = joined.strip_prefix(root) {
|
|
434
|
+
return Some(relative.to_path_buf());
|
|
435
|
+
}
|
|
436
|
+
if let Ok(relative) = strip_verbatim(joined).strip_prefix(strip_verbatim(root)) {
|
|
437
|
+
return Some(relative.to_path_buf());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
217
441
|
let resolved = fs::canonicalize(path).ok()?;
|
|
218
442
|
let root = fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
|
|
219
443
|
resolved
|
|
@@ -227,6 +451,80 @@ pub(super) fn project_relative(path: &Path, root: &Path) -> Option<PathBuf> {
|
|
|
227
451
|
.map(Path::to_path_buf)
|
|
228
452
|
}
|
|
229
453
|
|
|
454
|
+
/// `path` with its `..` segments folded away, borrowed whenever it has none -- which is every path
|
|
455
|
+
/// a `sonicop .` run walks.
|
|
456
|
+
///
|
|
457
|
+
/// This is for `project_relative`, whose comparisons are all `strip_prefix` and therefore run over
|
|
458
|
+
/// `Components`, which already hides the `.` segments a walk introduces. Only `..` needs settling
|
|
459
|
+
/// there, and settling it is lexical, exactly as `File.expand_path` is: a `..` that crosses a
|
|
460
|
+
/// symlink lands where the text says rather than where the link points. That is upstream's answer
|
|
461
|
+
/// as well, and it is what keeps this from costing a `readlink` per file. Matching against the
|
|
462
|
+
/// *text* of a path needs more than this -- see `folded_components`.
|
|
463
|
+
fn without_parent_segments(path: &Path) -> Cow<'_, Path> {
|
|
464
|
+
if !path
|
|
465
|
+
.components()
|
|
466
|
+
.any(|component| component == Component::ParentDir)
|
|
467
|
+
{
|
|
468
|
+
// `strip_prefix` -- the only thing the fold is for here -- compares `Components`, and those
|
|
469
|
+
// already drop the `.` segments a walk introduces. A path with no `..` in it is therefore
|
|
470
|
+
// its own folded form as far as `project_relative` is concerned, and can be borrowed.
|
|
471
|
+
return Cow::Borrowed(path);
|
|
472
|
+
}
|
|
473
|
+
Cow::Owned(folded_components(path))
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// `path` rebuilt out of its components, with `.` and `..` folded away.
|
|
477
|
+
///
|
|
478
|
+
/// Unlike `without_parent_segments` this always rebuilds, because a glob is matched against the
|
|
479
|
+
/// *text* of a path: `Components` hides the `./` a `sonicop .` walk leaves in the middle of
|
|
480
|
+
/// `<cwd>/./lib/a.rb`, but `to_string_lossy` does not, and no pattern spelled `<root>/lib/**/*.rb`
|
|
481
|
+
/// can match a string with that `./` still in it.
|
|
482
|
+
fn folded_components(path: &Path) -> PathBuf {
|
|
483
|
+
let mut kept: Vec<Component<'_>> = Vec::new();
|
|
484
|
+
for component in path.components() {
|
|
485
|
+
match component {
|
|
486
|
+
Component::CurDir => {}
|
|
487
|
+
Component::ParentDir => match kept.last() {
|
|
488
|
+
// `a/../b` is `b`.
|
|
489
|
+
Some(Component::Normal(_)) => {
|
|
490
|
+
kept.pop();
|
|
491
|
+
}
|
|
492
|
+
// `/..` is `/`: a path cannot climb past its own root.
|
|
493
|
+
Some(Component::RootDir) => {}
|
|
494
|
+
// A leading `..`, or one following another, has nothing to cancel and has to
|
|
495
|
+
// survive -- `File.expand_path` keeps those too, once a relative path has run out
|
|
496
|
+
// of segments to undo.
|
|
497
|
+
_ => kept.push(component),
|
|
498
|
+
},
|
|
499
|
+
other => kept.push(other),
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
kept.iter().collect()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/// `File.expand_path(path)`: the absolute spelling of `path`, folded.
|
|
506
|
+
///
|
|
507
|
+
/// `Config#file_to_exclude?` matches the absolute path and `Config#file_to_include?` matches the
|
|
508
|
+
/// relative *or* the absolute one, so a pattern written absolutely reaches project files upstream.
|
|
509
|
+
/// The working directory is what a relative path is joined to, because that is what
|
|
510
|
+
/// `File.expand_path` uses -- not the project root, which a run started outside the project does
|
|
511
|
+
/// not sit in.
|
|
512
|
+
fn absolute_form(path: &Path) -> Option<PathBuf> {
|
|
513
|
+
if path.is_absolute() {
|
|
514
|
+
return Some(folded_components(path));
|
|
515
|
+
}
|
|
516
|
+
Some(folded_components(&working_directory()?.join(path)))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// The working directory, asked of the kernel once.
|
|
520
|
+
///
|
|
521
|
+
/// Nothing in the program changes it, and `project_relative` needs it for every file a relative
|
|
522
|
+
/// argument expands to, so the answer is kept rather than fetched again.
|
|
523
|
+
fn working_directory() -> Option<&'static Path> {
|
|
524
|
+
static CWD: OnceLock<Option<PathBuf>> = OnceLock::new();
|
|
525
|
+
CWD.get_or_init(|| std::env::current_dir().ok()).as_deref()
|
|
526
|
+
}
|
|
527
|
+
|
|
230
528
|
/// Drops Windows' `\\?\` extended-length marker so that two spellings of one path can be compared.
|
|
231
529
|
/// Other platforms never carry the prefix, so this is the identity there.
|
|
232
530
|
fn strip_verbatim(path: &Path) -> &Path {
|
|
@@ -247,16 +545,30 @@ fn strip_verbatim(path: &Path) -> &Path {
|
|
|
247
545
|
/// the same project-relative path, so both walk the components here instead of
|
|
248
546
|
/// each carrying its own copy of the rule.
|
|
249
547
|
pub(super) fn has_hidden_component(path: &Path) -> bool {
|
|
250
|
-
path.components()
|
|
251
|
-
component
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
548
|
+
path.components()
|
|
549
|
+
.any(|component| component.as_os_str().to_str().is_some_and(hidden_segment))
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/// Whether one path segment is hidden.
|
|
553
|
+
///
|
|
554
|
+
/// `.` and `..` begin with a dot without being hidden, and both can reach a matcher: a path outside
|
|
555
|
+
/// the project root keeps its `..` segments, and a walk hands out `./`-prefixed spellings.
|
|
556
|
+
/// Upstream draws the same line -- `file_to_include?` asks
|
|
557
|
+
/// `relative_file_path.start_with?('.') && !relative_file_path.start_with?('..')`. Reading a `..`
|
|
558
|
+
/// as hidden put every wildcard pattern out of the running, which is how `sonicop ../lib` came to
|
|
559
|
+
/// inspect nothing at all.
|
|
560
|
+
fn hidden_segment(segment: &str) -> bool {
|
|
561
|
+
segment.starts_with('.') && segment != "." && segment != ".."
|
|
256
562
|
}
|
|
257
563
|
|
|
258
564
|
/// Verbatim copy of the pre-compilation matcher, kept so the compiled matcher can be
|
|
259
565
|
/// proven byte-for-byte equivalent over a cross product of paths and patterns.
|
|
566
|
+
///
|
|
567
|
+
/// It compiles a `Glob` per pattern per question and knows nothing of `PatternSet`, `has_absolute`
|
|
568
|
+
/// or the dot-segment tables, so the accelerators the real matcher is built out of have somewhere
|
|
569
|
+
/// to be checked against. What it does share is `absolute_form` and `hidden_segment`: those decide
|
|
570
|
+
/// *which path* is being matched rather than *how*, and a second copy of them would only prove the
|
|
571
|
+
/// two copies agree.
|
|
260
572
|
#[cfg(test)]
|
|
261
573
|
fn matches_patterns_reference(
|
|
262
574
|
path: &Path,
|
|
@@ -265,22 +577,25 @@ fn matches_patterns_reference(
|
|
|
265
577
|
respect_hidden: bool,
|
|
266
578
|
) -> bool {
|
|
267
579
|
let relative = path.strip_prefix(root).unwrap_or(path);
|
|
268
|
-
let
|
|
580
|
+
let relative = relative
|
|
269
581
|
.to_string_lossy()
|
|
270
582
|
.replace('\\', "/")
|
|
271
583
|
.trim_start_matches("./")
|
|
272
584
|
.to_owned();
|
|
585
|
+
let absolute = absolute_form(path).map(|path| slashed(&path).into_owned());
|
|
273
586
|
let basename = path
|
|
274
587
|
.file_name()
|
|
275
588
|
.and_then(|name| name.to_str())
|
|
276
589
|
.unwrap_or("");
|
|
277
|
-
let (directories, file) = normalized.rsplit_once('/').unwrap_or(("", &normalized));
|
|
278
|
-
let hidden_directories: Vec<&str> = directories
|
|
279
|
-
.split('/')
|
|
280
|
-
.filter(|part| part.starts_with('.') && *part != "." && *part != "..")
|
|
281
|
-
.collect();
|
|
282
|
-
let hidden_file = file.starts_with('.') && file != "." && file != "..";
|
|
283
590
|
patterns.iter().any(|pattern| {
|
|
591
|
+
// `match_relative_or_absolute_path?`: the absolute spelling of the path is in play for an
|
|
592
|
+
// absolute pattern and for nothing else.
|
|
593
|
+
let mut spellings = vec![relative.as_str()];
|
|
594
|
+
if Path::new(pattern.as_str()).is_absolute()
|
|
595
|
+
&& let Some(absolute) = absolute.as_deref()
|
|
596
|
+
{
|
|
597
|
+
spellings.push(absolute);
|
|
598
|
+
}
|
|
284
599
|
let pattern = pattern.trim_start_matches("./");
|
|
285
600
|
// Only a dot-prefixed segment of the pattern reaches a dot component, and a dot file at the
|
|
286
601
|
// top level needs the same. Below the top level a dot file is reached anyway.
|
|
@@ -290,28 +605,36 @@ fn matches_patterns_reference(
|
|
|
290
605
|
&& Glob::new(part).is_ok_and(|glob| glob.compile_matcher().is_match(segment))
|
|
291
606
|
})
|
|
292
607
|
};
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
608
|
+
spellings.into_iter().any(|normalized| {
|
|
609
|
+
let (directories, file) = normalized.rsplit_once('/').unwrap_or(("", normalized));
|
|
610
|
+
let hidden_directories: Vec<&str> = directories
|
|
611
|
+
.split('/')
|
|
612
|
+
.filter(|part| hidden_segment(part))
|
|
613
|
+
.collect();
|
|
614
|
+
let hidden_file = hidden_segment(file);
|
|
615
|
+
if respect_hidden
|
|
616
|
+
&& (!hidden_directories.iter().all(|part| reaches(part))
|
|
617
|
+
|| (hidden_file && directories.is_empty() && !reaches(file)))
|
|
618
|
+
{
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
Glob::new(pattern)
|
|
622
|
+
.map(|glob| {
|
|
623
|
+
let matcher = glob.compile_matcher();
|
|
624
|
+
matcher.is_match(normalized)
|
|
625
|
+
|| pattern.strip_suffix("/**/*").is_some_and(|prefix| {
|
|
626
|
+
Glob::new(prefix).is_ok_and(|prefix_glob| {
|
|
627
|
+
let prefix_matcher = prefix_glob.compile_matcher();
|
|
628
|
+
Path::new(normalized).ancestors().skip(1).any(|ancestor| {
|
|
629
|
+
prefix_matcher
|
|
630
|
+
.is_match(ancestor.to_string_lossy().replace('\\', "/"))
|
|
631
|
+
})
|
|
309
632
|
})
|
|
310
633
|
})
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
634
|
+
|| (!pattern.contains('/') && matcher.is_match(basename))
|
|
635
|
+
})
|
|
636
|
+
.unwrap_or(false)
|
|
637
|
+
})
|
|
315
638
|
})
|
|
316
639
|
}
|
|
317
640
|
|
|
@@ -332,9 +655,11 @@ fn matches_any_reference(path: &Path, root: &Path, patterns: &[String]) -> bool
|
|
|
332
655
|
mod tests {
|
|
333
656
|
use std::path::{Path, PathBuf};
|
|
334
657
|
|
|
658
|
+
use serde_yaml_ng::Value;
|
|
659
|
+
|
|
335
660
|
use super::{
|
|
336
|
-
PathPatterns,
|
|
337
|
-
strip_verbatim,
|
|
661
|
+
PathPatterns, hidden_segment, mapping_patterns, matches_any_reference,
|
|
662
|
+
matches_patterns_reference, project_relative, slashed, strip_verbatim,
|
|
338
663
|
};
|
|
339
664
|
|
|
340
665
|
fn owned(patterns: &[&str]) -> Vec<String> {
|
|
@@ -344,6 +669,14 @@ mod tests {
|
|
|
344
669
|
.collect()
|
|
345
670
|
}
|
|
346
671
|
|
|
672
|
+
/// `AllCops`'s `Include`/`Exclude` read the way the configuration hands it over, so the shape
|
|
673
|
+
/// of the YAML is part of what is under test.
|
|
674
|
+
fn all_cops_patterns(yaml: &str, key: &str) -> Option<PathPatterns> {
|
|
675
|
+
let value: Value = serde_yaml_ng::from_str(yaml).unwrap();
|
|
676
|
+
let all_cops = value.as_mapping().unwrap().get("AllCops").unwrap();
|
|
677
|
+
mapping_patterns(all_cops.as_mapping().unwrap(), key)
|
|
678
|
+
}
|
|
679
|
+
|
|
347
680
|
fn includes(path: &str, root: &str, patterns: &[&str]) -> bool {
|
|
348
681
|
PathPatterns::compile(&owned(patterns)).matches_includes(Path::new(path), Path::new(root))
|
|
349
682
|
}
|
|
@@ -390,6 +723,24 @@ mod tests {
|
|
|
390
723
|
);
|
|
391
724
|
}
|
|
392
725
|
|
|
726
|
+
/// `sonicop .` が渡すのは `./lib/a.rb` のような相対パスで、絶対のルートとは文字列が
|
|
727
|
+
/// 重ならない。作業ディレクトリを継ぎ足して解決できなければ、ファイルごとに
|
|
728
|
+
/// `canonicalize` へ落ちて探索が検査より高くつく。存在しないパスで確かめているのは、
|
|
729
|
+
/// 継ぎ足しで答えが出たことを `canonicalize` の成功と取り違えないため。
|
|
730
|
+
#[test]
|
|
731
|
+
fn a_relative_path_resolves_against_the_working_directory() {
|
|
732
|
+
let cwd = std::env::current_dir().unwrap();
|
|
733
|
+
assert_eq!(
|
|
734
|
+
project_relative(Path::new("nowhere/lib/a.rb"), &cwd.join("nowhere")),
|
|
735
|
+
Some(PathBuf::from("lib/a.rb"))
|
|
736
|
+
);
|
|
737
|
+
// 走査が挟む `.` は `Components` が落とすので、綴りの違いは結果に出ない。
|
|
738
|
+
assert_eq!(
|
|
739
|
+
project_relative(Path::new("./nowhere/lib/a.rb"), &cwd.join("nowhere")),
|
|
740
|
+
Some(PathBuf::from("lib/a.rb"))
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
393
744
|
/// プロジェクト外は None のままでなければ、相対パターンが外部へ届いてしまう。
|
|
394
745
|
#[test]
|
|
395
746
|
fn a_path_outside_the_root_has_no_project_relative_form() {
|
|
@@ -429,6 +780,134 @@ mod tests {
|
|
|
429
780
|
assert!(excludes("/p/.git/config.rb", "/p", &["**/*.rb"]));
|
|
430
781
|
}
|
|
431
782
|
|
|
783
|
+
/// `.` and `..` begin with a dot without being hidden, and a matcher meets both: a walk hands
|
|
784
|
+
/// out `./`-prefixed spellings, and a file outside the project root keeps its `..` segments.
|
|
785
|
+
/// Reading either as hidden put every pattern that does not spell a dot -- which is the whole
|
|
786
|
+
/// of the default `Include` list -- out of the running, and `sonicop ../lib` inspected nothing.
|
|
787
|
+
#[test]
|
|
788
|
+
fn a_parent_directory_segment_is_not_hidden() {
|
|
789
|
+
assert!(!hidden_segment("."));
|
|
790
|
+
assert!(!hidden_segment(".."));
|
|
791
|
+
assert!(!hidden_segment("lib"));
|
|
792
|
+
// The exemption is those two names and nothing else, which is where `hidden_dir?` and
|
|
793
|
+
// `hidden_file?` draw it too -- both ask only `start_with?('.')` of a whole segment.
|
|
794
|
+
assert!(hidden_segment(".git"));
|
|
795
|
+
assert!(hidden_segment("..."));
|
|
796
|
+
assert!(hidden_segment(".rubocop.yml"));
|
|
797
|
+
// A path that keeps its `..` -- one outside the project root -- is still reachable by an
|
|
798
|
+
// ordinary wildcard, which is what upstream does once it switches to the absolute spelling.
|
|
799
|
+
assert!(includes("../other/x.rb", "/p", &["**/*.rb"]));
|
|
800
|
+
// A genuinely hidden directory still has to be named by the pattern.
|
|
801
|
+
assert!(!includes("/p/.git/config.rb", "/p", &["**/*.rb"]));
|
|
802
|
+
assert!(includes("/p/.git/config.rb", "/p", &[".git/**/*"]));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/// `PathUtil.relative_path` expands the path before comparing it with the base directory, so a
|
|
806
|
+
/// project-relative spelling never keeps a `..`. `strip_prefix` compares components and leaves
|
|
807
|
+
/// one in place, so `sonicop ../lib` run from `sub/` produced `sub/../lib/a.rb` where upstream
|
|
808
|
+
/// produces `lib/a.rb`.
|
|
809
|
+
#[test]
|
|
810
|
+
fn a_parent_directory_target_folds_before_the_root_is_stripped() {
|
|
811
|
+
assert_eq!(
|
|
812
|
+
project_relative(Path::new("/p/sub/../lib/a.rb"), Path::new("/p")),
|
|
813
|
+
Some(PathBuf::from("lib/a.rb"))
|
|
814
|
+
);
|
|
815
|
+
// Folding must not invent a hit: `..` that climbs out of the root still leaves the file
|
|
816
|
+
// outside it.
|
|
817
|
+
assert_eq!(
|
|
818
|
+
project_relative(
|
|
819
|
+
Path::new("/p/../other/x.rb"),
|
|
820
|
+
Path::new("/p/does-not-exist")
|
|
821
|
+
),
|
|
822
|
+
None
|
|
823
|
+
);
|
|
824
|
+
// The spelling a walk actually produces is relative to the working directory, and a leading
|
|
825
|
+
// `..` only has something to cancel once that directory is in front of it. Non-existent
|
|
826
|
+
// paths again, so that an answer can only have come from the lexical fold.
|
|
827
|
+
let cwd = std::env::current_dir().unwrap();
|
|
828
|
+
let sibling = cwd.parent().unwrap().join("nowhere");
|
|
829
|
+
assert_eq!(
|
|
830
|
+
project_relative(Path::new("../nowhere/lib/a.rb"), &sibling),
|
|
831
|
+
Some(PathBuf::from("lib/a.rb"))
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/// `file_to_exclude?` matches the absolute path and `file_to_include?` the relative *or* the
|
|
836
|
+
/// absolute one, so a pattern written absolutely reaches project files upstream. Asking only
|
|
837
|
+
/// the project-relative spelling meant an absolute `Exclude` was quietly ignored and an
|
|
838
|
+
/// absolute `Include` matched nothing at all.
|
|
839
|
+
#[test]
|
|
840
|
+
fn an_absolute_pattern_reaches_a_file_inside_the_project() {
|
|
841
|
+
// A root under the working directory, so the relative spellings below resolve against it.
|
|
842
|
+
// Nothing here exists; the answers can only come from the lexical expansion.
|
|
843
|
+
let cwd = std::env::current_dir().unwrap();
|
|
844
|
+
let root = cwd.join("nowhere/deep");
|
|
845
|
+
let root_text = slashed(&root).into_owned();
|
|
846
|
+
let file = root.join("lib/a.rb");
|
|
847
|
+
let temporary = root.join("tmp/t.rb");
|
|
848
|
+
|
|
849
|
+
let include = PathPatterns::compile(&[format!("{root_text}/lib/**/*.rb")]);
|
|
850
|
+
assert!(include.matches_includes(&file, &root));
|
|
851
|
+
assert!(!include.matches_includes(&temporary, &root));
|
|
852
|
+
// The same file spelled the way a walk hands it over. `./` is the spelling `sonicop .`
|
|
853
|
+
// produces, and `Components` hides it from `strip_prefix` while leaving it in the text a
|
|
854
|
+
// glob is matched against, so it needs its own case.
|
|
855
|
+
assert!(include.matches_includes(Path::new("nowhere/deep/lib/a.rb"), &root));
|
|
856
|
+
assert!(include.matches_includes(Path::new("./nowhere/deep/lib/a.rb"), &root));
|
|
857
|
+
|
|
858
|
+
let exclude = PathPatterns::compile(&[format!("{root_text}/tmp/**/*")]);
|
|
859
|
+
assert!(exclude.matches_any(&temporary, &root));
|
|
860
|
+
assert!(exclude.matches_any(Path::new("./nowhere/deep/tmp/t.rb"), &root));
|
|
861
|
+
assert!(!exclude.matches_any(&file, &root));
|
|
862
|
+
|
|
863
|
+
// The other side of the boundary: a *relative* pattern is never held against the absolute
|
|
864
|
+
// spelling. `**/nowhere/**/*` names a directory that only exists above the project root, so
|
|
865
|
+
// matching it there would exclude every file in the project over a name the user never
|
|
866
|
+
// wrote about.
|
|
867
|
+
assert!(!excludes(
|
|
868
|
+
"lib/a.rb",
|
|
869
|
+
root.to_str().unwrap(),
|
|
870
|
+
&["**/nowhere/**/*"]
|
|
871
|
+
));
|
|
872
|
+
assert!(!includes(
|
|
873
|
+
"lib/a.rb",
|
|
874
|
+
root.to_str().unwrap(),
|
|
875
|
+
&["**/nowhere/**/*"]
|
|
876
|
+
));
|
|
877
|
+
// And a relative pattern still matches the relative spelling, absolutely as before.
|
|
878
|
+
assert!(excludes("lib/a.rb", root.to_str().unwrap(), &["lib/**/*"]));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/// `Array(...)`: a lone scalar is a one-element list. Upstream raises on the shape
|
|
882
|
+
/// (`undefined method 'map!' for an instance of String`), and the answer this project gives
|
|
883
|
+
/// instead must not be "the entry never existed" -- a forgotten `Exclude` inspects the files
|
|
884
|
+
/// the user ruled out and says nothing about it.
|
|
885
|
+
#[test]
|
|
886
|
+
fn a_scalar_pattern_list_is_read_as_one_element() {
|
|
887
|
+
let scalar = all_cops_patterns("AllCops:\n Exclude: tmp/**/*\n", "Exclude").unwrap();
|
|
888
|
+
let list = all_cops_patterns("AllCops:\n Exclude:\n - tmp/**/*\n", "Exclude").unwrap();
|
|
889
|
+
for patterns in [&scalar, &list] {
|
|
890
|
+
assert!(!patterns.is_empty());
|
|
891
|
+
assert!(patterns.matches_any(Path::new("/p/tmp/t.rb"), Path::new("/p")));
|
|
892
|
+
assert!(!patterns.matches_any(Path::new("/p/lib/a.rb"), Path::new("/p")));
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
let scalar = all_cops_patterns("AllCops:\n Include: lib/**/*.rb\n", "Include").unwrap();
|
|
896
|
+
let list =
|
|
897
|
+
all_cops_patterns("AllCops:\n Include:\n - lib/**/*.rb\n", "Include").unwrap();
|
|
898
|
+
for patterns in [&scalar, &list] {
|
|
899
|
+
assert!(patterns.matches_includes(Path::new("/p/lib/a.rb"), Path::new("/p")));
|
|
900
|
+
assert!(!patterns.matches_includes(Path::new("/p/tmp/t.rb"), Path::new("/p")));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// A key that is not written at all still has no list, which is a different thing from an
|
|
904
|
+
// empty one: `path_included` reads it as "every file".
|
|
905
|
+
assert!(all_cops_patterns("AllCops:\n Exclude: tmp/**/*\n", "Include").is_none());
|
|
906
|
+
// An empty list stays empty rather than turning into a one-element list of nothing.
|
|
907
|
+
let empty = all_cops_patterns("AllCops:\n Exclude: []\n", "Exclude").unwrap();
|
|
908
|
+
assert!(empty.is_empty());
|
|
909
|
+
}
|
|
910
|
+
|
|
432
911
|
#[test]
|
|
433
912
|
fn an_unusable_include_list_still_counts_as_configured() {
|
|
434
913
|
let patterns = PathPatterns::compile(&owned(&["["]));
|
|
@@ -451,6 +930,10 @@ mod tests {
|
|
|
451
930
|
"**/.*",
|
|
452
931
|
"/abs/**/*",
|
|
453
932
|
"/abs/x.rb",
|
|
933
|
+
// Absolute patterns that reach *into* the project root, which is the shape a hand
|
|
934
|
+
// written `Include`/`Exclude` takes and the one the relative-only matcher never saw.
|
|
935
|
+
"/p/**/*.rb",
|
|
936
|
+
"/p/.git/**/*",
|
|
454
937
|
"spec/**/*_spec.rb",
|
|
455
938
|
"a/*/c",
|
|
456
939
|
"[",
|
|
@@ -505,6 +988,25 @@ mod tests {
|
|
|
505
988
|
matches_any_reference(path, root, &selected),
|
|
506
989
|
"excludes {path:?} against {selected:?}"
|
|
507
990
|
);
|
|
991
|
+
// Selecting on `has_uppercase` has to mean the same thing as handing the
|
|
992
|
+
// matcher only the patterns written with a capital in the first place. The two
|
|
993
|
+
// reach the question by different routes -- one filters a compiled set, the
|
|
994
|
+
// other compiles a filtered list -- so they are worth holding against each
|
|
995
|
+
// other for every window.
|
|
996
|
+
let capitals: Vec<String> = selected
|
|
997
|
+
.iter()
|
|
998
|
+
.filter(|pattern| {
|
|
999
|
+
pattern
|
|
1000
|
+
.chars()
|
|
1001
|
+
.any(|character| character.is_ascii_uppercase())
|
|
1002
|
+
})
|
|
1003
|
+
.cloned()
|
|
1004
|
+
.collect();
|
|
1005
|
+
assert_eq!(
|
|
1006
|
+
compiled.matches_uppercase_includes(path, root),
|
|
1007
|
+
PathPatterns::compile(&capitals).matches_includes(path, root),
|
|
1008
|
+
"uppercase includes {path:?} against {selected:?}"
|
|
1009
|
+
);
|
|
508
1010
|
}
|
|
509
1011
|
}
|
|
510
1012
|
}
|