kaizo 0.8.0 → 0.9.2
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 +4 -4
- data/CHANGELOG.md +89 -1
- data/README.md +297 -300
- data/config/default.yml +80 -7
- data/lib/kaizo/version.rb +1 -1
- data/lib/rubocop/cop/kaizo/agent_noun_class_name.rb +23 -0
- data/lib/rubocop/cop/kaizo/argument_counting.rb +17 -7
- data/lib/rubocop/cop/kaizo/explicit_begin.rb +9 -0
- data/lib/rubocop/cop/kaizo/file_utils_inclusion.rb +5 -0
- data/lib/rubocop/cop/kaizo/keyword_arguments.rb +22 -0
- data/lib/rubocop/cop/kaizo/nested_method_calls.rb +12 -0
- data/lib/rubocop/cop/kaizo/next_in_non_void_enumerable.rb +11 -0
- data/lib/rubocop/cop/kaizo/plural_collection_name.rb +30 -2
- data/lib/rubocop/cop/kaizo/positional_arguments.rb +16 -0
- data/lib/rubocop/cop/kaizo/prefer_pathname.rb +13 -0
- data/lib/rubocop/cop/kaizo/spec_comment.rb +23 -1
- data/lib/rubocop/cop/kaizo/spec_description_prose.rb +61 -21
- data/lib/rubocop/cop/kaizo/spec_subject.rb +117 -0
- data/lib/rubocop/cop/kaizo/tempfile_create.rb +71 -0
- data/lib/rubocop/cop/kaizo/total_arguments.rb +18 -0
- data/lib/rubocop/cop/kaizo_cops.rb +2 -0
- metadata +4 -2
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Kaizo
|
|
4
|
+
# Requires the unit under test to be declared with `subject`, not `let`.
|
|
5
|
+
# `subject` is RSpec's name for the object being specified; hiding it in a
|
|
6
|
+
# `let` obscures which object the examples are about and forfeits
|
|
7
|
+
# `is_expected`/one-liner syntax.
|
|
8
|
+
#
|
|
9
|
+
# A `let` (or `let!`) is flagged when the value it returns is confidently
|
|
10
|
+
# an instance of the class under test: the block's final expression is a
|
|
11
|
+
# `.new` call on `described_class`, on the constant named by an enclosing
|
|
12
|
+
# `describe`/`context` (full or short name), or on a constant matching the
|
|
13
|
+
# spec's file name (`pool_spec.rb` names `Pool`, `api_client_spec.rb`
|
|
14
|
+
# names `APIClient`).
|
|
15
|
+
#
|
|
16
|
+
# A `let` that builds a second instance on purpose -- an `other` for an
|
|
17
|
+
# equality spec, say -- is exempted through `AllowedMethods` or
|
|
18
|
+
# `AllowedPatterns`, both matched against the `let` name.
|
|
19
|
+
#
|
|
20
|
+
# There is no autocorrection: renaming the helper every example refers to
|
|
21
|
+
# is a change the spec's author should make deliberately.
|
|
22
|
+
#
|
|
23
|
+
# == Configuration
|
|
24
|
+
#
|
|
25
|
+
# [+AllowedMethods+] `let` names never flagged. Default: none.
|
|
26
|
+
# [+AllowedPatterns+] Regexps matched against the `let` name; a match is
|
|
27
|
+
# exempt. Default: none.
|
|
28
|
+
# [+Include+] Files the cop runs on. Default: <tt>**/*_spec.rb</tt>.
|
|
29
|
+
#
|
|
30
|
+
# Kaizo/SpecSubject:
|
|
31
|
+
# AllowedMethods:
|
|
32
|
+
# - other # a second instance for equality specs
|
|
33
|
+
# AllowedPatterns:
|
|
34
|
+
# - '\Aother_'
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# # bad
|
|
38
|
+
# RSpec.describe Session::Pool do
|
|
39
|
+
# let(:pool) { described_class.new }
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# # good
|
|
43
|
+
# RSpec.describe Session::Pool do
|
|
44
|
+
# subject(:pool) { described_class.new }
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# @example AllowedMethods: ['other'] (default: [])
|
|
48
|
+
# # good - a deliberate second instance
|
|
49
|
+
# RSpec.describe Session::Pool do
|
|
50
|
+
# subject(:pool) { described_class.new }
|
|
51
|
+
#
|
|
52
|
+
# let(:other) { described_class.new }
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
class SpecSubject < Base
|
|
56
|
+
include AllowedMethods
|
|
57
|
+
include AllowedPattern
|
|
58
|
+
|
|
59
|
+
MSG = "Declare the unit under test with `subject(:%<name>s)`, not `let`.".freeze
|
|
60
|
+
|
|
61
|
+
# @!method let_declaration(node)
|
|
62
|
+
def_node_matcher :let_declaration, <<~PATTERN
|
|
63
|
+
(block (send nil? {:let :let!} (sym $_)) _ $_)
|
|
64
|
+
PATTERN
|
|
65
|
+
|
|
66
|
+
# @!method constructed_class(node)
|
|
67
|
+
def_node_matcher :constructed_class, <<~PATTERN
|
|
68
|
+
(send ${(send nil? :described_class) (const _ _)} :new ...)
|
|
69
|
+
PATTERN
|
|
70
|
+
|
|
71
|
+
# @!method described_constant(node)
|
|
72
|
+
def_node_matcher :described_constant, <<~PATTERN
|
|
73
|
+
(block (send {(const {nil? cbase} :RSpec) nil?} {:describe :context} $(const ...) ...) ...)
|
|
74
|
+
PATTERN
|
|
75
|
+
|
|
76
|
+
def on_block(node)
|
|
77
|
+
name, body = let_declaration(node)
|
|
78
|
+
return unless name
|
|
79
|
+
return if allowed_method?(name) || matches_allowed_pattern?(name.to_s)
|
|
80
|
+
return unless unit_under_test?(node, final_expression(body))
|
|
81
|
+
|
|
82
|
+
add_offense(node.send_node.loc.selector, message: format(MSG, name:))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def final_expression(body)
|
|
88
|
+
body&.begin_type? ? body.children.last : body
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unit_under_test?(node, expression)
|
|
92
|
+
receiver = expression && constructed_class(expression)
|
|
93
|
+
return false unless receiver
|
|
94
|
+
return true if receiver.send_type?
|
|
95
|
+
|
|
96
|
+
described?(node, receiver)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def described?(node, const_node)
|
|
100
|
+
described_constants(node).any? do |described|
|
|
101
|
+
described.const_name == const_node.const_name ||
|
|
102
|
+
described.short_name == const_node.short_name
|
|
103
|
+
end || file_named_after?(const_node.short_name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def described_constants(node)
|
|
107
|
+
node.each_ancestor(:block).filter_map { |ancestor| described_constant(ancestor) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def file_named_after?(short_name)
|
|
111
|
+
base = processed_source.file_path&.[](%r{([^/]+)_spec\.rb\z}, 1)
|
|
112
|
+
base && short_name.to_s.downcase == base.delete("_")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module RuboCop
|
|
2
|
+
module Cop
|
|
3
|
+
module Kaizo
|
|
4
|
+
# Requires temporary files to be created with block-form `Tempfile.create`,
|
|
5
|
+
# and flags `Tempfile.new`, `Tempfile.open`, and blockless `Tempfile.create`.
|
|
6
|
+
#
|
|
7
|
+
# Only the block form cleans up deterministically: the file is closed and
|
|
8
|
+
# removed when the block returns, however it returns. A `Tempfile` built
|
|
9
|
+
# with `.new` or `.open` is removed by a GC finalizer that runs at some
|
|
10
|
+
# unpredictable point -- possibly never -- and blockless `Tempfile.create`
|
|
11
|
+
# hands back a plain `File` that is never removed automatically at all.
|
|
12
|
+
#
|
|
13
|
+
# There is no autocorrection: moving the file's users into the block is a
|
|
14
|
+
# restructuring, and the block's return value replaces the handle the old
|
|
15
|
+
# code held onto.
|
|
16
|
+
#
|
|
17
|
+
# == Configuration
|
|
18
|
+
#
|
|
19
|
+
# No cop-specific options; the standard per-cop settings (+Enabled+,
|
|
20
|
+
# +Severity+, +Include+/+Exclude+) apply.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# # bad
|
|
24
|
+
# file = Tempfile.new("report")
|
|
25
|
+
# file = Tempfile.open("report")
|
|
26
|
+
# file = Tempfile.create("report")
|
|
27
|
+
#
|
|
28
|
+
# # good
|
|
29
|
+
# Tempfile.create("report") do |file|
|
|
30
|
+
# file.write(data)
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
class TempfileCreate < Base
|
|
34
|
+
MSG = "Use `Tempfile.create` with a block instead of `Tempfile.%<method>s`; " \
|
|
35
|
+
"finalizer-based cleanup is unpredictable.".freeze
|
|
36
|
+
BLOCKLESS_CREATE_MSG = "Pass a block to `Tempfile.create`; " \
|
|
37
|
+
"without one the file is never removed.".freeze
|
|
38
|
+
|
|
39
|
+
RESTRICT_ON_SEND = %i[new open create].freeze
|
|
40
|
+
|
|
41
|
+
# @!method tempfile_call?(node)
|
|
42
|
+
def_node_matcher :tempfile_call?, <<~PATTERN
|
|
43
|
+
(send (const {nil? cbase} :Tempfile) _ ...)
|
|
44
|
+
PATTERN
|
|
45
|
+
|
|
46
|
+
def on_send(node)
|
|
47
|
+
return unless tempfile_call?(node)
|
|
48
|
+
return if node.method?(:create) && block_given_to?(node)
|
|
49
|
+
|
|
50
|
+
range = node.receiver.source_range.join(node.loc.selector)
|
|
51
|
+
add_offense(range, message: message_for(node))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def message_for(node)
|
|
57
|
+
return BLOCKLESS_CREATE_MSG if node.method?(:create)
|
|
58
|
+
|
|
59
|
+
format(MSG, method: node.method_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def block_given_to?(node)
|
|
63
|
+
return true if node.last_argument&.block_pass_type?
|
|
64
|
+
|
|
65
|
+
parent = node.parent
|
|
66
|
+
parent&.any_block_type? && parent.send_node.equal?(node)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -12,6 +12,24 @@ module RuboCop
|
|
|
12
12
|
# `initialize` of a `Struct.new`/`Data.define` block is exempt, since those
|
|
13
13
|
# parameters mirror the value object's attributes.
|
|
14
14
|
#
|
|
15
|
+
# == Configuration
|
|
16
|
+
#
|
|
17
|
+
# [+Max+] Most arguments -- positional plus keyword -- a method may
|
|
18
|
+
# declare. Default: +2+.
|
|
19
|
+
# [+AllowedMethods+] Method names exempt from the limit. Default: none.
|
|
20
|
+
# [+AllowedPatterns+] Regexps matched against the method name; a match is
|
|
21
|
+
# exempt. Default: none.
|
|
22
|
+
# [+Exclude+] Paths the cop skips. Default: <tt>**/spec/**/*</tt> and
|
|
23
|
+
# <tt>**/test/**/*</tt>, matching `KeywordArguments` -- a
|
|
24
|
+
# total bound would re-police the keyword freedom tests get.
|
|
25
|
+
# `PositionalArguments` alone still bounds positional
|
|
26
|
+
# arguments there. Set it to <tt>[]</tt> to police tests too.
|
|
27
|
+
#
|
|
28
|
+
# Kaizo/TotalArguments:
|
|
29
|
+
# Max: 3
|
|
30
|
+
# AllowedMethods:
|
|
31
|
+
# - initialize
|
|
32
|
+
#
|
|
15
33
|
# @example Max: 3
|
|
16
34
|
# # bad
|
|
17
35
|
# def calculate_volume(width, length, height, shape_type)
|
|
@@ -7,7 +7,9 @@ require_relative "kaizo/agent_noun_class_name"
|
|
|
7
7
|
require_relative "kaizo/nested_method_calls"
|
|
8
8
|
require_relative "kaizo/spec_comment"
|
|
9
9
|
require_relative "kaizo/spec_description_prose"
|
|
10
|
+
require_relative "kaizo/spec_subject"
|
|
10
11
|
require_relative "kaizo/file_utils_inclusion"
|
|
11
12
|
require_relative "kaizo/prefer_pathname"
|
|
12
13
|
require_relative "kaizo/explicit_begin"
|
|
13
14
|
require_relative "kaizo/next_in_non_void_enumerable"
|
|
15
|
+
require_relative "kaizo/tempfile_create"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kaizo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -67,6 +67,8 @@ files:
|
|
|
67
67
|
- lib/rubocop/cop/kaizo/prefer_pathname.rb
|
|
68
68
|
- lib/rubocop/cop/kaizo/spec_comment.rb
|
|
69
69
|
- lib/rubocop/cop/kaizo/spec_description_prose.rb
|
|
70
|
+
- lib/rubocop/cop/kaizo/spec_subject.rb
|
|
71
|
+
- lib/rubocop/cop/kaizo/tempfile_create.rb
|
|
70
72
|
- lib/rubocop/cop/kaizo/total_arguments.rb
|
|
71
73
|
- lib/rubocop/cop/kaizo_cops.rb
|
|
72
74
|
homepage: https://github.com/flipmine/kaizo
|
|
@@ -91,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
91
93
|
- !ruby/object:Gem::Version
|
|
92
94
|
version: '0'
|
|
93
95
|
requirements: []
|
|
94
|
-
rubygems_version: 4.0.
|
|
96
|
+
rubygems_version: 4.0.18
|
|
95
97
|
specification_version: 4
|
|
96
98
|
summary: A strict, punishing set of RuboCop design cops for AI-agent-authored Ruby.
|
|
97
99
|
test_files: []
|