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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +529 -0
- data/config/default.yml +210 -0
- data/lib/kaizo/plugin.rb +27 -0
- data/lib/kaizo/version.rb +3 -0
- data/lib/kaizo.rb +15 -0
- data/lib/rubocop/cop/kaizo/agent_noun_class_name.rb +76 -0
- data/lib/rubocop/cop/kaizo/argument_counting.rb +74 -0
- data/lib/rubocop/cop/kaizo/explicit_begin.rb +119 -0
- data/lib/rubocop/cop/kaizo/file_utils_inclusion.rb +92 -0
- data/lib/rubocop/cop/kaizo/keyword_arguments.rb +35 -0
- data/lib/rubocop/cop/kaizo/nested_method_calls.rb +130 -0
- data/lib/rubocop/cop/kaizo/next_in_non_void_enumerable.rb +134 -0
- data/lib/rubocop/cop/kaizo/positional_arguments.rb +35 -0
- data/lib/rubocop/cop/kaizo/prefer_pathname.rb +63 -0
- data/lib/rubocop/cop/kaizo/spec_comment.rb +75 -0
- data/lib/rubocop/cop/kaizo/spec_description_prose.rb +136 -0
- data/lib/rubocop/cop/kaizo/total_arguments.rb +39 -0
- data/lib/rubocop/cop/kaizo_cops.rb +12 -0
- metadata +96 -0
data/config/default.yml
ADDED
|
@@ -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/**/*'
|
data/lib/kaizo/plugin.rb
ADDED
|
@@ -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
|
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
|