rubocop-callbacksystems 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +78 -0
- data/config/default.yml +590 -0
- data/docs/cops.md +111 -0
- data/lib/rubocop/callbacksystems/autocorrection/batch.rb +135 -0
- data/lib/rubocop/callbacksystems/autocorrection/comment_rescue.rb +112 -0
- data/lib/rubocop/callbacksystems/autocorrection/rehearsal.rb +65 -0
- data/lib/rubocop/callbacksystems/autocorrection/rewrite_permit.rb +14 -0
- data/lib/rubocop/callbacksystems/class_structure/builder_assignment.rb +48 -0
- data/lib/rubocop/callbacksystems/class_structure/class_body.rb +50 -0
- data/lib/rubocop/callbacksystems/class_structure/divergence.rb +31 -0
- data/lib/rubocop/callbacksystems/class_structure/node_visibility.rb +82 -0
- data/lib/rubocop/callbacksystems/class_structure/reference_order.rb +36 -0
- data/lib/rubocop/callbacksystems/class_structure/statement_rank.rb +84 -0
- data/lib/rubocop/callbacksystems/class_structure/statement_run.rb +42 -0
- data/lib/rubocop/callbacksystems/class_structure/visibility.rb +84 -0
- data/lib/rubocop/callbacksystems/cops_document.rb +78 -0
- data/lib/rubocop/callbacksystems/execution/deferred_local_variables.rb +138 -0
- data/lib/rubocop/callbacksystems/execution/discarded_expression.rb +45 -0
- data/lib/rubocop/callbacksystems/execution/eager_evaluation_path.rb +73 -0
- data/lib/rubocop/callbacksystems/execution/immediate.rb +71 -0
- data/lib/rubocop/callbacksystems/execution/local_variable_occurrences.rb +112 -0
- data/lib/rubocop/callbacksystems/hashes/key_transform_detection.rb +108 -0
- data/lib/rubocop/callbacksystems/hashes/keyword_options.rb +37 -0
- data/lib/rubocop/callbacksystems/helpers/bodies.rb +150 -0
- data/lib/rubocop/callbacksystems/helpers/builders.rb +103 -0
- data/lib/rubocop/callbacksystems/helpers/comments.rb +56 -0
- data/lib/rubocop/callbacksystems/helpers/corrections.rb +15 -0
- data/lib/rubocop/callbacksystems/helpers/methods.rb +46 -0
- data/lib/rubocop/callbacksystems/helpers/names.rb +65 -0
- data/lib/rubocop/callbacksystems/helpers/node_types.rb +154 -0
- data/lib/rubocop/callbacksystems/helpers/rails.rb +52 -0
- data/lib/rubocop/callbacksystems/helpers/ranges.rb +107 -0
- data/lib/rubocop/callbacksystems/helpers/recursion.rb +29 -0
- data/lib/rubocop/callbacksystems/helpers/variables.rb +44 -0
- data/lib/rubocop/callbacksystems/helpers/visibility.rb +121 -0
- data/lib/rubocop/callbacksystems/helpers.rb +43 -0
- data/lib/rubocop/callbacksystems/methods/calls.rb +38 -0
- data/lib/rubocop/callbacksystems/methods/definitions.rb +218 -0
- data/lib/rubocop/callbacksystems/methods/delegate_macro.rb +144 -0
- data/lib/rubocop/callbacksystems/methods/delegation_target.rb +55 -0
- data/lib/rubocop/callbacksystems/methods/domain.rb +77 -0
- data/lib/rubocop/callbacksystems/methods/implicit_invocations.rb +17 -0
- data/lib/rubocop/callbacksystems/methods/macro_references.rb +238 -0
- data/lib/rubocop/callbacksystems/methods/reference_evidence.rb +74 -0
- data/lib/rubocop/callbacksystems/methods/siblings.rb +24 -0
- data/lib/rubocop/callbacksystems/methods/statement_definitions.rb +68 -0
- data/lib/rubocop/callbacksystems/offense.rb +13 -0
- data/lib/rubocop/callbacksystems/plugin.rb +27 -0
- data/lib/rubocop/callbacksystems/project_index/cache.rb +16 -0
- data/lib/rubocop/callbacksystems/project_index/confinement_cache.rb +40 -0
- data/lib/rubocop/callbacksystems/project_index/constant_references.rb +74 -0
- data/lib/rubocop/callbacksystems/project_index/corrections.rb +35 -0
- data/lib/rubocop/callbacksystems/project_index/coverage.rb +56 -0
- data/lib/rubocop/callbacksystems/project_index/diagnostics.rb +60 -0
- data/lib/rubocop/callbacksystems/project_index/freshness.rb +104 -0
- data/lib/rubocop/callbacksystems/project_index/loader.rb +9 -0
- data/lib/rubocop/callbacksystems/project_index/location.rb +29 -0
- data/lib/rubocop/callbacksystems/project_index/nested_class_confinement.rb +297 -0
- data/lib/rubocop/callbacksystems/project_index/nested_class_method_dispatch.rb +50 -0
- data/lib/rubocop/callbacksystems/project_index/reading.rb +129 -0
- data/lib/rubocop/callbacksystems/project_index/source_file.rb +59 -0
- data/lib/rubocop/callbacksystems/project_index/source_file_match.rb +57 -0
- data/lib/rubocop/callbacksystems/project_index/support.rb +46 -0
- data/lib/rubocop/callbacksystems/source/comment_blocks.rb +43 -0
- data/lib/rubocop/callbacksystems/source/comments.rb +141 -0
- data/lib/rubocop/callbacksystems/source/file_ast.rb +15 -0
- data/lib/rubocop/callbacksystems/source/files_checksum.rb +43 -0
- data/lib/rubocop/callbacksystems/source/single_line_call.rb +181 -0
- data/lib/rubocop/callbacksystems/source/statement_with_comments.rb +131 -0
- data/lib/rubocop/callbacksystems/testing/cop_helpers.rb +42 -0
- data/lib/rubocop/callbacksystems/testing/fixture_call.rb +30 -0
- data/lib/rubocop/callbacksystems/testing/fixture_names.rb +62 -0
- data/lib/rubocop/callbacksystems/testing/path_mapping.rb +104 -0
- data/lib/rubocop/cop/callbacksystems/application_controller_method_definition.rb +44 -0
- data/lib/rubocop/cop/callbacksystems/assert_with_expected_argument.rb +60 -0
- data/lib/rubocop/cop/callbacksystems/base.rb +134 -0
- data/lib/rubocop/cop/callbacksystems/body_order.rb +478 -0
- data/lib/rubocop/cop/callbacksystems/builder_method_parentheses.rb +74 -0
- data/lib/rubocop/cop/callbacksystems/collapse_multiline_expression.rb +140 -0
- data/lib/rubocop/cop/callbacksystems/comment_block_length.rb +137 -0
- data/lib/rubocop/cop/callbacksystems/complex_conditional.rb +75 -0
- data/lib/rubocop/cop/callbacksystems/controller_test_response_assertion.rb +105 -0
- data/lib/rubocop/cop/callbacksystems/controller_test_response_first_assertion.rb +123 -0
- data/lib/rubocop/cop/callbacksystems/data_clump.rb +339 -0
- data/lib/rubocop/cop/callbacksystems/declarative_method_naming.rb +350 -0
- data/lib/rubocop/cop/callbacksystems/delegate_requires_private_option.rb +81 -0
- data/lib/rubocop/cop/callbacksystems/eager_loading_scope_naming.rb +386 -0
- data/lib/rubocop/cop/callbacksystems/early_return.rb +215 -0
- data/lib/rubocop/cop/callbacksystems/empty_line_before_method.rb +168 -0
- data/lib/rubocop/cop/callbacksystems/empty_line_between_sections.rb +169 -0
- data/lib/rubocop/cop/callbacksystems/empty_private_section.rb +107 -0
- data/lib/rubocop/cop/callbacksystems/expanded_empty_class_or_module.rb +71 -0
- data/lib/rubocop/cop/callbacksystems/inline_memoized_computation.rb +110 -0
- data/lib/rubocop/cop/callbacksystems/job_enqueue_methods.rb +93 -0
- data/lib/rubocop/cop/callbacksystems/max_safe_navigation_depth.rb +118 -0
- data/lib/rubocop/cop/callbacksystems/method_invocation_order.rb +261 -0
- data/lib/rubocop/cop/callbacksystems/nested_class_order.rb +255 -0
- data/lib/rubocop/cop/callbacksystems/no_abbreviations.rb +197 -0
- data/lib/rubocop/cop/callbacksystems/no_anemic_delegation.rb +101 -0
- data/lib/rubocop/cop/callbacksystems/no_anemic_record.rb +309 -0
- data/lib/rubocop/cop/callbacksystems/no_assert_select.rb +31 -0
- data/lib/rubocop/cop/callbacksystems/no_bang_method_without_counterpart.rb +451 -0
- data/lib/rubocop/cop/callbacksystems/no_call_method.rb +73 -0
- data/lib/rubocop/cop/callbacksystems/no_comments_in_test_class_body.rb +159 -0
- data/lib/rubocop/cop/callbacksystems/no_consecutive_appends.rb +261 -0
- data/lib/rubocop/cop/callbacksystems/no_custom_route_actions.rb +124 -0
- data/lib/rubocop/cop/callbacksystems/no_direct_eager_loading.rb +90 -0
- data/lib/rubocop/cop/callbacksystems/no_each_with_early_return.rb +58 -0
- data/lib/rubocop/cop/callbacksystems/no_explicit_public_modifier.rb +81 -0
- data/lib/rubocop/cop/callbacksystems/no_hash_with_default_block.rb +68 -0
- data/lib/rubocop/cop/callbacksystems/no_local_variable_return.rb +173 -0
- data/lib/rubocop/cop/callbacksystems/no_manual_accumulation.rb +188 -0
- data/lib/rubocop/cop/callbacksystems/no_mixed_memoization.rb +145 -0
- data/lib/rubocop/cop/callbacksystems/no_nested_modules_in_classes.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/no_nil_at_end_of_method.rb +74 -0
- data/lib/rubocop/cop/callbacksystems/no_redundant_validation_name.rb +37 -0
- data/lib/rubocop/cop/callbacksystems/no_redundant_wrapper_method.rb +161 -0
- data/lib/rubocop/cop/callbacksystems/no_section_divider_comments.rb +105 -0
- data/lib/rubocop/cop/callbacksystems/no_send_in_tests.rb +75 -0
- data/lib/rubocop/cop/callbacksystems/no_service_objects.rb +64 -0
- data/lib/rubocop/cop/callbacksystems/no_single_letter_names.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/no_singleton_class_in_modules.rb +43 -0
- data/lib/rubocop/cop/callbacksystems/no_tap_collection_building.rb +173 -0
- data/lib/rubocop/cop/callbacksystems/no_turbo_stream_in_controllers.rb +52 -0
- data/lib/rubocop/cop/callbacksystems/no_typographic_clutter.rb +159 -0
- data/lib/rubocop/cop/callbacksystems/no_writes_in_get_actions.rb +149 -0
- data/lib/rubocop/cop/callbacksystems/ordered_mixin_arguments.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/plural_controller_names.rb +103 -0
- data/lib/rubocop/cop/callbacksystems/prefer_attr_reader_over_instance_variable.rb +86 -0
- data/lib/rubocop/cop/callbacksystems/prefer_backslash_continuation.rb +478 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_for_state.rb +532 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_new_for_empty_nested_class.rb +247 -0
- data/lib/rubocop/cop/callbacksystems/prefer_class_over_builder_block.rb +124 -0
- data/lib/rubocop/cop/callbacksystems/prefer_default_argument.rb +225 -0
- data/lib/rubocop/cop/callbacksystems/prefer_delegate.rb +242 -0
- data/lib/rubocop/cop/callbacksystems/prefer_delegate_over_instance_variable_assignment.rb +175 -0
- data/lib/rubocop/cop/callbacksystems/prefer_excluding.rb +204 -0
- data/lib/rubocop/cop/callbacksystems/prefer_extract_associated.rb +54 -0
- data/lib/rubocop/cop/callbacksystems/prefer_it_block_parameter.rb +156 -0
- data/lib/rubocop/cop/callbacksystems/prefer_many.rb +57 -0
- data/lib/rubocop/cop/callbacksystems/prefer_method_over_lambda.rb +211 -0
- data/lib/rubocop/cop/callbacksystems/prefer_method_over_variable.rb +172 -0
- data/lib/rubocop/cop/callbacksystems/prefer_module_for_static_class.rb +286 -0
- data/lib/rubocop/cop/callbacksystems/prefer_ordinal_array_access.rb +136 -0
- data/lib/rubocop/cop/callbacksystems/prefer_positive_wrap.rb +265 -0
- data/lib/rubocop/cop/callbacksystems/prefer_squish.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/prefer_stringify_keys.rb +24 -0
- data/lib/rubocop/cop/callbacksystems/prefer_symbolize_keys.rb +24 -0
- data/lib/rubocop/cop/callbacksystems/prefer_turbo_page_refresh_broadcasts.rb +40 -0
- data/lib/rubocop/cop/callbacksystems/private_constants_in_private_section.rb +160 -0
- data/lib/rubocop/cop/callbacksystems/private_delegate_placement.rb +342 -0
- data/lib/rubocop/cop/callbacksystems/private_methods_in_private_section.rb +70 -0
- data/lib/rubocop/cop/callbacksystems/private_nested_class_method_visibility.rb +269 -0
- data/lib/rubocop/cop/callbacksystems/private_nested_classes.rb +94 -0
- data/lib/rubocop/cop/callbacksystems/public_methods_must_have_tests.rb +190 -0
- data/lib/rubocop/cop/callbacksystems/redundant_concern_extend.rb +157 -0
- data/lib/rubocop/cop/callbacksystems/redundant_constant_freeze.rb +57 -0
- data/lib/rubocop/cop/callbacksystems/redundant_constant_namespace.rb +134 -0
- data/lib/rubocop/cop/callbacksystems/reflow_comments.rb +133 -0
- data/lib/rubocop/cop/callbacksystems/repeated_fixture_in_tests.rb +106 -0
- data/lib/rubocop/cop/callbacksystems/restricted_controller_actions.rb +85 -0
- data/lib/rubocop/cop/callbacksystems/routes_module_scope.rb +336 -0
- data/lib/rubocop/cop/callbacksystems/single_line_setup_block.rb +107 -0
- data/lib/rubocop/cop/callbacksystems/single_singleton_class.rb +49 -0
- data/lib/rubocop/cop/callbacksystems/single_top_level_class_or_module.rb +160 -0
- data/lib/rubocop/cop/callbacksystems/single_use_fixture_variable.rb +211 -0
- data/lib/rubocop/cop/callbacksystems/single_use_setup_variable.rb +684 -0
- data/lib/rubocop/cop/callbacksystems/singular_scope_names.rb +47 -0
- data/lib/rubocop/cop/callbacksystems/space_inside_percent_literal.rb +73 -0
- data/lib/rubocop/cop/callbacksystems/system_test_browser_driver.rb +30 -0
- data/lib/rubocop/cop/callbacksystems/test_method_definition.rb +86 -0
- data/lib/rubocop/cop/callbacksystems/test_method_order.rb +319 -0
- data/lib/rubocop/cop/callbacksystems/too_many_instance_variables.rb +78 -0
- data/lib/rubocop/cop/callbacksystems/too_many_local_variables.rb +77 -0
- data/lib/rubocop/cop/callbacksystems/too_many_validation_guards.rb +84 -0
- data/lib/rubocop/cop/callbacksystems/unnecessary_local_variable.rb +176 -0
- data/lib/rubocop/cop/callbacksystems/unused_private_method_in_nested_class.rb +203 -0
- data/lib/rubocop-callbacksystems.rb +21 -0
- data/rubocop.yml +685 -0
- metadata +337 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Keeps Turbo Stream markup out of controllers. A call on the `turbo_stream`
|
|
2
|
+
# tag builder renders elements, and markup belongs to the view, where the same
|
|
3
|
+
# `turbo_stream.replace` reads as a template while the controller only decides
|
|
4
|
+
# what to render and with which status.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# # bad - the controller builds the stream
|
|
8
|
+
# class MessagesController < ApplicationController
|
|
9
|
+
# def update
|
|
10
|
+
# if @message.update(message_params)
|
|
11
|
+
# redirect_to @message
|
|
12
|
+
# else
|
|
13
|
+
# render turbo_stream: turbo_stream.replace(@message, partial: "messages/form")
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good - the controller renders and the view builds the stream
|
|
19
|
+
# class MessagesController < ApplicationController
|
|
20
|
+
# def update
|
|
21
|
+
# if @message.update(message_params)
|
|
22
|
+
# redirect_to @message
|
|
23
|
+
# else
|
|
24
|
+
# respond_to do |format|
|
|
25
|
+
# format.turbo_stream { render status: :unprocessable_content }
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# # app/views/messages/update.turbo_stream.erb
|
|
32
|
+
# <%= turbo_stream.replace @message, partial: "messages/form" %>
|
|
33
|
+
#
|
|
34
|
+
class RuboCop::Cop::Callbacksystems::NoTurboStreamInControllers < RuboCop::Cop::Callbacksystems::Base
|
|
35
|
+
MESSAGE = "Build the Turbo Stream in a `.turbo_stream.erb` view instead of calling `turbo_stream.%<method>s` in " \
|
|
36
|
+
"the controller."
|
|
37
|
+
|
|
38
|
+
def on_send(node)
|
|
39
|
+
add_offense(node, message: format(MESSAGE, method: node.method_name)) if turbo_stream_builder_call?(node)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias on_csend on_send
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
def turbo_stream_builder_call?(node)
|
|
46
|
+
controller_file?(processed_source.file_path) && turbo_stream_builder?(node.receiver)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def turbo_stream_builder?(receiver)
|
|
50
|
+
call_on_self?(receiver) && receiver.method?(:turbo_stream)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AI agents (and pasting from formatted text) sneak typographic characters into
|
|
2
|
+
# source: smart quotes, dashes, ellipses, invisible spaces. What counts as
|
|
3
|
+
# clutter depends on where the character sits.
|
|
4
|
+
#
|
|
5
|
+
# A comment only describes the code, so a typographic spelling of punctuation is
|
|
6
|
+
# noise there: it breaks grep and reads as sloppy review. A string shows the text
|
|
7
|
+
# it holds, so a character a reader can see is content someone wrote on purpose.
|
|
8
|
+
# Only the invisible marks are clutter inside a string, since those change what
|
|
9
|
+
# the string compares as without showing it, so an equality that reads as true
|
|
10
|
+
# comes out false and a `split` misses where you meant it to cut.
|
|
11
|
+
#
|
|
12
|
+
# Arrows, bullets, check marks and guillemets are left alone in both places. They
|
|
13
|
+
# carry meaning of their own and no ASCII spelling replaces them.
|
|
14
|
+
#
|
|
15
|
+
# The characters below are written as escapes, since this file is checked too.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# # bad - an em dash in a comment
|
|
19
|
+
# # name \u2014 required
|
|
20
|
+
# name = params[:name]
|
|
21
|
+
#
|
|
22
|
+
# # bad - a double hyphen (two `-` in a row) standing in for a dash
|
|
23
|
+
# # name \u002D\u002D required
|
|
24
|
+
#
|
|
25
|
+
# # bad - a no-break space in a string, which compares unlike a plain space
|
|
26
|
+
# label = "first\u00A0name"
|
|
27
|
+
#
|
|
28
|
+
# # good - plain ASCII punctuation in the comment
|
|
29
|
+
# # name, required
|
|
30
|
+
# name = params[:name]
|
|
31
|
+
#
|
|
32
|
+
# # good - a visible character left as written inside the string
|
|
33
|
+
# label = "said \u201Cyes\u201D"
|
|
34
|
+
#
|
|
35
|
+
class RuboCop::Cop::Callbacksystems::NoTypographicClutter < RuboCop::Cop::Callbacksystems::Base
|
|
36
|
+
extend RuboCop::Cop::AutoCorrector
|
|
37
|
+
|
|
38
|
+
def on_new_investigation
|
|
39
|
+
prose_comments_in(processed_source).each do |comment|
|
|
40
|
+
report_each Clutter.new(comment.source_range, comment.text, clutter: COMMENT_CLUTTER), rewrites_comments: true
|
|
41
|
+
report_each DoubleHyphens.new(comment), rewrites_comments: true
|
|
42
|
+
end
|
|
43
|
+
string_ranges.each { report_each Clutter.new(it, it.source, clutter: STRING_CLUTTER) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
# Marks a reader cannot see: no-break space, soft hyphen, narrow no-break space, word joiner, BOM.
|
|
48
|
+
INVISIBLE_POINTS = [ 0x00A0, 0x00AD, 0x202F, 0x2060, 0xFEFF ]
|
|
49
|
+
COMMENT_CLUTTER = Set[*INVISIBLE_POINTS, 0x2013, 0x2014, 0x2026, 0x2212, *0x200B..0x200D, *0x2018..0x201F]
|
|
50
|
+
# Zero-width marks stay out of strings, since they join an emoji sequence and deleting one splits the emoji.
|
|
51
|
+
STRING_CLUTTER = Set[*INVISIBLE_POINTS]
|
|
52
|
+
|
|
53
|
+
def string_ranges
|
|
54
|
+
nodes_in(processed_source.ast, :str).map { string_range_of(it) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def string_range_of(node)
|
|
58
|
+
node.loc?(:heredoc_body) ? node.loc.heredoc_body : node.source_range
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Clutter
|
|
62
|
+
def initialize(range, text, clutter:)
|
|
63
|
+
@range = range
|
|
64
|
+
@text = text
|
|
65
|
+
@clutter = clutter
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def each_offense
|
|
69
|
+
occurrences.each { yield it.offense }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
attr_reader :range, :text, :clutter
|
|
74
|
+
|
|
75
|
+
def occurrences
|
|
76
|
+
text.each_char.with_index.filter_map do |character, offset|
|
|
77
|
+
Occurrence.new(range, character, offset) if clutter.include?(character.ord)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class Occurrence
|
|
82
|
+
MESSAGE = "Remove typographic character `%<character>s` (U+%<code_point>s). Use ASCII equivalents."
|
|
83
|
+
# Only characters with one meaning-preserving ASCII spelling, keyed by escape so this file passes its own cop.
|
|
84
|
+
ASCII_EQUIVALENT = {
|
|
85
|
+
"\u2018" => "'", "\u2019" => "'",
|
|
86
|
+
"\u201C" => "\"", "\u201D" => "\"",
|
|
87
|
+
"\u2026" => "...",
|
|
88
|
+
"\u00A0" => " ", "\u202F" => " ",
|
|
89
|
+
"\u200B" => "", "\u200C" => "", "\u200D" => "", "\u2060" => "", "\uFEFF" => "",
|
|
90
|
+
"\u00AD" => ""
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def initialize(source_range, character, offset)
|
|
94
|
+
@source_range = source_range
|
|
95
|
+
@character = character
|
|
96
|
+
@offset = offset
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def offense
|
|
100
|
+
RuboCop::Callbacksystems::Offense.new(range, message, correcting: !replacement.nil?) { correct(it) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
attr_reader :source_range, :character, :offset
|
|
105
|
+
|
|
106
|
+
def range
|
|
107
|
+
source_range.with(begin_pos: source_range.begin_pos + offset, end_pos: range_end)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def range_end
|
|
111
|
+
source_range.begin_pos + offset + character.length
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def message
|
|
115
|
+
format(MESSAGE, character: character, code_point: code_point)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def code_point
|
|
119
|
+
format("%04X", character.ord)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def replacement
|
|
123
|
+
ASCII_EQUIVALENT[character]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def correct(corrector)
|
|
127
|
+
corrector.replace(range, replacement)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Double hyphens standing in for dashes. Which punctuation replaces one depends on the sentence, so there is no fix.
|
|
133
|
+
class DoubleHyphens
|
|
134
|
+
MESSAGE = "Avoid `--` in comments; use a comma, colon, or parentheses."
|
|
135
|
+
|
|
136
|
+
def initialize(comment)
|
|
137
|
+
@comment = comment
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def each_offense
|
|
141
|
+
offsets.each { yield RuboCop::Callbacksystems::Offense.new(range_at(it), MESSAGE) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
attr_reader :comment
|
|
146
|
+
|
|
147
|
+
def offsets
|
|
148
|
+
comment.text.enum_for(:scan, /--/).map { Regexp.last_match.begin(0) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def range_at(offset)
|
|
152
|
+
comment.source_range.with(begin_pos: comment.source_range.begin_pos + offset, end_pos: range_end(offset))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def range_end(offset)
|
|
156
|
+
comment.source_range.begin_pos + offset + 2
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Keeps GET actions free of writes and enqueued side effects. HTTP promises a
|
|
2
|
+
# GET changes nothing, and browsers, crawlers and link previews act on that
|
|
3
|
+
# promise by prefetching, retrying and replaying it, so a write in `show` runs
|
|
4
|
+
# as many times as the page is opened and by clients that never meant to act.
|
|
5
|
+
# Only applies to index, show, new, and edit in controller classes, and only
|
|
6
|
+
# to calls made directly in the action body, so a write pushed into a model
|
|
7
|
+
# method stays a deliberate choice.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad - GET action persists a change
|
|
11
|
+
# class MessagesController < ApplicationController
|
|
12
|
+
# def show
|
|
13
|
+
# @message = Message.find(params[:id])
|
|
14
|
+
# @message.update!(read_at: Time.current)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # bad - GET action enqueues a side effect
|
|
19
|
+
# class MessagesController < ApplicationController
|
|
20
|
+
# def show
|
|
21
|
+
# ReadReceiptJob.perform_later(params[:id])
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good - the write moves behind a non-GET action
|
|
26
|
+
# class Messages::ReadingsController < ApplicationController
|
|
27
|
+
# def create
|
|
28
|
+
# Message.find(params[:message_id]).update!(read_at: Time.current)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
class RuboCop::Cop::Callbacksystems::NoWritesInGetActions < RuboCop::Cop::Callbacksystems::Base
|
|
33
|
+
def on_def(node)
|
|
34
|
+
report_each GetAction.new(node)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
class GetAction
|
|
39
|
+
include RuboCop::Callbacksystems::Helpers
|
|
40
|
+
|
|
41
|
+
MESSAGE = "`%<action>s` serves GET requests, so it must not call `%<method>s`. Move the write behind a non-GET " \
|
|
42
|
+
"action."
|
|
43
|
+
GET_ACTIONS = %i[ index show new edit ]
|
|
44
|
+
WRITE_METHODS = %i[
|
|
45
|
+
create create! save save! update update! update_attribute update_column update_columns
|
|
46
|
+
destroy destroy! destroy_all delete delete_all update_all touch increment! decrement! toggle!
|
|
47
|
+
find_or_create_by find_or_create_by! create_or_find_by create_or_find_by!
|
|
48
|
+
first_or_create first_or_create! increment_counter decrement_counter update_counters
|
|
49
|
+
insert_all insert_all! upsert upsert_all deliver_now perform_now
|
|
50
|
+
]
|
|
51
|
+
DEFERRED_SUFFIX = "_later"
|
|
52
|
+
REQUEST_STATE_RECEIVERS = %i[ session cookies flash params headers request response ]
|
|
53
|
+
|
|
54
|
+
def initialize(node)
|
|
55
|
+
@node = node
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def each_offense
|
|
59
|
+
writes.each { yield RuboCop::Callbacksystems::Offense.new(it, message_for(it)) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
attr_reader :node
|
|
64
|
+
|
|
65
|
+
def writes
|
|
66
|
+
if get_action?
|
|
67
|
+
node.each_node(:call).select { write?(it) && !inside_deferred_callable?(it) }
|
|
68
|
+
else
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_action?
|
|
74
|
+
GET_ACTIONS.include?(node.method_name) && direct_method_definition?(node) && instance_method? &&
|
|
75
|
+
enclosing_controller_class? && public_method?(node)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def instance_method?
|
|
79
|
+
RuboCop::Callbacksystems::Methods::Domain.new(node).scope == :instance
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def enclosing_controller_class?
|
|
83
|
+
controller_class?(enclosing_class_or_module_of(node))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def write?(call)
|
|
87
|
+
write_name?(call.method_name) && !RequestStateReceiver.new(call.receiver).request_state?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def write_name?(name)
|
|
91
|
+
WRITE_METHODS.include?(name) || name.end_with?(DEFERRED_SUFFIX)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def inside_deferred_callable?(call)
|
|
95
|
+
call.each_ancestor.take_while { !it.equal?(node) }.any? do |ancestor|
|
|
96
|
+
ancestor.any_def_type? || deferred_callable_block?(ancestor)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def message_for(write)
|
|
101
|
+
format(MESSAGE, action: node.method_name, method: write.method_name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class RequestStateReceiver
|
|
105
|
+
def initialize(receiver)
|
|
106
|
+
@receiver = receiver
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def request_state?
|
|
110
|
+
ReceiverPath.new(receiver).any? { request_state_reader?(it) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
attr_reader :receiver
|
|
115
|
+
|
|
116
|
+
def request_state_reader?(candidate)
|
|
117
|
+
candidate.call_type? && REQUEST_STATE_RECEIVERS.include?(candidate.method_name) &&
|
|
118
|
+
(candidate.receiver.nil? || candidate.receiver.self_type?)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class ReceiverPath
|
|
122
|
+
include Enumerable
|
|
123
|
+
|
|
124
|
+
def initialize(receiver)
|
|
125
|
+
@receiver = receiver
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def each
|
|
129
|
+
if block_given?
|
|
130
|
+
current = receiver
|
|
131
|
+
until current.nil?
|
|
132
|
+
yield current
|
|
133
|
+
current = if current.begin_type? && current.children.one?
|
|
134
|
+
current.children.first
|
|
135
|
+
elsif current.call_type?
|
|
136
|
+
current.receiver
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
to_enum(__method__)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
attr_reader :receiver
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Ensures mixin arguments are sorted alphabetically.
|
|
2
|
+
#
|
|
3
|
+
# When including, extending, or prepending multiple modules on the same line,
|
|
4
|
+
# they should be sorted alphabetically for consistency and easier scanning.
|
|
5
|
+
#
|
|
6
|
+
# The order of one `include` decides which module wins when two define the same
|
|
7
|
+
# method, so sorting can change what the class does. The fix runs under `-A`
|
|
8
|
+
# rather than `-a` for that reason. Write the modules on separate lines when the
|
|
9
|
+
# order between them matters.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad - unsorted
|
|
13
|
+
# include Searchable, Confirmable, Accessible
|
|
14
|
+
#
|
|
15
|
+
# # good - sorted alphabetically
|
|
16
|
+
# include Accessible, Confirmable, Searchable
|
|
17
|
+
#
|
|
18
|
+
# # good - single module (nothing to sort)
|
|
19
|
+
# include Searchable
|
|
20
|
+
#
|
|
21
|
+
# # good - separate lines (each line is independent)
|
|
22
|
+
# include Searchable
|
|
23
|
+
# include Confirmable # depends on Searchable
|
|
24
|
+
#
|
|
25
|
+
class RuboCop::Cop::Callbacksystems::OrderedMixinArguments < RuboCop::Cop::Callbacksystems::Base
|
|
26
|
+
extend RuboCop::Cop::AutoCorrector
|
|
27
|
+
|
|
28
|
+
def on_send(node)
|
|
29
|
+
report MixinCall.new(node, processed_source)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias on_csend on_send
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
class MixinCall
|
|
36
|
+
include RuboCop::Callbacksystems::Helpers
|
|
37
|
+
|
|
38
|
+
MESSAGE = "Sort mixin arguments alphabetically: `%<sorted>s`."
|
|
39
|
+
|
|
40
|
+
def initialize(node, processed_source)
|
|
41
|
+
@node = node
|
|
42
|
+
@processed_source = processed_source
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def offense
|
|
46
|
+
RuboCop::Callbacksystems::Offense.new(node, message, correcting: uncommented?) { correct(it) } if unsorted?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
attr_reader :node, :processed_source
|
|
51
|
+
delegate :arguments, to: :node, private: true
|
|
52
|
+
|
|
53
|
+
def unsorted?
|
|
54
|
+
mixin_macro?(node) && arguments.many? && names != sorted_names
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def names
|
|
58
|
+
@names ||= arguments.map(&:source)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def sorted_names
|
|
62
|
+
@sorted_names ||= names.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def message
|
|
66
|
+
format(MESSAGE, sorted: sorted_names.join(", "))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Once arguments move, a comment on their lines no longer names the element or precedence it was written for.
|
|
70
|
+
def uncommented?
|
|
71
|
+
processed_source.comments.none? { it.loc.line.between?(node.first_line, node.last_line) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def correct(corrector)
|
|
75
|
+
corrector.replace(range_spanning(arguments), sorted_names.join(", "))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Ensures controller names are plural. A controller stands for the collection
|
|
2
|
+
# its routes address, `/users` and `/users/1` alike, so the plural is the name
|
|
3
|
+
# Rails derives the route and the resource from, and a singular one reads as a
|
|
4
|
+
# model where a reader expects a resource.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# # bad
|
|
8
|
+
# class UserController < ApplicationController
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# # good
|
|
12
|
+
# class UsersController < ApplicationController
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # good - already plural
|
|
16
|
+
# class PeopleController < ApplicationController
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good - ignored names (configurable)
|
|
20
|
+
# class ApplicationController < ActionController::Base
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# class BaseController < ApplicationController
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class RuboCop::Cop::Callbacksystems::PluralControllerNames < RuboCop::Cop::Callbacksystems::Base
|
|
27
|
+
def on_class(node)
|
|
28
|
+
report ControllerClass.new(node, ignored_names: cop_config["IgnoredNames"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
class ControllerClass
|
|
33
|
+
include RuboCop::Callbacksystems::Helpers
|
|
34
|
+
|
|
35
|
+
MESSAGE = "Controller names should be plural. Use `%<plural>sController` instead of `%<singular>sController`."
|
|
36
|
+
|
|
37
|
+
def initialize(node, ignored_names:)
|
|
38
|
+
@node = node
|
|
39
|
+
@ignored_names = ignored_names
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def offense
|
|
43
|
+
RuboCop::Callbacksystems::Offense.new(node.loc.name, message) if singular?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
attr_reader :node, :ignored_names
|
|
48
|
+
|
|
49
|
+
def singular?
|
|
50
|
+
controller_class?(node) && named_controller? && !resource_name.excluded?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def named_controller?
|
|
54
|
+
class_name.end_with?("Controller")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def class_name
|
|
58
|
+
constant_name_of(node.identifier)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resource_name
|
|
62
|
+
@resource_name ||= ResourceName.new(class_name.delete_suffix("Controller").split("::").last, ignored_names:)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def message
|
|
66
|
+
format(MESSAGE, plural: resource_name.plural, singular: resource_name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class ResourceName
|
|
71
|
+
def initialize(name, ignored_names:)
|
|
72
|
+
@name = name
|
|
73
|
+
@ignored_names = ignored_names
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def excluded?
|
|
77
|
+
ignored? || plural?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_s
|
|
81
|
+
name
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def plural
|
|
85
|
+
name.pluralize
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
attr_reader :name, :ignored_names
|
|
90
|
+
|
|
91
|
+
def ignored?
|
|
92
|
+
ignored_names.include?(name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def plural?
|
|
96
|
+
singular != name || singular.pluralize == name
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def singular
|
|
100
|
+
@singular ||= name.singularize
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Detects direct use of instance variables that were assigned in initialize.
|
|
2
|
+
# Reading `@node` where `node` would do ties the method to how the value is
|
|
3
|
+
# stored, so a getter that later computes or memoizes it has to touch every
|
|
4
|
+
# reader. Behind an `attr_reader` the method reads the name and nothing else.
|
|
5
|
+
#
|
|
6
|
+
# This cop only applies to instance variables assigned in the constructor.
|
|
7
|
+
# Instance variables assigned elsewhere (like in controllers) are not flagged.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad - using @node directly when it was set in initialize
|
|
11
|
+
# class Foo
|
|
12
|
+
# def initialize(node)
|
|
13
|
+
# @node = node
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# def process
|
|
17
|
+
# @node.children
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # good - using attr_reader
|
|
22
|
+
# class Foo
|
|
23
|
+
# def initialize(node)
|
|
24
|
+
# @node = node
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def process
|
|
28
|
+
# node.children
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# private
|
|
32
|
+
# attr_reader :node
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
class RuboCop::Cop::Callbacksystems::PreferAttrReaderOverInstanceVariable < RuboCop::Cop::Callbacksystems::Base
|
|
36
|
+
def on_class(node)
|
|
37
|
+
report_each InstanceVariableReads.new(node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias on_module on_class
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
class InstanceVariableReads
|
|
44
|
+
include RuboCop::Callbacksystems::Helpers
|
|
45
|
+
|
|
46
|
+
MESSAGE = "Use `%<name>s` (via attr_reader) instead of `@%<name>s` for instance variables assigned in initialize."
|
|
47
|
+
|
|
48
|
+
def initialize(class_node)
|
|
49
|
+
@class_node = class_node
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def each_offense
|
|
53
|
+
reads_of_constructor_variables.each { yield offense_for(it) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
attr_reader :class_node
|
|
58
|
+
|
|
59
|
+
def reads_of_constructor_variables
|
|
60
|
+
reads_outside_constructor.select { constructor_variable_names.include?(it.name) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reads_outside_constructor
|
|
64
|
+
nodes_in(class_node.body, :ivar).select { instance_method_read?(it) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def instance_method_read?(read)
|
|
68
|
+
method = enclosing_method_of(read)
|
|
69
|
+
method&.def_type? && direct_method_definition?(method) && !method.method?(:initialize) &&
|
|
70
|
+
enclosing_class_or_module_of(read) == class_node && self_preserved_between?(read, boundary: method)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def constructor_variable_names
|
|
74
|
+
@constructor_variable_names ||= nodes_in(initialize_method&.body, :ivasgn)
|
|
75
|
+
.select { self_preserved_between?(it, boundary: initialize_method) }.to_set(&:name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def initialize_method
|
|
79
|
+
direct_method_nodes_in(class_node.body).find { it.method?(:initialize) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def offense_for(read)
|
|
83
|
+
RuboCop::Callbacksystems::Offense.new(read, format(MESSAGE, name: name_without_sigil(read.name)))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|