gitlab-styles 7.0.0 → 9.0.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -0
  3. data/Gemfile +3 -3
  4. data/gitlab-styles.gemspec +6 -6
  5. data/lib/gitlab/styles/common/banned_constants.rb +28 -0
  6. data/lib/gitlab/styles/rubocop/model_helpers.rb +1 -1
  7. data/lib/gitlab/styles/rubocop.rb +2 -2
  8. data/lib/gitlab/styles/version.rb +1 -1
  9. data/lib/rubocop/cop/active_record_dependent.rb +32 -0
  10. data/lib/rubocop/cop/active_record_serialize.rb +20 -0
  11. data/lib/rubocop/cop/avoid_return_from_blocks.rb +79 -0
  12. data/lib/rubocop/cop/code_reuse/active_record.rb +80 -0
  13. data/lib/rubocop/cop/custom_error_class.rb +67 -0
  14. data/lib/rubocop/cop/fips/md5.rb +27 -0
  15. data/lib/rubocop/cop/fips/open_ssl.rb +31 -0
  16. data/lib/rubocop/cop/fips/sha1.rb +27 -0
  17. data/lib/rubocop/cop/gem_fetcher.rb +37 -0
  18. data/lib/rubocop/cop/in_batches.rb +18 -0
  19. data/lib/rubocop/cop/internal_affairs/deprecate_cop_helper.rb +42 -0
  20. data/lib/rubocop/cop/line_break_after_guard_clauses.rb +101 -0
  21. data/lib/rubocop/cop/line_break_around_conditional_block.rb +127 -0
  22. data/lib/rubocop/cop/migration/update_large_table.rb +61 -0
  23. data/lib/rubocop/cop/performance/rubyzip.rb +36 -0
  24. data/lib/rubocop/cop/polymorphic_associations.rb +25 -0
  25. data/lib/rubocop/cop/rails/include_url_helper.rb +28 -0
  26. data/lib/rubocop/cop/redirect_with_status.rb +46 -0
  27. data/lib/rubocop/cop/rspec/base.rb +12 -0
  28. data/lib/rubocop/cop/rspec/empty_line_after_let_block.rb +61 -0
  29. data/lib/rubocop/cop/rspec/empty_line_after_shared_example.rb +63 -0
  30. data/lib/rubocop/cop/rspec/example_starting_character.rb +122 -0
  31. data/lib/rubocop/cop/rspec/have_link_parameters.rb +45 -0
  32. data/lib/rubocop/cop/rspec/single_line_hook.rb +43 -0
  33. data/lib/rubocop/cop/rspec/verbose_include_metadata.rb +73 -0
  34. data/lib/rubocop/cop/style/hash_transformation.rb +81 -0
  35. data/lib/rubocop/cop/style/open_struct_use.rb +40 -0
  36. data/lib/rubocop/cop/without_reactive_cache.rb +16 -0
  37. data/rubocop-default.yml +1 -0
  38. data/rubocop-fips.yml +15 -0
  39. data/rubocop-layout.yml +1 -1
  40. data/rubocop-rspec.yml +13 -5
  41. metadata +44 -47
  42. data/lib/gitlab/styles/rubocop/cop/active_record_dependent.rb +0 -36
  43. data/lib/gitlab/styles/rubocop/cop/active_record_serialize.rb +0 -24
  44. data/lib/gitlab/styles/rubocop/cop/avoid_return_from_blocks.rb +0 -81
  45. data/lib/gitlab/styles/rubocop/cop/code_reuse/active_record.rb +0 -84
  46. data/lib/gitlab/styles/rubocop/cop/custom_error_class.rb +0 -73
  47. data/lib/gitlab/styles/rubocop/cop/gem_fetcher.rb +0 -41
  48. data/lib/gitlab/styles/rubocop/cop/in_batches.rb +0 -22
  49. data/lib/gitlab/styles/rubocop/cop/internal_affairs/deprecate_cop_helper.rb +0 -43
  50. data/lib/gitlab/styles/rubocop/cop/line_break_after_guard_clauses.rb +0 -104
  51. data/lib/gitlab/styles/rubocop/cop/line_break_around_conditional_block.rb +0 -132
  52. data/lib/gitlab/styles/rubocop/cop/migration/update_large_table.rb +0 -64
  53. data/lib/gitlab/styles/rubocop/cop/performance/rubyzip.rb +0 -39
  54. data/lib/gitlab/styles/rubocop/cop/polymorphic_associations.rb +0 -29
  55. data/lib/gitlab/styles/rubocop/cop/rails/include_url_helper.rb +0 -31
  56. data/lib/gitlab/styles/rubocop/cop/redirect_with_status.rb +0 -50
  57. data/lib/gitlab/styles/rubocop/cop/rspec/base.rb +0 -18
  58. data/lib/gitlab/styles/rubocop/cop/rspec/empty_line_after_final_let_it_be.rb +0 -51
  59. data/lib/gitlab/styles/rubocop/cop/rspec/empty_line_after_let_block.rb +0 -65
  60. data/lib/gitlab/styles/rubocop/cop/rspec/empty_line_after_shared_example.rb +0 -65
  61. data/lib/gitlab/styles/rubocop/cop/rspec/example_starting_character.rb +0 -124
  62. data/lib/gitlab/styles/rubocop/cop/rspec/have_link_parameters.rb +0 -48
  63. data/lib/gitlab/styles/rubocop/cop/rspec/single_line_hook.rb +0 -45
  64. data/lib/gitlab/styles/rubocop/cop/rspec/verbose_include_metadata.rb +0 -75
  65. data/lib/gitlab/styles/rubocop/cop/style/hash_transformation.rb +0 -87
  66. data/lib/gitlab/styles/rubocop/cop/style/open_struct_use.rb +0 -43
  67. data/lib/gitlab/styles/rubocop/cop/without_reactive_cache.rb +0 -20
  68. data/lib/gitlab/styles/rubocop/rspec/helpers.rb +0 -18
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubocop
4
+ module Cop
5
+ # Ensures a line break after guard clauses.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # return unless condition
10
+ # do_stuff
11
+ #
12
+ # # good
13
+ # return unless condition
14
+ #
15
+ # do_stuff
16
+ #
17
+ # # bad
18
+ # raise if condition
19
+ # do_stuff
20
+ #
21
+ # # good
22
+ # raise if condition
23
+ #
24
+ # do_stuff
25
+ #
26
+ # Multiple guard clauses are allowed without
27
+ # line break.
28
+ #
29
+ # # good
30
+ # return unless condition_a
31
+ # return unless condition_b
32
+ #
33
+ # do_stuff
34
+ #
35
+ # Guard clauses in case statement are allowed without
36
+ # line break.
37
+ #
38
+ # # good
39
+ # case model
40
+ # when condition_a
41
+ # return true unless condition_b
42
+ # when
43
+ # ...
44
+ # end
45
+ #
46
+ # Guard clauses before end are allowed without
47
+ # line break.
48
+ #
49
+ # # good
50
+ # if condition_a
51
+ # do_something
52
+ # else
53
+ # do_something_else
54
+ # return unless condition
55
+ # end
56
+ #
57
+ # do_something_more
58
+ class LineBreakAfterGuardClauses < RuboCop::Cop::Base
59
+ MSG = 'Add a line break after guard clauses'
60
+
61
+ # @!method guard_clause_node?(node)
62
+ def_node_matcher :guard_clause_node?, <<-PATTERN
63
+ [{(send nil? {:raise :fail :throw} ...) return break next} single_line?]
64
+ PATTERN
65
+
66
+ def on_if(node)
67
+ return unless node.single_line?
68
+ return unless guard_clause?(node)
69
+ return if next_line(node).blank? || clause_last_line?(next_line(node)) || guard_clause?(next_sibling(node))
70
+
71
+ add_offense(node)
72
+ end
73
+
74
+ def autocorrect(node)
75
+ lambda do |corrector|
76
+ corrector.insert_after(node.loc.expression, "\n")
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def guard_clause?(node)
83
+ return false unless node.if_type?
84
+
85
+ guard_clause_node?(node.if_branch)
86
+ end
87
+
88
+ def next_sibling(node)
89
+ node.parent.children[node.sibling_index + 1]
90
+ end
91
+
92
+ def next_line(node)
93
+ processed_source[node.loc.line]
94
+ end
95
+
96
+ def clause_last_line?(line)
97
+ line =~ /^\s*(?:end|elsif|else|when|rescue|ensure)/
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubocop
4
+ module Cop
5
+ # Ensures a line break around conditional blocks.
6
+ #
7
+ # @example
8
+ # # bad
9
+ # do_something
10
+ # if condition
11
+ # do_extra_stuff
12
+ # end
13
+ # do_something_more
14
+ #
15
+ # # good
16
+ # do_something
17
+ #
18
+ # if condition
19
+ # do_extra_stuff
20
+ # end
21
+ #
22
+ # do_something_more
23
+ #
24
+ # # bad
25
+ # do_something
26
+ # unless condition
27
+ # do_extra_stuff
28
+ # end
29
+ #
30
+ # do_something_more
31
+ #
32
+ # # good
33
+ # def a_method
34
+ # if condition
35
+ # do_something
36
+ # end
37
+ # end
38
+ #
39
+ # # good
40
+ # on_block do
41
+ # if condition
42
+ # do_something
43
+ # end
44
+ # end
45
+ class LineBreakAroundConditionalBlock < RuboCop::Cop::Base
46
+ extend RuboCop::Cop::AutoCorrector
47
+ include RuboCop::Cop::RangeHelp
48
+
49
+ MSG = 'Add a line break around conditional blocks'
50
+
51
+ def on_if(node)
52
+ # This cop causes errors in haml files, so let's skip those
53
+ return if in_haml?(node)
54
+ return if node.single_line?
55
+ return unless node.if? || node.unless?
56
+
57
+ corrector_block = lambda do |corrector|
58
+ line = range_by_whole_lines(node.source_range)
59
+
60
+ corrector.insert_before(line, "\n") unless previous_line_valid?(node)
61
+ corrector.insert_after(line, "\n") unless last_line_valid?(node)
62
+ end
63
+
64
+ add_offense(node, &corrector_block) unless previous_line_valid?(node)
65
+ add_offense(node, &corrector_block) unless last_line_valid?(node)
66
+ end
67
+
68
+ private
69
+
70
+ def previous_line_valid?(node)
71
+ previous_line(node).empty? ||
72
+ start_clause_line?(previous_line(node)) ||
73
+ block_start?(previous_line(node)) ||
74
+ begin_line?(previous_line(node)) ||
75
+ assignment_line?(previous_line(node)) ||
76
+ rescue_line?(previous_line(node))
77
+ end
78
+
79
+ def last_line_valid?(node)
80
+ last_line(node).empty? ||
81
+ end_line?(last_line(node)) ||
82
+ end_clause_line?(last_line(node))
83
+ end
84
+
85
+ def previous_line(node)
86
+ processed_source[node.loc.line - 2]
87
+ end
88
+
89
+ def last_line(node)
90
+ processed_source[node.loc.last_line]
91
+ end
92
+
93
+ def start_clause_line?(line)
94
+ line =~ /^\s*(def|=end|#|module|class|if|unless|else|elsif|ensure|when)/
95
+ end
96
+
97
+ def end_clause_line?(line)
98
+ line =~ /^\s*(#|rescue|else|elsif|when)/
99
+ end
100
+
101
+ def begin_line?(line)
102
+ # an assignment followed by a begin or ust a begin
103
+ line =~ /^\s*(@?(\w|\|+|=|\[|\]|\s)+begin|begin)/
104
+ end
105
+
106
+ def assignment_line?(line)
107
+ line =~ /^\s*.*=/
108
+ end
109
+
110
+ def rescue_line?(line)
111
+ line =~ /^\s*rescue/
112
+ end
113
+
114
+ def block_start?(line)
115
+ line.match(/ (do|{)( \|.*?\|)?\s?(#.+)?\z/)
116
+ end
117
+
118
+ def end_line?(line)
119
+ line =~ /^\s*(end|})/
120
+ end
121
+
122
+ def in_haml?(node)
123
+ node.location.expression.source_buffer.name.end_with?('.haml.rb')
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../../../gitlab/styles/rubocop/migration_helpers'
3
+
4
+ module Rubocop
5
+ module Cop
6
+ module Migration
7
+ # Checks for methods that may lead to batch type issues on a table that's been
8
+ # explicitly denied because of its size.
9
+ #
10
+ # Even though though these methods perform functions to avoid
11
+ # downtime, using it with tables with millions of rows still causes a
12
+ # significant delay in the deploy process and is best avoided.
13
+ #
14
+ # See https://gitlab.com/gitlab-com/infrastructure/issues/1602 for more
15
+ # information.
16
+ class UpdateLargeTable < RuboCop::Cop::Base
17
+ include Gitlab::Styles::Rubocop::MigrationHelpers
18
+
19
+ MSG = 'Using `%s` on the `%s` table will take a long time to ' \
20
+ 'complete, and should be avoided unless absolutely ' \
21
+ 'necessary'
22
+
23
+ # @!method batch_update?(node)
24
+ def_node_matcher :batch_update?, <<~PATTERN
25
+ (send nil? ${#denied_method?}
26
+ (sym $...)
27
+ ...)
28
+ PATTERN
29
+
30
+ def on_send(node)
31
+ return if denied_tables.empty? || denied_methods.empty?
32
+ return unless in_migration?(node)
33
+
34
+ matches = batch_update?(node)
35
+ return unless matches
36
+
37
+ update_method = matches.first
38
+ table = matches.last.to_a.first
39
+
40
+ return unless denied_tables.include?(table)
41
+
42
+ add_offense(node, message: format(MSG, update_method, table))
43
+ end
44
+
45
+ private
46
+
47
+ def denied_tables
48
+ cop_config['DeniedTables'] || []
49
+ end
50
+
51
+ def denied_method?(method_name)
52
+ denied_methods.include?(method_name)
53
+ end
54
+
55
+ def denied_methods
56
+ cop_config['DeniedMethods'] || []
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubocop
4
+ module Cop
5
+ module Performance
6
+ # Flags inefficient uses of rubyzip's Zip::File, since when instantiated
7
+ # it reads the file's Central Directory into memory entirely. For zips with many
8
+ # files and directories, this can be very expensive even when the archive's size
9
+ # in bytes is small.
10
+ #
11
+ # See also:
12
+ # - https://github.com/rubyzip/rubyzip/issues/506
13
+ # - https://github.com/rubyzip/rubyzip#notes-on-zipinputstream
14
+ class Rubyzip < RuboCop::Cop::Base
15
+ MSG = 'Be careful when opening or iterating zip files via Zip::File. ' \
16
+ 'Zip archives may contain many entries, and their file index is ' \
17
+ 'read into memory upon construction, which can lead to ' \
18
+ 'high memory use and poor performance. ' \
19
+ 'Consider iterating archive entries via Zip::InputStream instead.'
20
+
21
+ # @!method reads_central_directory?(node)
22
+ def_node_matcher :reads_central_directory?, <<-PATTERN
23
+ (send
24
+ (const
25
+ (const {nil? (cbase)} :Zip) :File) {:new :open :foreach} ...)
26
+ PATTERN
27
+
28
+ def on_send(node)
29
+ return unless reads_central_directory?(node)
30
+
31
+ add_offense(node)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../gitlab/styles/rubocop/model_helpers'
4
+
5
+ module Rubocop
6
+ module Cop
7
+ # Cop that prevents the use of polymorphic associations
8
+ class PolymorphicAssociations < RuboCop::Cop::Base
9
+ include Gitlab::Styles::Rubocop::ModelHelpers
10
+
11
+ MSG = 'Do not use polymorphic associations, use separate tables instead'
12
+
13
+ def on_send(node)
14
+ return unless in_model?(node)
15
+ return unless node.children[1] == :belongs_to
16
+
17
+ node.children.last.each_node(:pair) do |pair|
18
+ key_name = pair.children[0].children[0]
19
+
20
+ add_offense(pair) if key_name == :polymorphic
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../gitlab/styles/rubocop/model_helpers'
4
+
5
+ module Rubocop
6
+ module Cop
7
+ module Rails
8
+ class IncludeUrlHelper < RuboCop::Cop::Base
9
+ MSG = <<~MSG
10
+ Avoid including `ActionView::Helpers::UrlHelper`.
11
+ It adds/overrides ~40 methods while usually only one is needed.
12
+ Instead, use the `Gitlab::Routing.url_helpers`/`Application.routes.url_helpers`(outside of gitlab)
13
+ and `ActionController::Base.helpers.link_to`.
14
+ See https://gitlab.com/gitlab-org/gitlab/-/issues/340567.
15
+ MSG
16
+
17
+ # @!method include_url_helpers_node?(node)
18
+ def_node_matcher :include_url_helpers_node?, <<~PATTERN
19
+ (send nil? :include (const (const (const {nil? cbase} :ActionView) :Helpers) :UrlHelper))
20
+ PATTERN
21
+
22
+ def on_send(node)
23
+ add_offense(node) if include_url_helpers_node?(node)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubocop
4
+ module Cop
5
+ # Prevents usage of 'redirect_to' in actions 'destroy' without specifying 'status'.
6
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/31840
7
+ class RedirectWithStatus < RuboCop::Cop::Base
8
+ MSG = 'Do not use "redirect_to" without "status" in "destroy" action'
9
+
10
+ def on_def(node)
11
+ return unless in_controller?(node)
12
+ return unless destroy?(node) || destroy_all?(node)
13
+
14
+ node.each_descendant(:send) do |def_node|
15
+ next unless redirect_to?(def_node)
16
+
17
+ methods = []
18
+
19
+ def_node.children.last.each_node(:pair) do |pair|
20
+ methods << pair.children.first.children.first
21
+ end
22
+
23
+ add_offense(def_node.loc.selector) unless methods.include?(:status)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def in_controller?(node)
30
+ node.location.expression.source_buffer.name.end_with?('_controller.rb')
31
+ end
32
+
33
+ def destroy?(node)
34
+ node.children.first == :destroy
35
+ end
36
+
37
+ def destroy_all?(node)
38
+ node.children.first == :destroy_all
39
+ end
40
+
41
+ def redirect_to?(node)
42
+ node.children[1] == :redirect_to
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop-rspec'
4
+
5
+ module Rubocop
6
+ module Cop
7
+ module RSpec
8
+ class Base < RuboCop::Cop::RSpec::Base
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop-rspec'
4
+ require_relative 'base'
5
+
6
+ module Rubocop
7
+ module Cop
8
+ module RSpec
9
+ # Checks if there is an empty line after let blocks.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # RSpec.describe Foo do
14
+ # let(:something) { 'something' }
15
+ # let(:another_thing) do
16
+ # end
17
+ # let(:something_else) do
18
+ # end
19
+ # let(:last_thing) { 'last thing' }
20
+ # end
21
+ #
22
+ # # good
23
+ # RSpec.describe Foo do
24
+ # let(:something) { 'something' }
25
+ # let(:another_thing) do
26
+ # end
27
+ #
28
+ # let(:something_else) do
29
+ # end
30
+ #
31
+ # let(:last_thing) { 'last thing' }
32
+ # end
33
+ #
34
+ # # good - it's ok to have non-separated without do/end blocks
35
+ # RSpec.describe Foo do
36
+ # let(:something) { 'something' }
37
+ # let(:last_thing) { 'last thing' }
38
+ # end
39
+ #
40
+ class EmptyLineAfterLetBlock < Base
41
+ extend RuboCop::Cop::AutoCorrector
42
+ include RuboCop::Cop::RSpec::EmptyLineSeparation
43
+
44
+ MSG = 'Add an empty line after `%<let>s` block.'
45
+
46
+ def on_block(node)
47
+ RuboCop::RSpec::ExampleGroup.new(node).lets.each do |let|
48
+ break if last_child?(let)
49
+ next if let.single_line?
50
+
51
+ missing_separating_line_offense(let) do |method|
52
+ format(MSG, let: method)
53
+ format(MSG, let: method)
54
+ end
55
+ end
56
+ end
57
+ alias_method :on_numblock, :on_block
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop-rspec'
4
+ require_relative 'base'
5
+
6
+ module Rubocop
7
+ module Cop
8
+ module RSpec
9
+ # Checks if there is an empty line after shared example blocks.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # RSpec.describe Foo do
14
+ # it_behaves_like 'do this first'
15
+ # it_behaves_like 'does this' do
16
+ # end
17
+ # it_behaves_like 'does that' do
18
+ # end
19
+ # it_behaves_like 'do some more'
20
+ # end
21
+ #
22
+ # # good
23
+ # RSpec.describe Foo do
24
+ # it_behaves_like 'do this first'
25
+ # it_behaves_like 'does this' do
26
+ # end
27
+ #
28
+ # it_behaves_like 'does that' do
29
+ # end
30
+ #
31
+ # it_behaves_like 'do some more'
32
+ # end
33
+ #
34
+ # # fair - it's ok to have non-separated without blocks
35
+ # RSpec.describe Foo do
36
+ # it_behaves_like 'do this first'
37
+ # it_behaves_like 'does this'
38
+ # end
39
+ #
40
+ class EmptyLineAfterSharedExample < Base
41
+ extend RuboCop::Cop::AutoCorrector
42
+ include RuboCop::Cop::RSpec::EmptyLineSeparation
43
+
44
+ MSG = 'Add an empty line after `%<example>s` block.'
45
+
46
+ # @!method shared_examples(node)
47
+ def_node_matcher :shared_examples,
48
+ block_pattern('{#SharedGroups.all #Includes.all}')
49
+
50
+ def on_block(node)
51
+ shared_examples(node) do
52
+ break if last_child?(node)
53
+
54
+ missing_separating_line_offense(node) do |method|
55
+ format(MSG, example: method)
56
+ end
57
+ end
58
+ end
59
+ alias_method :on_numblock, :on_block
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop-rspec'
4
+ require_relative 'base'
5
+
6
+ module Rubocop
7
+ module Cop
8
+ module RSpec
9
+ # Checks for common mistakes in example descriptions.
10
+ #
11
+ # This cop will correct docstrings that begin/end with space or words that start with a capital letter.
12
+ #
13
+ # @see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46336#note_442669518
14
+ #
15
+ # @example
16
+ # # bad
17
+ # it 'Does something' do
18
+ # end
19
+ #
20
+ # # good
21
+ # it 'does nothing' do
22
+ # end
23
+ #
24
+ # @example
25
+ # # bad
26
+ # it ' does something' do
27
+ # end
28
+ #
29
+ # # good
30
+ # it 'does something' do
31
+ # end
32
+ #
33
+ # @example
34
+ # # bad
35
+ # it 'does something ' do
36
+ # end
37
+ #
38
+ # # good
39
+ # it 'does something' do
40
+ # end
41
+ #
42
+ # @example
43
+ # # bad
44
+ # it ' does something ' do
45
+ # end
46
+ #
47
+ # # good
48
+ # it 'does something' do
49
+ # end
50
+ class ExampleStartingCharacter < Base
51
+ extend RuboCop::Cop::AutoCorrector
52
+
53
+ MSG = 'Only start words with lowercase alpha with no leading/trailing spaces when describing your tests.'
54
+
55
+ # @!method it_description(node)
56
+ def_node_matcher :it_description, <<-PATTERN
57
+ (block (send _ :it ${
58
+ (str $_)
59
+ (dstr (str $_ ) ...)
60
+ } ...) ...)
61
+ PATTERN
62
+
63
+ def on_block(node)
64
+ it_description(node) do |description_node, _message|
65
+ add_wording_offense(description_node, MSG) if invalid_description?(text(description_node))
66
+ end
67
+ end
68
+ alias_method :on_numblock, :on_block
69
+
70
+ private
71
+
72
+ def add_wording_offense(node, message)
73
+ docstring = docstring(node)
74
+ add_offense(docstring, message: message) do |corrector|
75
+ corrector.replace(docstring, replacement_text(node))
76
+ end
77
+ end
78
+
79
+ def docstring(node)
80
+ expr = node.loc.expression
81
+
82
+ Parser::Source::Range.new(
83
+ expr.source_buffer,
84
+ expr.begin_pos + 1,
85
+ expr.end_pos - 1
86
+ )
87
+ end
88
+
89
+ def invalid_description?(message)
90
+ message.match?(/(^([A-Z]{1}[a-z]+\s|\s)|\s$)/)
91
+ end
92
+
93
+ def replacement_text(node)
94
+ text = text(node)
95
+
96
+ text.strip!
97
+
98
+ text = downcase_first_letter(text) if invalid_description?(text)
99
+
100
+ text
101
+ end
102
+
103
+ # Recursive processing is required to process nested dstr nodes
104
+ # that is the case for \-separated multiline strings with interpolation.
105
+ def text(node)
106
+ case node.type
107
+ when :dstr
108
+ node.node_parts.map { |child_node| text(child_node) }.join
109
+ when :str
110
+ node.value
111
+ when :begin
112
+ node.source
113
+ end
114
+ end
115
+
116
+ def downcase_first_letter(str)
117
+ str[0].downcase + str[1..]
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end