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.
@@ -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.8.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.17
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: []