kaizo 0.7.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.
@@ -0,0 +1,210 @@
1
+ # Default configuration for kaizo.
2
+ #
3
+ # These cops apply pressure toward good domain modeling and away from primitive
4
+ # obsession: the `Kaizo/*` cops bound how many arguments a method declares, and
5
+ # the `Kaizo/*` cops flag class names that describe an action rather than the
6
+ # concept they model.
7
+
8
+ # The `Kaizo/*` cops check `def`, `def self.`, `define_method`, and
9
+ # `define_singleton_method`. `*rest`, `**keyword-rest`, and `&block` parameters
10
+ # are not counted. The defaults are deliberately strict -- at most one positional
11
+ # and one keyword argument -- to apply maximum pressure; loosen them if that is
12
+ # too aggressive for your codebase.
13
+ Kaizo/KeywordArguments:
14
+ Description: 'Checks that a method does not declare too many keyword arguments.'
15
+ Enabled: true
16
+ VersionAdded: '0.1'
17
+ Max: 1
18
+
19
+ Kaizo/PositionalArguments:
20
+ Description: 'Checks that a method does not declare too many positional arguments.'
21
+ Enabled: true
22
+ VersionAdded: '0.1'
23
+ Max: 1
24
+
25
+ Kaizo/TotalArguments:
26
+ Description: 'Checks that a method does not declare too many arguments in total.'
27
+ Enabled: true
28
+ VersionAdded: '0.1'
29
+ Max: 2
30
+
31
+ # `Kaizo/AgentNounClassName` flags `class` definitions and
32
+ # `Struct.new`/`Data.define`/`Class.new` assignments whose name ends in `er`/`or`
33
+ # (or in a `ForbiddenSuffixes` entry), unless it ends in an `AllowedSuffixes`
34
+ # entry. `ForbiddenSuffixes` wins over `AllowedSuffixes`. Extend either list
35
+ # without restating it using RuboCop's `inherit_mode: merge`.
36
+ Kaizo/AgentNounClassName:
37
+ Description: 'Avoid naming classes as agent nouns (doers) rather than domain concepts.'
38
+ Enabled: true
39
+ VersionAdded: '0.2'
40
+ AllowedSuffixes:
41
+ - Adapter
42
+ - Anchor
43
+ - Author
44
+ - Banner
45
+ - Border
46
+ - Builder
47
+ - Character
48
+ - Cluster
49
+ - Color
50
+ - Container
51
+ - Controller
52
+ - Corner
53
+ - Cursor
54
+ - Customer
55
+ - Decorator
56
+ - Door
57
+ - Driver
58
+ - Enumerator
59
+ - Error
60
+ - Folder
61
+ - Footer
62
+ - Header
63
+ - Identifier
64
+ - Iterator
65
+ - Layer
66
+ - Ledger
67
+ - Letter
68
+ - Logger
69
+ - Mailer
70
+ - Marker
71
+ - Member
72
+ - Mirror
73
+ - Monitor
74
+ - Number
75
+ - Order
76
+ - Owner
77
+ - Paper
78
+ - Parameter
79
+ - Parser
80
+ - Partner
81
+ - Pointer
82
+ - Poster
83
+ - Presenter
84
+ - Quarter
85
+ - Register
86
+ - Router
87
+ - Sector
88
+ - Sensor
89
+ - Serializer
90
+ - Server
91
+ - User
92
+ - Vector
93
+ - Vendor
94
+ - Visitor
95
+ - Wrapper
96
+ ForbiddenSuffixes:
97
+ - Service
98
+ - Util
99
+ - Utils
100
+
101
+ # `Kaizo/NestedMethodCalls` flags calls whose arguments are themselves calls,
102
+ # nested deeper than `Max` levels -- e.g. `foo(SomeClass.new(another("bar").chain))`.
103
+ # Only argument nesting is counted (receiver chains are a separate concern);
104
+ # operator methods never count; `AllowedMethods` exempts named methods.
105
+ Kaizo/NestedMethodCalls:
106
+ Description: 'Checks for method calls nested too deeply in argument positions.'
107
+ Enabled: true
108
+ VersionAdded: '0.4'
109
+ Max: 1
110
+ AllowedMethods: []
111
+
112
+ # `Kaizo/SpecComment` treats comments in spec files as a smell -- a comment
113
+ # usually wants to be a `context`/`it` description or a better-structured
114
+ # example. Magic comments, `# rubocop:` directives, and shebangs are exempt;
115
+ # add more exemptions with `AllowedPatterns`. Broaden `Include` (e.g. add
116
+ # `**/spec/**/*` or `**/*_test.rb`) to cover support files or Minitest.
117
+ Kaizo/SpecComment:
118
+ Description: 'Avoid comments in spec files; express the intent as well-structured specs.'
119
+ Enabled: true
120
+ VersionAdded: '0.5'
121
+ Include:
122
+ - '**/*_spec.rb'
123
+ AllowedPatterns: []
124
+
125
+ # `Kaizo/ExplicitBegin` requires an explicit `begin`/`end` block when a method
126
+ # body attaches a `rescue` or `ensure` directly to the `def` (an "implicit
127
+ # begin"). Modifier `rescue` (`foo rescue nil`) and endless method definitions
128
+ # are never flagged. Autocorrection wraps the body in `begin`/`end`, but is
129
+ # skipped when the body does not sit on its own lines between `def` and `end`,
130
+ # or holds a heredoc or other multiline string, symbol, or regexp literal, where
131
+ # re-indenting could change their contents.
132
+ Kaizo/ExplicitBegin:
133
+ Description: 'Require an explicit `begin` block when a method body uses `rescue` or `ensure`.'
134
+ Enabled: true
135
+ VersionAdded: '0.6'
136
+
137
+ # `Kaizo/NextInNonVoidEnumerable` flags `next` inside the block of a
138
+ # value-returning `Enumerable` method (`map`, `select`, `reduce`, ...), where
139
+ # `next` is control-flow-as-value. Void iteration methods (`each`,
140
+ # `each_with_object`, ...) and non-`Enumerable` loops (`loop`, `while`) are never
141
+ # flagged; a `next` bound to a nested block or loop is attributed to that inner
142
+ # scope. `AllowedMethods`/`AllowedPatterns` exempt named methods. No autocorrection.
143
+ Kaizo/NextInNonVoidEnumerable:
144
+ Description: 'Avoid `next` in the block of a value-returning `Enumerable` method.'
145
+ Enabled: true
146
+ VersionAdded: '0.6'
147
+ AllowedMethods: []
148
+ AllowedPatterns: []
149
+
150
+ # `Style/RedundantBegin` enforces the exact opposite of `Kaizo/ExplicitBegin`
151
+ # (and its autocorrection would fight ours -- the two would loop forever), so
152
+ # loading this plugin disables it. Re-enable it explicitly in your own
153
+ # `.rubocop.yml` at your peril.
154
+ Style/RedundantBegin:
155
+ Enabled: false
156
+ # `Kaizo/SpecDescriptionProse` requires RSpec `it`/`context` descriptions to
157
+ # read as one-behavior prose: no commas, conjunctions (`Conjunctions`), or code
158
+ # in an `it` description; `context` descriptions carry no code and open with a
159
+ # `ContextPrefixes` word. `describe` strings are exempt. No autocorrection.
160
+ Kaizo/SpecDescriptionProse:
161
+ Description: 'Require RSpec it/context descriptions to read as one-behavior prose.'
162
+ Enabled: true
163
+ VersionAdded: '0.6'
164
+ Include:
165
+ - '**/*_spec.rb'
166
+ Conjunctions:
167
+ - and
168
+ - but
169
+ - or
170
+ - nor
171
+ - so
172
+ - yet
173
+ - when
174
+ - whenever
175
+ - if
176
+ - unless
177
+ - while
178
+ - until
179
+ - because
180
+ - although
181
+ - though
182
+ ContextPrefixes:
183
+ - when
184
+ - with
185
+ - without
186
+ - after
187
+
188
+ # `Kaizo/FileUtilsInclusion` requires `FileUtils` to be mixed in with `include`
189
+ # or `extend` once it is used more than once in a class or module, instead of
190
+ # repeating the `FileUtils.` receiver. The namespace is reported once. A single
191
+ # qualified call, and a namespace that already mixes `FileUtils` in, are left
192
+ # alone; nested classes/modules are counted on their own. No autocorrection.
193
+ Kaizo/FileUtilsInclusion:
194
+ Description: 'Include or extend FileUtils instead of repeatedly qualifying its methods.'
195
+ Enabled: true
196
+ VersionAdded: '0.6'
197
+
198
+ # `Kaizo/PreferPathname` flags calls to `File` class methods that have a
199
+ # `Pathname` instance-method equivalent (`File.read`, `File.exist?`, `File.join`,
200
+ # ...), preferring `Pathname`. Runs on `**/*.rb`; `exe/**/*` and `bin/**/*` are
201
+ # exempt by default (override with the standard Include/Exclude). No autocorrection.
202
+ Kaizo/PreferPathname:
203
+ Description: 'Prefer Pathname over File for methods Pathname provides.'
204
+ Enabled: true
205
+ VersionAdded: '0.6'
206
+ Include:
207
+ - '**/*.rb'
208
+ Exclude:
209
+ - 'exe/**/*'
210
+ - 'bin/**/*'
@@ -0,0 +1,27 @@
1
+ require "lint_roller"
2
+
3
+ module Kaizo
4
+ # A plugin that integrates kaizo with RuboCop's plugin system.
5
+ class Plugin < LintRoller::Plugin
6
+ def about
7
+ LintRoller::About.new(
8
+ name: "kaizo",
9
+ version: VERSION,
10
+ homepage: "https://github.com/flipmine/kaizo",
11
+ description: "A strict, punishing set of design cops for AI-agent-authored Ruby.",
12
+ )
13
+ end
14
+
15
+ def supported?(context)
16
+ context.engine == :rubocop
17
+ end
18
+
19
+ def rules(_context)
20
+ LintRoller::Rules.new(
21
+ type: :path,
22
+ config_format: :rubocop,
23
+ value: Pathname.new(__dir__).join("../../config/default.yml"),
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module Kaizo
2
+ VERSION = "0.7.0".freeze
3
+ end
data/lib/kaizo.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "rubocop"
2
+
3
+ require_relative "kaizo/version"
4
+ require_relative "kaizo/plugin"
5
+
6
+ require_relative "rubocop/cop/kaizo_cops"
7
+
8
+ # Kaizo ships a strict, punishing set of RuboCop cops aimed at AI-agent-authored
9
+ # code: they bound argument counts, flag agent-noun class names, flag calls
10
+ # nested too deeply in other calls' arguments, and treat comments and loose
11
+ # descriptions in specs as structure that wants to be expressed properly.
12
+ module Kaizo
13
+ # Base error class for kaizo.
14
+ class Error < StandardError; end
15
+ end
@@ -0,0 +1,76 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Kaizo
4
+ # Checks for classes named as agent nouns ("doers") rather than the domain
5
+ # concepts they model. A class whose name ends in `er` or `or` (`Manager`,
6
+ # `Processor`, `Handler`) -- or in any configured `ForbiddenSuffixes` such
7
+ # as `Service` -- usually signals procedural behavior that wants a clearer
8
+ # domain name or a different home.
9
+ #
10
+ # Names ending in an `AllowedSuffixes` entry are exempt; the match is by
11
+ # suffix, so `Controller` clears both `Controller` and `UsersController`.
12
+ # `ForbiddenSuffixes` always flags, even when also matched by
13
+ # `AllowedSuffixes` -- which is how you drop a default exemption.
14
+ #
15
+ # `class` definitions and `Struct.new`/`Data.define`/`Class.new` constant
16
+ # assignments are both checked. There is no autocorrection: renaming a
17
+ # class is a design decision.
18
+ #
19
+ # @example
20
+ # # bad
21
+ # class PaymentProcessor
22
+ # end
23
+ #
24
+ # # good
25
+ # class Payment
26
+ # end
27
+ #
28
+ # @example
29
+ # # good - ends in an allowed suffix
30
+ # class UsersController
31
+ # end
32
+ #
33
+ class AgentNounClassName < Base
34
+ MSG = "Avoid the doer-style class name `%<name>s`. " \
35
+ "Prefer a name for the concept it models over the action it performs.".freeze
36
+ AGENT_NOUN = /(?:er|or)\z/i
37
+
38
+ # @!method class_builder_assignment(node)
39
+ def_node_matcher :class_builder_assignment, <<~PATTERN
40
+ (casgn _ $_ {
41
+ (send (const _ {:Struct :Data :Class}) {:new :define} ...)
42
+ (block (send (const _ {:Struct :Data :Class}) {:new :define} ...) ...)
43
+ })
44
+ PATTERN
45
+
46
+ def on_class(node)
47
+ check_name(node.identifier.short_name.to_s, node.loc.name)
48
+ end
49
+
50
+ def on_casgn(node)
51
+ return unless (name = class_builder_assignment(node))
52
+
53
+ check_name(name.to_s, node.loc.name)
54
+ end
55
+
56
+ private
57
+
58
+ def check_name(name, location)
59
+ return unless offending?(name)
60
+
61
+ add_offense(location, message: format(MSG, name: name))
62
+ end
63
+
64
+ def offending?(name)
65
+ return true if ends_with_any?(name, "ForbiddenSuffixes")
66
+
67
+ AGENT_NOUN.match?(name) && !ends_with_any?(name, "AllowedSuffixes")
68
+ end
69
+
70
+ def ends_with_any?(name, config_key)
71
+ Array(cop_config[config_key]).any? { |suffix| name.end_with?(suffix) }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,74 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Kaizo
4
+ # Shared traversal and counting for the argument-arity cops.
5
+ #
6
+ # Walks method definitions written with `def`, `def self.`, `define_method`,
7
+ # and `define_singleton_method`, and reports when the argument count
8
+ # produced by the including cop exceeds the configured `Max`. Including cops
9
+ # must define a private `arity(arguments)` method and a `KIND` constant. The
10
+ # `initialize` of a `Struct.new`/`Data.define` block is exempt.
11
+ module ArgumentCounting
12
+ POSITIONAL_TYPES = %i[arg optarg].freeze
13
+ KEYWORD_TYPES = %i[kwarg kwoptarg].freeze
14
+ DEFINE_METHODS = %i[define_method define_singleton_method].freeze
15
+ STRUCT_OR_DATA = { "Struct" => :new, "Data" => :define }.freeze
16
+ MSG = "Method has too many %<kind>s. [%<count>d/%<max>d]".freeze
17
+
18
+ def on_def(node)
19
+ return if allowed_initialize?(node)
20
+
21
+ check_arity(node)
22
+ end
23
+ alias on_defs on_def
24
+
25
+ def on_block(node)
26
+ check_arity(node) if DEFINE_METHODS.include?(node.method_name)
27
+ end
28
+ alias on_numblock on_block
29
+ alias on_itblock on_block
30
+
31
+ private
32
+
33
+ def positional_arity(arguments)
34
+ arguments.count { |argument| POSITIONAL_TYPES.include?(argument.type) }
35
+ end
36
+
37
+ def keyword_arity(arguments)
38
+ arguments.count { |argument| KEYWORD_TYPES.include?(argument.type) }
39
+ end
40
+
41
+ def check_arity(node)
42
+ max = cop_config["Max"]
43
+ count = arity(node.arguments)
44
+ return unless max && count > max
45
+
46
+ message = format(MSG, kind: self.class::KIND, count: count, max: max)
47
+ add_offense(offense_location(node), message: message) { self.max = count }
48
+ end
49
+
50
+ def offense_location(node)
51
+ node.any_block_type? ? node.send_node.loc.selector : node.loc.name
52
+ end
53
+
54
+ def allowed_initialize?(node)
55
+ node.method?(:initialize) && struct_or_data_definition?(enclosing_block(node))
56
+ end
57
+
58
+ # The block a method definition lives in, seen through the `begin` that
59
+ # wraps the body when the block holds more than one statement.
60
+ def enclosing_block(node)
61
+ parent = node.parent
62
+ parent&.begin_type? ? parent.parent : parent
63
+ end
64
+
65
+ def struct_or_data_definition?(node)
66
+ return false unless node&.block_type?
67
+
68
+ receiver = node.receiver
69
+ receiver&.const_type? && node.method?(STRUCT_OR_DATA[receiver.const_name])
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,119 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Kaizo
4
+ # Requires an explicit `begin`...`end` block when a method body has a
5
+ # `rescue` or `ensure` clause, instead of attaching the clause directly
6
+ # to the method definition (an "implicit begin"). This is the inverse
7
+ # of `Style/RedundantBegin`, which kaizo's default configuration
8
+ # disables so the two cops do not fight each other.
9
+ #
10
+ # An explicit `begin` names the unit of work being guarded, keeps the
11
+ # rescue/ensure visually bound to it, and leaves room for the method to
12
+ # grow other statements around the guarded region without silently
13
+ # widening what the `rescue` covers.
14
+ #
15
+ # Modifier `rescue` expressions (`foo rescue nil`) and endless method
16
+ # definitions are not flagged.
17
+ #
18
+ # @safety
19
+ # Autocorrection is skipped when the body does not sit on its own lines
20
+ # between `def` and `end` (a single-line definition, for example), and
21
+ # for bodies containing heredocs or other multiline string, symbol, or
22
+ # regexp literals, because re-indenting those lines could change their
23
+ # contents.
24
+ #
25
+ # @example
26
+ # # bad
27
+ # def foo
28
+ # do_something
29
+ # ensure
30
+ # cleanup
31
+ # end
32
+ #
33
+ # # good
34
+ # def foo
35
+ # begin
36
+ # do_something
37
+ # ensure
38
+ # cleanup
39
+ # end
40
+ # end
41
+ #
42
+ class ExplicitBegin < Base
43
+ include Alignment
44
+ include RangeHelp
45
+ include RescueNode
46
+ extend AutoCorrector
47
+
48
+ MSG = "Use an explicit `begin` block for `rescue`/`ensure` in a method body.".freeze
49
+
50
+ def on_def(node)
51
+ return if node.endless?
52
+
53
+ body = node.body
54
+ return unless implicit_begin?(body)
55
+
56
+ range = clause_keyword(body)
57
+ if safe_to_correct?(node, body)
58
+ add_offense(range) { |corrector| wrap_in_begin(corrector, node, body) }
59
+ else
60
+ add_offense(range)
61
+ end
62
+ end
63
+ alias on_defs on_def
64
+
65
+ private
66
+
67
+ def implicit_begin?(body)
68
+ return false unless body
69
+
70
+ body.ensure_type? || clause_rescue?(body)
71
+ end
72
+
73
+ def clause_rescue?(node)
74
+ node&.rescue_type? && !rescue_modifier?(node.resbody_branches.first)
75
+ end
76
+
77
+ def clause_keyword(body)
78
+ clause = first_clause(body)
79
+ clause.rescue_type? ? clause.resbody_branches.first.loc.keyword : clause.loc.keyword
80
+ end
81
+
82
+ def first_clause(body)
83
+ return body unless body.ensure_type?
84
+
85
+ inner = body.children.first
86
+ clause_rescue?(inner) ? inner : body
87
+ end
88
+
89
+ def safe_to_correct?(node, body)
90
+ own_body_lines?(node, body) && !multiline_literal?(body)
91
+ end
92
+
93
+ def own_body_lines?(node, body)
94
+ node.loc.keyword.line < body.first_line && body.last_line < node.loc.end.line
95
+ end
96
+
97
+ def multiline_literal?(body)
98
+ body.each_descendant(:any_str, :any_sym, :regexp).any? do |literal|
99
+ literal.multiline? || (literal.respond_to?(:heredoc?) && literal.heredoc?)
100
+ end
101
+ end
102
+
103
+ def wrap_in_begin(corrector, node, body)
104
+ body_range = range_by_whole_lines(body.source_range)
105
+ indent = indentation(node)
106
+ corrector.replace(body_range, wrapped_body(body_range, indent))
107
+ end
108
+
109
+ def wrapped_body(body_range, indent)
110
+ step = " " * configured_indentation_width
111
+ shifted = body_range.source.each_line.map do |line|
112
+ line.strip.empty? ? line : "#{step}#{line}"
113
+ end.join
114
+ "#{indent}begin\n#{shifted.chomp}\n#{indent}end"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,92 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Kaizo
4
+ # Requires `FileUtils` to be mixed in with `include` (or `extend`) once it
5
+ # is used more than once in a class or module, rather than qualifying every
6
+ # call with the `FileUtils.` receiver.
7
+ #
8
+ # Repeating the `FileUtils.` receiver is noise; mixing the module in once
9
+ # names the capability and lets the body call `mkdir_p`, `cp`, and friends
10
+ # directly. Use `include` for instance-level use and `extend` for
11
+ # class/singleton-level use. A single qualified call is left alone, and a
12
+ # class or module that already mixes `FileUtils` in is not flagged.
13
+ #
14
+ # The class or module is reported once. Nested classes and modules are
15
+ # counted on their own -- one call in an outer class and one in a nested
16
+ # class do not add up -- and a `FileUtils` call in the superclass
17
+ # expression does not count toward the class body.
18
+ #
19
+ # There is no autocorrection: whether to `include` or `extend`, and where
20
+ # the mixin belongs, is a judgment call for a human.
21
+ #
22
+ # @example
23
+ # # bad - the `FileUtils.` receiver is repeated
24
+ # class Backup
25
+ # def run
26
+ # FileUtils.mkdir_p(dir)
27
+ # FileUtils.cp(src, dir)
28
+ # end
29
+ # end
30
+ #
31
+ # # good - mix it in once, then call unqualified
32
+ # class Backup
33
+ # include FileUtils
34
+ #
35
+ # def run
36
+ # mkdir_p(dir)
37
+ # cp(src, dir)
38
+ # end
39
+ # end
40
+ #
41
+ class FileUtilsInclusion < Base
42
+ MSG = "`FileUtils` is used %<count>d times in this %<scope>s; " \
43
+ "`include`/`extend` FileUtils and call its methods unqualified.".freeze
44
+
45
+ # @!method file_utils_call?(node)
46
+ def_node_matcher :file_utils_call?, <<~PATTERN
47
+ (send (const {nil? cbase} :FileUtils) ...)
48
+ PATTERN
49
+
50
+ # @!method file_utils_mixin?(node)
51
+ def_node_matcher :file_utils_mixin?, <<~PATTERN
52
+ (send nil? {:include :extend} (const {nil? cbase} :FileUtils) ...)
53
+ PATTERN
54
+
55
+ def on_class(node)
56
+ check(node, "class")
57
+ end
58
+
59
+ def on_module(node)
60
+ check(node, "module")
61
+ end
62
+
63
+ private
64
+
65
+ def check(node, scope)
66
+ return if own_sends(node) { |send| file_utils_mixin?(send) }.any?
67
+
68
+ count = own_sends(node) { |send| file_utils_call?(send) }.size
69
+ return if count < 2
70
+
71
+ add_offense(node.loc.name, message: format(MSG, count: count, scope: scope))
72
+ end
73
+
74
+ # Sends in `namespace`'s own body that match the block -- not its
75
+ # superclass expression, and not a nested class or module (those own
76
+ # their sends and are checked on their own).
77
+ def own_sends(namespace)
78
+ body = namespace.body
79
+ return [] unless body
80
+
81
+ [body, *body.each_descendant].select do |send_node|
82
+ send_node.send_type? && yield(send_node) && owning_namespace(send_node).equal?(namespace)
83
+ end
84
+ end
85
+
86
+ def owning_namespace(node)
87
+ node.each_ancestor(:class, :module).first
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,35 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Kaizo
4
+ # Checks that a method does not declare too many keyword arguments.
5
+ #
6
+ # Keyword arguments are self-documenting, but a long list of them is still
7
+ # a sign that the related values want to be modeled as an object. Required
8
+ # (`kwarg`) and optional (`kwoptarg`) parameters are counted; `**rest` is
9
+ # not.
10
+ #
11
+ # @example Max: 2
12
+ # # bad
13
+ # def calculate_volume(width:, length:, height:)
14
+ # end
15
+ #
16
+ # # good
17
+ # def calculate_volume(shape)
18
+ # end
19
+ #
20
+ class KeywordArguments < Base
21
+ include ArgumentCounting
22
+
23
+ exclude_limit "Max"
24
+
25
+ KIND = "keyword arguments".freeze
26
+
27
+ private
28
+
29
+ def arity(arguments)
30
+ keyword_arity(arguments)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end