rubocop-exception_messages 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc8af98d6bdcef4ebe89488a40c7bbb813ca3a9cbc6c6c87e7ad9fa5a09a1127
4
- data.tar.gz: e6f89e17ee748d3f978f4bf332a1ec7d3d9d6f1731ce6d5dbdf2fdb6bb39c852
3
+ metadata.gz: 30c6a33ca2d34e81294dc56ebfbc93487038a551befad0f019569e9622606635
4
+ data.tar.gz: 2302c462589b5aa9981c6c75bdb0cd25b3a76ed4e4c1e97e34a9bc6db9b051e9
5
5
  SHA512:
6
- metadata.gz: debf11ec95b5b9533c58d471b44d142747437fe951c797fa5734723d6eab32ae945e511b24a179590d822155e859f85d983a85365af8a2b6cb3b858353df4b7b
7
- data.tar.gz: f8cf2d88a4542efc375d5e9074f0efcee550266a2f69d18ada7dfccadddeb66850b6e7be8df8056d166f0a25c6f49f89bb539e13bb4b9c5984266b2124935ba0
6
+ metadata.gz: 656845e5a38eeff400ab2ad31c1ae62537ec57d577978a648f50c192f14a29ef58564bf8630701e83cda3f44738d502c4dd99f916b942e6c55e467fa19ae8fec
7
+ data.tar.gz: d1dcbe5ed3a6c9ac92548f3ed2892ace8797cb5354b1477635a009ef93f04de6b1b9161710b64750993495576a6a4dbba01a950f76a18272b3841b8d32c1768f
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
- ### 0.1.0 (2026/09/05)
1
+ ### 0.2.0 (2026/09/05)
2
+
3
+ * [#6](https://github.com/dblock/rubocop-exception_messages/pull/6): Add `EnforcedStyle` support to `ExceptionMessages/Casing` (`lowercase`/`uppercase`) and `ExceptionMessages/Punctuation` (`no_period`/`period`) to allow the opposite convention - [@dblock](https://github.com/dblock).
4
+ * [#7](https://github.com/dblock/rubocop-exception_messages/pull/7): Add `ExceptionMessages/RedundantExceptionName` cop that flags exception messages redundantly repeating the exception class name - [@dblock](https://github.com/dblock).
5
+ * [#8](https://github.com/dblock/rubocop-exception_messages/pull/8): Add `ExceptionMessages/QuoteStyle` cop that checks consistent quoting of interpolated values in exception messages - [@dblock](https://github.com/dblock).
6
+ * [#16](https://github.com/dblock/rubocop-exception_messages/pull/16): Fix `ExceptionMessages/Punctuation` not detecting a trailing period in heredoc messages, and an infinite autocorrection loop when it did; fix `ExceptionMessages/QuoteStyle` raising an error when autocorrecting a heredoc message - [@dblock](https://github.com/dblock).
2
7
 
3
- #### Features
8
+ ### 0.1.0 (2026/09/05)
4
9
 
5
10
  * Initial release with `ExceptionMessages/Casing` and `ExceptionMessages/Punctuation` cops - [@dblock](https://github.com/dblock).
6
11
  * Add Danger CI check that enforces a CHANGELOG entry on every pull request - [@dblock](https://github.com/dblock).
data/README.md CHANGED
@@ -30,34 +30,125 @@ plugins:
30
30
 
31
31
  ### ExceptionMessages/Casing
32
32
 
33
- Checks that raised exception messages start with a lowercase letter.
33
+ Checks the capitalization of raised exception messages. Defaults to `EnforcedStyle: lowercase`.
34
34
 
35
35
  ```ruby
36
36
  # bad
37
37
  raise ArgumentError, "Block is required"
38
- raise ArgumentError.new("Block is required")
39
38
 
40
39
  # good
41
40
  raise ArgumentError, "block is required"
42
- raise ArgumentError.new("block is required")
41
+ ```
42
+
43
+ Configure `EnforcedStyle: uppercase` to require the opposite convention instead.
44
+
45
+ ```yaml
46
+ ExceptionMessages/Casing:
47
+ EnforcedStyle: uppercase
48
+ ```
49
+
50
+ ```ruby
51
+ # bad
52
+ raise ArgumentError, "block is required"
53
+
54
+ # good
55
+ raise ArgumentError, "Block is required"
43
56
  ```
44
57
 
45
58
  ### ExceptionMessages/Punctuation
46
59
 
47
- Checks that raised exception messages do not end with a period.
60
+ Checks the trailing punctuation of raised exception messages. Defaults to `EnforcedStyle: no_period`. A literal ellipsis (`"..."`) is never considered an offense.
48
61
 
49
62
  ```ruby
50
63
  # bad
51
64
  raise ArgumentError, "block is required."
52
- raise ArgumentError.new("block is required.")
53
65
 
54
66
  # good
55
67
  raise ArgumentError, "block is required"
56
- raise ArgumentError.new("block is required")
68
+ ```
69
+
70
+ Configure `EnforcedStyle: period` to require a trailing period instead.
71
+
72
+ ```yaml
73
+ ExceptionMessages/Punctuation:
74
+ EnforcedStyle: period
75
+ ```
76
+
77
+ ```ruby
78
+ # bad
79
+ raise ArgumentError, "block is required"
80
+
81
+ # good
82
+ raise ArgumentError, "block is required."
57
83
  ```
58
84
 
59
85
  Both cops support autocorrection (`rubocop -A`).
60
86
 
87
+ ### ExceptionMessages/RedundantExceptionName
88
+
89
+ Checks that raised exception messages do not redundantly repeat the exception class name, since Ruby already prints the class name ahead of the message in a backtrace.
90
+
91
+ ```ruby
92
+ # bad
93
+ raise ArgumentError, "ArgumentError: block is required"
94
+
95
+ # good
96
+ raise ArgumentError, "block is required"
97
+ ```
98
+
99
+ ### ExceptionMessages/QuoteStyle
100
+
101
+ Checks that interpolated values in raised exception messages are consistently quoted, making it easier to spot where a dynamic value begins and ends in a rendered message. Defaults to `EnforcedStyle: backticks`.
102
+
103
+ ```ruby
104
+ # bad
105
+ raise ArgumentError, "unknown type: #{type}"
106
+
107
+ # good
108
+ raise ArgumentError, "unknown type: `#{type}`"
109
+ ```
110
+
111
+ Configure `EnforcedStyle: single_quotes`, `double_quotes`, `square_brackets`, `parentheses`, or `curly_braces` to require a different wrapping instead.
112
+
113
+ ```yaml
114
+ ExceptionMessages/QuoteStyle:
115
+ Enabled: true
116
+ EnforcedStyle: single_quotes
117
+ ```
118
+
119
+ ```ruby
120
+ # good
121
+ raise ArgumentError, "unknown type: '#{type}'"
122
+ ```
123
+
124
+ Configure `EnforcedStyle: custom` with `Prefix`/`Suffix` for anything else, including a single-sided marker with no closing character.
125
+
126
+ ```yaml
127
+ ExceptionMessages/QuoteStyle:
128
+ Enabled: true
129
+ EnforcedStyle: custom
130
+ Prefix: '?'
131
+ ```
132
+
133
+ ```ruby
134
+ # good
135
+ raise ArgumentError, "unknown type: ?#{type}"
136
+ ```
137
+
138
+ `Prefix`/`Suffix` aren't limited to a single character.
139
+
140
+ ```yaml
141
+ ExceptionMessages/QuoteStyle:
142
+ Enabled: true
143
+ EnforcedStyle: custom
144
+ Prefix: '--'
145
+ ```
146
+
147
+ ```ruby
148
+ # good
149
+ raise ArgumentError, "unknown type: --#{type}"
150
+ ```
151
+
61
152
  ## Contributing
62
153
 
63
154
  See [CONTRIBUTING.md](CONTRIBUTING.md).
data/RELEASING.md CHANGED
@@ -58,12 +58,6 @@ Add the next release to [CHANGELOG.md](CHANGELOG.md).
58
58
  ```
59
59
  ### 0.1.1 (Next)
60
60
 
61
- #### Features
62
-
63
- * Your contribution here.
64
-
65
- #### Fixes
66
-
67
61
  * Your contribution here.
68
62
  ```
69
63
 
data/config/default.yml CHANGED
@@ -2,11 +2,42 @@ ExceptionMessages:
2
2
  Enabled: true
3
3
 
4
4
  ExceptionMessages/Casing:
5
- Description: 'Checks that raised exception messages start with a lowercase letter.'
5
+ Description: 'Checks the capitalization of raised exception messages.'
6
6
  Enabled: true
7
+ EnforcedStyle: lowercase
8
+ SupportedStyles:
9
+ - lowercase
10
+ - uppercase
7
11
  VersionAdded: '0.1'
12
+ VersionChanged: '0.2'
8
13
 
9
14
  ExceptionMessages/Punctuation:
10
- Description: 'Checks that raised exception messages do not end with a period.'
15
+ Description: 'Checks the trailing punctuation of raised exception messages.'
11
16
  Enabled: true
17
+ EnforcedStyle: no_period
18
+ SupportedStyles:
19
+ - no_period
20
+ - period
12
21
  VersionAdded: '0.1'
22
+ VersionChanged: '0.2'
23
+
24
+ ExceptionMessages/RedundantExceptionName:
25
+ Description: 'Checks that raised exception messages do not repeat the exception class name.'
26
+ Enabled: true
27
+ VersionAdded: '0.2'
28
+
29
+ ExceptionMessages/QuoteStyle:
30
+ Description: 'Checks that interpolated values in raised exception messages are consistently quoted.'
31
+ Enabled: true
32
+ EnforcedStyle: backticks
33
+ SupportedStyles:
34
+ - backticks
35
+ - single_quotes
36
+ - double_quotes
37
+ - square_brackets
38
+ - parentheses
39
+ - curly_braces
40
+ - custom
41
+ Prefix: ~
42
+ Suffix: ~
43
+ VersionAdded: '0.2'
@@ -3,23 +3,31 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module ExceptionMessages
6
- # Checks that raised exception messages start with a lowercase letter,
7
- # consistent with Ruby's own core and standard library exceptions
8
- # (e.g. `TypeError: no implicit conversion from nil to integer`).
6
+ # Checks the capitalization of raised exception messages. By default,
7
+ # requires a lowercase first letter, consistent with Ruby's own core
8
+ # and standard library exceptions (e.g. `TypeError: no implicit
9
+ # conversion from nil to integer`). Configure `EnforcedStyle: uppercase`
10
+ # to require the opposite convention instead.
9
11
  #
10
- # @example
12
+ # @example EnforcedStyle: lowercase (default)
11
13
  # # bad
12
14
  # raise ArgumentError, "Block is required"
13
- # raise ArgumentError.new("Block is required")
14
15
  #
15
16
  # # good
16
17
  # raise ArgumentError, "block is required"
17
- # raise ArgumentError.new("block is required")
18
+ #
19
+ # @example EnforcedStyle: uppercase
20
+ # # bad
21
+ # raise ArgumentError, "block is required"
22
+ #
23
+ # # good
24
+ # raise ArgumentError, "Block is required"
18
25
  class Casing < Base
26
+ include ConfigurableEnforcedStyle
19
27
  include MessageNode
20
28
  extend AutoCorrector
21
29
 
22
- MSG = 'Exception messages should start with a lowercase letter.'
30
+ MSG = 'Exception messages should start with a %<style>s letter.'
23
31
 
24
32
  def on_send(node)
25
33
  message_node = raise_message_node(node)
@@ -29,16 +37,29 @@ module RuboCop
29
37
  return unless first_segment
30
38
 
31
39
  content = first_segment.value
32
- return if content.empty? || content[0] !~ /[A-Z]/
40
+ return if content.empty? || !offense?(content)
33
41
 
34
- add_offense(message_node) do |corrector|
35
- corrected = content.sub(/\A./, &:downcase)
36
- corrector.replace(first_segment.loc.expression, first_segment.source.sub(content, corrected))
37
- end
42
+ register_offense(message_node, first_segment, content)
38
43
  end
39
44
 
40
45
  private
41
46
 
47
+ def register_offense(message_node, segment, content)
48
+ add_offense(message_node, message: format(MSG, style: style)) do |corrector|
49
+ corrected = content.sub(/\A./, &correction_method)
50
+ corrector.replace(segment.loc.expression, segment.source.sub(content, corrected))
51
+ end
52
+ end
53
+
54
+ def offense?(content)
55
+ first_char = content[0]
56
+ first_char =~ (style == :lowercase ? /[A-Z]/ : /[a-z]/)
57
+ end
58
+
59
+ def correction_method
60
+ style == :lowercase ? :downcase : :upcase
61
+ end
62
+
42
63
  def first_string_segment(message_node)
43
64
  segment = message_node.str_type? ? message_node : message_node.child_nodes.first
44
65
  segment if segment&.str_type?
@@ -3,23 +3,34 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module ExceptionMessages
6
- # Checks that raised exception messages do not end with a period,
7
- # consistent with Ruby's own core and standard library exceptions
8
- # (e.g. `ArgumentError: wrong number of arguments`).
6
+ # Checks the trailing punctuation of raised exception messages. By
7
+ # default, disallows a trailing period, consistent with Ruby's own
8
+ # core and standard library exceptions (e.g. `ArgumentError: wrong
9
+ # number of arguments`). Configure `EnforcedStyle: period` to require
10
+ # the opposite convention instead. A literal ellipsis (`"..."`) is
11
+ # never considered an offense, since it's stylistic rather than a
12
+ # sentence-ending period.
9
13
  #
10
- # @example
14
+ # @example EnforcedStyle: no_period (default)
11
15
  # # bad
12
16
  # raise ArgumentError, "block is required."
13
- # raise ArgumentError.new("block is required.")
14
17
  #
15
18
  # # good
16
19
  # raise ArgumentError, "block is required"
17
- # raise ArgumentError.new("block is required")
20
+ #
21
+ # @example EnforcedStyle: period
22
+ # # bad
23
+ # raise ArgumentError, "block is required"
24
+ #
25
+ # # good
26
+ # raise ArgumentError, "block is required."
18
27
  class Punctuation < Base
28
+ include ConfigurableEnforcedStyle
19
29
  include MessageNode
20
30
  extend AutoCorrector
21
31
 
22
- MSG = 'Exception messages should not end with a period.'
32
+ MSG_NO_PERIOD = 'Exception messages should not end with a period.'
33
+ MSG_PERIOD = 'Exception messages should end with a period.'
23
34
 
24
35
  def on_send(node)
25
36
  message_node = raise_message_node(node)
@@ -34,16 +45,39 @@ module RuboCop
34
45
  private
35
46
 
36
47
  def check_segment(message_node, segment)
37
- content = segment.value
38
- return unless content.end_with?('.')
48
+ content = segment.value.chomp("\n")
39
49
  return if content.end_with?('..') # ellipsis-style, not a sentence-ending period
40
50
 
41
- add_offense(message_node) do |corrector|
51
+ if style == :no_period
52
+ check_no_period(message_node, segment, content)
53
+ else
54
+ check_period(message_node, segment, content)
55
+ end
56
+ end
57
+
58
+ def check_no_period(message_node, segment, content)
59
+ return unless content.end_with?('.')
60
+
61
+ add_offense(message_node, message: MSG_NO_PERIOD) do |corrector|
42
62
  corrected = content.sub(/\.\z/, '')
43
- corrector.replace(segment.loc.expression, segment.source.sub(content, corrected))
63
+ replace_content(corrector, segment, content, corrected)
44
64
  end
45
65
  end
46
66
 
67
+ def check_period(message_node, segment, content)
68
+ return if content.empty? || content.end_with?('.')
69
+
70
+ add_offense(message_node, message: MSG_PERIOD) do |corrector|
71
+ corrected = "#{content}."
72
+ replace_content(corrector, segment, content, corrected)
73
+ end
74
+ end
75
+
76
+ def replace_content(corrector, segment, content, corrected)
77
+ range = segment.loc.respond_to?(:heredoc_body) ? segment.loc.heredoc_body : segment.loc.expression
78
+ corrector.replace(range, range.source.sub(content, corrected))
79
+ end
80
+
47
81
  def last_string_segment(message_node)
48
82
  segment = message_node.str_type? ? message_node : message_node.child_nodes.last
49
83
  segment if segment&.str_type?
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ExceptionMessages
6
+ # Checks that interpolated values in raised exception messages are
7
+ # consistently quoted, making it easier to spot where a dynamic value
8
+ # begins and ends in a rendered message.
9
+ #
10
+ # @example EnforcedStyle: backticks (default)
11
+ # # bad
12
+ # raise ArgumentError, "unknown type: #{type}"
13
+ #
14
+ # # good
15
+ # raise ArgumentError, "unknown type: `#{type}`"
16
+ #
17
+ # @example EnforcedStyle: custom (with Prefix: '?', Suffix: '')
18
+ # # bad
19
+ # raise ArgumentError, "unknown type: #{type}"
20
+ #
21
+ # # good
22
+ # raise ArgumentError, "unknown type: ?#{type}"
23
+ class QuoteStyle < Base
24
+ include ConfigurableEnforcedStyle
25
+ include MessageNode
26
+ extend AutoCorrector
27
+
28
+ MSG = 'Interpolated values in exception messages should be wrapped in %<style>s.'
29
+
30
+ QUOTES = {
31
+ backticks: ['`', '`'],
32
+ single_quotes: ["'", "'"],
33
+ double_quotes: ['"', '"'],
34
+ square_brackets: ['[', ']'],
35
+ parentheses: ['(', ')'],
36
+ curly_braces: ['{', '}']
37
+ }.freeze
38
+
39
+ def on_send(node)
40
+ message_node = raise_message_node(node)
41
+ return unless message_node&.dstr_type?
42
+
43
+ message_node.child_nodes.each do |segment|
44
+ next unless segment.begin_type?
45
+
46
+ check_interpolation(message_node, segment)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def check_interpolation(message_node, segment)
53
+ open_quote, close_quote = quotes_for_style
54
+ return if quoted?(segment, open_quote, close_quote)
55
+
56
+ add_offense(segment, message: format(MSG, style: style_description)) do |corrector|
57
+ open_quote, close_quote = escape_for_literal(message_node, open_quote, close_quote)
58
+ corrector.insert_before(segment.loc.expression, open_quote)
59
+ corrector.insert_after(segment.loc.expression, close_quote) unless close_quote.empty?
60
+ end
61
+ end
62
+
63
+ def quotes_for_style
64
+ return QUOTES.fetch(style) unless style == :custom
65
+
66
+ [cop_config['Prefix'] || '', cop_config['Suffix'] || '']
67
+ end
68
+
69
+ def style_description
70
+ return "`#{cop_config['Prefix']}` / `#{cop_config['Suffix']}`" if style == :custom
71
+
72
+ style.to_s.tr('_', ' ')
73
+ end
74
+
75
+ def escape_for_literal(message_node, open_quote, close_quote)
76
+ return [open_quote, close_quote] unless message_node.loc.respond_to?(:begin) && message_node.loc.begin
77
+
78
+ outer_quote = message_node.loc.begin.source
79
+ return [open_quote, close_quote] unless outer_quote == open_quote || outer_quote == close_quote
80
+
81
+ [open_quote == outer_quote ? "\\#{open_quote}" : open_quote,
82
+ close_quote == outer_quote ? "\\#{close_quote}" : close_quote]
83
+ end
84
+
85
+ def quoted?(segment, open_quote, close_quote)
86
+ preceding = segment.left_sibling
87
+ following = segment.right_sibling
88
+
89
+ preceding_ok = open_quote.empty? ||
90
+ (preceding.is_a?(RuboCop::AST::StrNode) && preceding.value.end_with?(open_quote))
91
+ following_ok = close_quote.empty? ||
92
+ (following.is_a?(RuboCop::AST::StrNode) && following.value.start_with?(close_quote))
93
+
94
+ preceding_ok && following_ok
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module ExceptionMessages
6
+ # Checks that raised exception messages do not redundantly repeat the
7
+ # exception class name, since Ruby already prints the class name
8
+ # ahead of the message in a backtrace (`ArgumentError: block is
9
+ # required`, not `ArgumentError: ArgumentError: block is required`).
10
+ #
11
+ # @example
12
+ # # bad
13
+ # raise ArgumentError, "ArgumentError: block is required"
14
+ #
15
+ # # good
16
+ # raise ArgumentError, "block is required"
17
+ class RedundantExceptionName < Base
18
+ include MessageNode
19
+
20
+ MSG = 'Exception messages should not repeat the exception class name.'
21
+
22
+ def on_send(node)
23
+ message_node = raise_message_node(node)
24
+ return unless message_node
25
+
26
+ first_segment = first_string_segment(message_node)
27
+ return unless first_segment
28
+
29
+ exception_name = exception_class_name(node)
30
+ return unless exception_name
31
+
32
+ return unless redundant?(first_segment.value, exception_name)
33
+
34
+ add_offense(message_node)
35
+ end
36
+
37
+ private
38
+
39
+ def redundant?(content, exception_name)
40
+ content.match?(/\A#{Regexp.escape(exception_name)}\b/)
41
+ end
42
+
43
+ def exception_class_name(node)
44
+ klass = raised_class_node(node)
45
+ return unless klass&.const_type?
46
+
47
+ klass.const_name&.split('::')&.last
48
+ end
49
+
50
+ def raised_class_node(node)
51
+ first_arg = node.arguments.first
52
+ return unless first_arg
53
+ return first_arg.receiver if first_arg.send_type? && first_arg.method?(:new)
54
+
55
+ first_arg
56
+ end
57
+
58
+ def first_string_segment(message_node)
59
+ segment = message_node.str_type? ? message_node : message_node.child_nodes.first
60
+ segment if segment&.str_type?
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -3,3 +3,5 @@
3
3
  require_relative 'exception_messages/message_node'
4
4
  require_relative 'exception_messages/casing'
5
5
  require_relative 'exception_messages/punctuation'
6
+ require_relative 'exception_messages/redundant_exception_name'
7
+ require_relative 'exception_messages/quote_style'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ExceptionMessages
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-exception_messages
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel (dB.) Doubrovkine
@@ -59,6 +59,8 @@ files:
59
59
  - lib/rubocop/cop/exception_messages/casing.rb
60
60
  - lib/rubocop/cop/exception_messages/message_node.rb
61
61
  - lib/rubocop/cop/exception_messages/punctuation.rb
62
+ - lib/rubocop/cop/exception_messages/quote_style.rb
63
+ - lib/rubocop/cop/exception_messages/redundant_exception_name.rb
62
64
  - lib/rubocop/cop/exception_messages_cops.rb
63
65
  - lib/rubocop/exception_messages.rb
64
66
  - lib/rubocop/exception_messages/plugin.rb