gitlab-styles 7.0.0 → 9.0.0

Sign up to get free protection for your applications and to get access to all the features.
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