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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +18 -0
- data/config/default.yml +14 -0
- data/lib/rubocop/cop/greenroom/candidate_is_private.rb +66 -0
- data/lib/rubocop/cop/greenroom/science_block_shape.rb +56 -0
- data/lib/rubocop/cop/greenroom/unsafe_science_body.rb +86 -0
- data/lib/rubocop/greenroom/plugin.rb +31 -0
- data/lib/rubocop/greenroom/version.rb +7 -0
- data/lib/rubocop-greenroom.rb +8 -0
- metadata +70 -0
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.
|
data/config/default.yml
ADDED
|
@@ -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,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: []
|