rubocop-greenroom 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2bc6ffc2c1db29089a00155da4b0146f48242684cf671bf8ee9a13768c2ac32a
4
+ data.tar.gz: 238286bb8cf29ae8dd6fc0da0da83862c8ebd5de462da05097f6d63254c30e62
5
+ SHA512:
6
+ metadata.gz: 6f8f35105863b02830d0e12de9705d950133f0e671046d60c562161e1e4a3171e9b72aeac31e562c8ace4040dce0837c8d6173a202efe6979072681a0afe338e
7
+ data.tar.gz: 41c77a641b78a0b7ddfcf0c7c159fccb219f66ddd1b635f4140d0cfde0e02cce1930ec11e97d91b5521a54fe8761f260c48e916763dab67ed714d3c63cf34e9d
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-08-22
4
+
5
+ - Add `Greenroom/ScienceBlockShape`, which requires `use`, `try`, and
6
+ `compare` once each and in that order inside a `Scientist.run` block.
7
+ - Add `Greenroom/UnsafeScienceBody`, which reports a `return` or a `binding`
8
+ that moves into a `use` or `try` block.
9
+ - Add `Greenroom/CandidateIsPrivate`, which requires the method a `try` block
10
+ calls to be private when the same file defines it.
11
+ - Ship the cops as a lint_roller plugin, so a host adds them through the
12
+ `plugins` key of its RuboCop configuration.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Greenroom contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # rubocop-greenroom
2
+
3
+ `rubocop-greenroom` reports structural problems that RuboCop can identify from one Ruby file.
4
+
5
+ Add the plugin to the host application's `.rubocop.yml`:
6
+
7
+ ```yaml
8
+ plugins:
9
+ - rubocop-greenroom
10
+ ```
11
+
12
+ The plugin enables three cops:
13
+
14
+ - `Greenroom/ScienceBlockShape` requires one `use`, one `try`, and one `compare` call in that order.
15
+ - `Greenroom/UnsafeScienceBody` reports unsafe `return` and `binding` expressions in `use` and `try` bodies.
16
+ - `Greenroom/CandidateIsPrivate` reports a visible same-class candidate definition that is public or protected.
17
+
18
+ These cops inspect one file. A separate diff checker verifies that a `use` body preserves the previous method body.
@@ -0,0 +1,14 @@
1
+ Greenroom/ScienceBlockShape:
2
+ Description: Requires use, try, and compare once, in that order.
3
+ Enabled: true
4
+ VersionAdded: '0.1'
5
+
6
+ Greenroom/UnsafeScienceBody:
7
+ Description: Rejects return and binding in science bodies when they change behavior.
8
+ Enabled: true
9
+ VersionAdded: '0.1'
10
+
11
+ Greenroom/CandidateIsPrivate:
12
+ Description: Requires a same-class candidate method to be private when its definition is visible.
13
+ Enabled: true
14
+ VersionAdded: '0.1'
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Greenroom
6
+ # Requires the candidate method that `try` calls to be private.
7
+ #
8
+ # A candidate is scaffolding. Once the experiment wins, a later change
9
+ # folds the candidate into the method body and deletes the scaffolding. A
10
+ # public candidate invites a caller from outside the class, and that
11
+ # deletion then breaks the caller. A private one keeps the deletion
12
+ # inside the class that owns it.
13
+ #
14
+ # The cop answers only when this file shows the answer: the `try` body
15
+ # must be one call to the object itself, and the definition must sit in
16
+ # the same class. A definition in another file leaves this cop quiet,
17
+ # because the gate that reads the whole change decides that case.
18
+ class CandidateIsPrivate < Base
19
+ include VisibilityHelp
20
+
21
+ MSG = "A candidate method must be private."
22
+
23
+ def on_block(node)
24
+ return unless science_try_block?(node)
25
+
26
+ candidate_call = node.body
27
+ return unless candidate_call&.send_type?
28
+ return unless candidate_call.receiver.nil? || candidate_call.receiver.self_type?
29
+
30
+ class_node = node.each_ancestor.find(&:class_type?)
31
+ return unless class_node
32
+
33
+ definition = candidate_definition(class_node, candidate_call.method_name)
34
+ return unless definition
35
+ return if node_visibility(definition) == :private
36
+
37
+ add_offense(node)
38
+ end
39
+
40
+ private
41
+
42
+ def science_try_block?(node)
43
+ return false unless node.send_node.method?(:try)
44
+
45
+ science_block = node.each_ancestor.find(&:block_type?)
46
+ return false unless science_block
47
+
48
+ science_send = science_block.send_node
49
+ receiver = science_send.receiver
50
+ return false unless science_send.method?(:run) && receiver&.const_name == "Scientist"
51
+
52
+ try_receiver = node.send_node.receiver
53
+ argument_name = science_block.arguments.one? && science_block.arguments.first.children.first
54
+ try_receiver&.lvar_type? && try_receiver.children.first == argument_name
55
+ end
56
+
57
+ def candidate_definition(class_node, method_name)
58
+ class_node.each_descendant(:def).find do |definition|
59
+ definition.method_name == method_name &&
60
+ definition.each_ancestor.find(&:class_type?) == class_node
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Greenroom
6
+ # Requires a `Scientist.run` block to hold `use`, `try`, and `compare`,
7
+ # once each, in that order, and to hold nothing else.
8
+ #
9
+ # The order carries weight beyond taste. The gate that decides whether a
10
+ # change may merge compares the method body before the change against the
11
+ # body inside `use`. A fixed shape lets it find `use` directly, and a
12
+ # free arrangement would put a search in front of that comparison. A
13
+ # reader who wants to write the shape by hand also gets one form to copy
14
+ # rather than several.
15
+ #
16
+ # This cop reports what one file shows, so silence means that this cop
17
+ # cannot tell rather than that a change is accepted.
18
+ class ScienceBlockShape < Base
19
+ MSG = "A Scientist.run block must contain use, try, and compare once, in that order."
20
+
21
+ def on_block(node)
22
+ return unless science_block?(node)
23
+
24
+ calls = body_expressions(node).map { |expression| science_call(expression, node) }
25
+ add_offense(node.send_node) unless calls == %i[use try compare]
26
+ end
27
+
28
+ private
29
+
30
+ def science_block?(node)
31
+ send_node = node.send_node
32
+ receiver = send_node.receiver
33
+ send_node.method?(:run) && receiver&.const_type? && receiver.const_name == "Scientist"
34
+ end
35
+
36
+ def body_expressions(node)
37
+ body = node.body
38
+ return [] unless body
39
+
40
+ body.begin_type? ? body.children : [body]
41
+ end
42
+
43
+ def science_call(expression, science_block)
44
+ return unless expression.block_type?
45
+
46
+ send_node = expression.send_node
47
+ receiver = send_node.receiver
48
+ argument_name = science_block.arguments.one? && science_block.arguments.first.children.first
49
+ return unless receiver&.lvar_type? && receiver.children.first == argument_name
50
+
51
+ send_node.method_name
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Greenroom
6
+ # Rejects the two constructs that change meaning when a method body moves
7
+ # into a science block.
8
+ #
9
+ # A greenroom change moves an existing body, unchanged, into the `use`
10
+ # block. Most constructs survive that move: a trial of `yield`, of
11
+ # `rescue` and `ensure` written directly in the method, of `__method__`,
12
+ # of local variables, and of a constant read from the same lexical place,
13
+ # each behaved the same before and after the move. Two did not, and this
14
+ # cop names those two. The trial covered one form of each construct, so
15
+ # treat the list as measured rather than complete.
16
+ #
17
+ # `return` leaves the enclosing method from inside the block, so the
18
+ # comparison and the publication never run, and the experiment reports
19
+ # nothing while appearing to work.
20
+ #
21
+ # `binding` reports the block's local variables rather than the method's,
22
+ # so the same expression describes a different scope after the move.
23
+ #
24
+ # This cop reports what one file shows. The gate that decides whether a
25
+ # change may merge reads the version before the change as well, so
26
+ # silence here means that this cop cannot tell, rather than that a change
27
+ # is accepted.
28
+ class UnsafeScienceBody < Base
29
+ RETURN_MESSAGE = "A return in a science body bypasses comparison and publication."
30
+ BINDING_MESSAGE = "A binding in a science body observes the block scope."
31
+
32
+ def on_return(node)
33
+ add_offense(node, message: RETURN_MESSAGE) if unsafe_return?(node)
34
+ end
35
+
36
+ def on_send(node)
37
+ return unless node.receiver.nil? && node.method?(:binding)
38
+ return unless science_body_ancestor(node)
39
+
40
+ add_offense(node, message: BINDING_MESSAGE)
41
+ end
42
+
43
+ private
44
+
45
+ # Walks outward and answers for the nearest scope that a `return`
46
+ # leaves. A `return` inside a nested `def` or a lambda returns from
47
+ # that def or that lambda, and it goes to the same place before and
48
+ # after the move, so this cop stays quiet. A `proc` and an ordinary
49
+ # block both return from the enclosing method, so they carry the same
50
+ # hazard as a bare `return` and this cop reports them.
51
+ def unsafe_return?(node)
52
+ node.each_ancestor do |ancestor|
53
+ return false if ancestor.def_type? || ancestor.defs_type?
54
+ return false if ancestor.block_type? && ancestor.lambda?
55
+ return true if science_body_block?(ancestor)
56
+ end
57
+
58
+ false
59
+ end
60
+
61
+ def science_body_ancestor(node)
62
+ node.each_ancestor.find { |ancestor| science_body_block?(ancestor) }
63
+ end
64
+
65
+ # A block counts as a science body when it is `use` or `try` called on
66
+ # the parameter that `Scientist.run` yielded. Matching the parameter
67
+ # rather than the method name alone keeps the cop quiet about an
68
+ # unrelated `use` or `try` that happens to sit inside another block.
69
+ def science_body_block?(node)
70
+ return false unless node.block_type? && %i[use try].include?(node.send_node.method_name)
71
+
72
+ science_block = node.each_ancestor.find(&:block_type?)
73
+ return false unless science_block
74
+
75
+ science_send = science_block.send_node
76
+ receiver = science_send.receiver
77
+ return false unless science_send.method?(:run) && receiver&.const_name == "Scientist"
78
+
79
+ body_receiver = node.send_node.receiver
80
+ argument_name = science_block.arguments.one? && science_block.arguments.first.children.first
81
+ body_receiver&.lvar_type? && body_receiver.children.first == argument_name
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lint_roller"
4
+ require "pathname"
5
+
6
+ module RuboCop
7
+ module Greenroom
8
+ class Plugin < LintRoller::Plugin
9
+ def about
10
+ LintRoller::About.new(
11
+ name: "rubocop-greenroom",
12
+ version: VERSION,
13
+ homepage: "https://github.com/meganemura/greenroom",
14
+ description: "RuboCop rules for Greenroom science blocks"
15
+ )
16
+ end
17
+
18
+ def supported?(context)
19
+ context.engine == :rubocop
20
+ end
21
+
22
+ def rules(_context)
23
+ LintRoller::Rules.new(
24
+ type: :path,
25
+ config_format: :rubocop,
26
+ value: Pathname.new(__dir__).join("../../../config/default.yml")
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Greenroom
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require_relative "rubocop/greenroom/version"
5
+ require_relative "rubocop/greenroom/plugin"
6
+ require_relative "rubocop/cop/greenroom/science_block_shape"
7
+ require_relative "rubocop/cop/greenroom/unsafe_science_body"
8
+ require_relative "rubocop/cop/greenroom/candidate_is_private"
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-greenroom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - meganemura
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rubocop
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.88'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.88'
26
+ description: Checks the single-file structure of Greenroom science blocks.
27
+ email:
28
+ - meganemura@users.noreply.github.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - LICENSE.txt
35
+ - README.md
36
+ - config/default.yml
37
+ - lib/rubocop-greenroom.rb
38
+ - lib/rubocop/cop/greenroom/candidate_is_private.rb
39
+ - lib/rubocop/cop/greenroom/science_block_shape.rb
40
+ - lib/rubocop/cop/greenroom/unsafe_science_body.rb
41
+ - lib/rubocop/greenroom/plugin.rb
42
+ - lib/rubocop/greenroom/version.rb
43
+ homepage: https://github.com/meganemura/greenroom
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ default_lint_roller_plugin: RuboCop::Greenroom::Plugin
48
+ allowed_push_host: https://rubygems.org
49
+ homepage_uri: https://github.com/meganemura/greenroom
50
+ source_code_uri: https://github.com/meganemura/greenroom
51
+ changelog_uri: https://github.com/meganemura/greenroom/blob/main/gems/rubocop-greenroom/CHANGELOG.md
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.3.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.16
68
+ specification_version: 4
69
+ summary: RuboCop rules for Greenroom science blocks
70
+ test_files: []