decode 0.24.2 → 0.24.4

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/decode/rbs.rb +1 -1
  4. data/context/coverage.md +1 -1
  5. data/context/getting-started.md +1 -1
  6. data/context/ruby-documentation.md +3 -3
  7. data/context/types.md +127 -0
  8. data/lib/decode/comment/attribute.rb +5 -2
  9. data/lib/decode/comment/constant.rb +47 -0
  10. data/lib/decode/comment/node.rb +32 -12
  11. data/lib/decode/comment/option.rb +1 -1
  12. data/lib/decode/comment/parameter.rb +6 -2
  13. data/lib/decode/comment/rbs.rb +8 -8
  14. data/lib/decode/comment/tag.rb +19 -0
  15. data/lib/decode/comment/tags.rb +16 -5
  16. data/lib/decode/comment/text.rb +1 -0
  17. data/lib/decode/comment/yields.rb +5 -1
  18. data/lib/decode/definition.rb +33 -31
  19. data/lib/decode/documentation.rb +10 -5
  20. data/lib/decode/index.rb +12 -7
  21. data/lib/decode/language/generic.rb +10 -1
  22. data/lib/decode/language/reference.rb +7 -4
  23. data/lib/decode/language/ruby/class.rb +2 -2
  24. data/lib/decode/language/ruby/code.rb +21 -3
  25. data/lib/decode/language/ruby/definition.rb +15 -3
  26. data/lib/decode/language/ruby/generic.rb +2 -1
  27. data/lib/decode/language/ruby/parser.rb +132 -91
  28. data/lib/decode/language/ruby/reference.rb +4 -1
  29. data/lib/decode/language/ruby/segment.rb +2 -2
  30. data/lib/decode/languages.rb +29 -8
  31. data/lib/decode/location.rb +12 -1
  32. data/lib/decode/rbs/class.rb +91 -14
  33. data/lib/decode/rbs/generator.rb +67 -11
  34. data/lib/decode/rbs/method.rb +394 -65
  35. data/lib/decode/rbs/module.rb +81 -5
  36. data/lib/decode/rbs/type.rb +51 -0
  37. data/lib/decode/rbs/wrapper.rb +10 -3
  38. data/lib/decode/scope.rb +2 -2
  39. data/lib/decode/segment.rb +3 -2
  40. data/lib/decode/source.rb +5 -14
  41. data/lib/decode/syntax/rewriter.rb +4 -1
  42. data/lib/decode/trie.rb +29 -21
  43. data/lib/decode/version.rb +2 -1
  44. data/readme.md +6 -0
  45. data/releases.md +6 -0
  46. data/sig/decode.rbs +1189 -0
  47. data.tar.gz.sig +0 -0
  48. metadata +5 -15
  49. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6a3ca54cbbfe55c6946466efdeef71ee9b103ceb59ab27c313a9e3a4345264b
4
- data.tar.gz: 00fe1c5faa769e7daa607cd46a48920ad04b4926493e1b841154d9a5f193b12a
3
+ metadata.gz: 69a1cefaeaebe1d53746d8244acad208122dccd964ef8ded1c70b7f3e1ab856f
4
+ data.tar.gz: f3ef222826e20af488964480baf64d72ee9b55a6ad849057f40eebe09934ca37
5
5
  SHA512:
6
- metadata.gz: 288e3c278877013c633055242d28c1f2097ca38576896f86d486ee56f1ef3e246af36dce75d30c9c364cd65b9d4928b875b0f7d384c485efde3b27cad8658b14
7
- data.tar.gz: debf415bab150aaeb345e3013bc6d07896ccf02006e0787eef2ba73754b9c02725007df042e397bedfc02d0b7b8acc266a946535acafcc6d03bd4e069836a937
6
+ metadata.gz: a2c97db6ff06407597b2c42513a0cb4827824abb41ec480a8e6fb1e2d3b8c4366e61c2d34f1f2f706d3cee33f6be6ff5464ef8e668e491534a9692954d14bba3
7
+ data.tar.gz: f051d9ea291738263feee8091a40137ca4578316f762b6d93ef26dc03001f1a138d26a25878c39a7029eee4f59a742a2d59492a1b0ac55a77c8312d28de53a86
checksums.yaml.gz.sig CHANGED
Binary file
data/bake/decode/rbs.rb CHANGED
@@ -13,6 +13,6 @@ end
13
13
  # @parameter root [String] The root path to index.
14
14
  def generate(root)
15
15
  index = Decode::Index.for(root)
16
- generator = Decode::RBS::Generator.new
16
+ generator = Decode::RBS::Generator.new(include_private: true)
17
17
  generator.generate(index)
18
18
  end
data/context/coverage.md CHANGED
@@ -60,7 +60,7 @@ end
60
60
 
61
61
  # Process user data and return formatted results.
62
62
  # @parameter name [String] The user's name.
63
- # @returns [Boolean] Success status.
63
+ # @returns [bool] Success status.
64
64
  def process(name)
65
65
  # Validation logic here:
66
66
  return false if name.empty?
@@ -71,7 +71,7 @@ source_code = <<~RUBY
71
71
  # Initialize a new user.
72
72
  # @parameter email [String] The user's email address.
73
73
  # @parameter options [Hash] Additional options.
74
- # @option :active [Boolean] Whether the account is active.
74
+ # @option :active [bool] Whether the account is active.
75
75
  # @raises [ArgumentError] If email is invalid.
76
76
  def initialize(email, options = {})
77
77
  # Validate the email format:
@@ -51,7 +51,7 @@ class User
51
51
 
52
52
  # Authenticate the user with the provided password.
53
53
  # @parameter password [String] The password to verify.
54
- # @returns [Boolean] True if authentication succeeds.
54
+ # @returns [bool] True if authentication succeeds.
55
55
  def authenticate(password)
56
56
  # Hash the password for comparison:
57
57
  hashed = hash_password(password)
@@ -64,7 +64,7 @@ class User
64
64
  # This method sets the user's status to inactive. Use this instead of
65
65
  # the deprecated {disable!} method. The account status can be checked
66
66
  # using `active?` or by examining the `:active` attribute.
67
- # @returns [Boolean] Returns `true` if deactivation was successful.
67
+ # @returns [bool] Returns `true` if deactivation was successful.
68
68
  def deactivate!
69
69
  @active = false
70
70
  true
@@ -187,7 +187,7 @@ Documents hash options (keyword arguments).
187
187
 
188
188
  ```ruby
189
189
  # @parameter user [User] The user object.
190
- # @option :cached [Boolean] Whether to cache the result.
190
+ # @option :cached [bool] Whether to cache the result.
191
191
  # @option :timeout [Integer] Request timeout in seconds.
192
192
  def fetch_user_data(user, **options)
193
193
  # ...
data/context/types.md ADDED
@@ -0,0 +1,127 @@
1
+ # Setting Up RBS Types and Steep Type Checking for Ruby Gems
2
+
3
+ This guide covers the process for establishing robust type checking in Ruby gems using RBS and Steep, focusing on automated generation from source documentation and proper validation.
4
+
5
+ ## Core Process
6
+
7
+ ### Documentation-Driven RBS Generation
8
+
9
+ Generate RBS files from documentation:
10
+
11
+ ```bash
12
+ bake decode:rbs:generate lib > sig/example/gem.rbs`
13
+ ```
14
+
15
+ At a minimum, add `@parameter`, `@attribute` and `@returns` documentation to all public methods.
16
+
17
+ #### Parametric Types
18
+
19
+ Use `@rbs generic` comments to define type parameters for classes and modules:
20
+
21
+ ```ruby
22
+ # @rbs generic T
23
+ class Container
24
+ # @parameter item [T] The item to store
25
+ def initialize(item)
26
+ @item = item
27
+ end
28
+
29
+ # @returns [T] The stored item
30
+ def get
31
+ @item
32
+ end
33
+ end
34
+ ```
35
+
36
+ Use `@rbs` comments for parametric method signatures:
37
+
38
+ ```ruby
39
+ # From above:
40
+ class Container
41
+ # @rbs () { (T) -> void } -> void
42
+ def each
43
+ yield @item
44
+ end
45
+ ```
46
+
47
+ #### Interfaces
48
+
49
+ Create interfaces in `sig/example/gem/interface.rbs`:
50
+
51
+ ```rbs
52
+ module Example
53
+ module Gem
54
+ interface _Interface
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ You can use the interface in `@parameter`, `@attribute` and `@returns` types.
61
+
62
+ ### Testing
63
+
64
+ Run tests using the `steep` gem.
65
+
66
+ ```bash
67
+ steep check
68
+ ```
69
+
70
+ **Process**: Start with basic generation, then refine based on Steep feedback.
71
+
72
+ 1. Generate initial RBS from documentation
73
+ 2. Run `steep check lib` to identify issues
74
+ 3. Fix structural problems (inheritance, missing docs)
75
+ 4. Iterate until clean validation
76
+
77
+ ### Deploymnet
78
+
79
+ Make sure `bake-test-types` is added to the `test` group in `gems.rb` (or `Gemfile`).
80
+
81
+ ```ruby
82
+ group :test do
83
+ # ...
84
+ gem "bake-test"
85
+ gem "bake-test-external"
86
+ gem "bake-test-types"
87
+ # ...
88
+ end
89
+ ```
90
+
91
+ Then, create `.github/workflows/test-types.yaml`:
92
+
93
+ ```yaml
94
+ name: Test Types
95
+
96
+ on: [push, pull_request]
97
+
98
+ permissions:
99
+ contents: read
100
+
101
+ env:
102
+ CONSOLE_OUTPUT: XTerm
103
+
104
+ jobs:
105
+ test:
106
+ name: ${{matrix.ruby}} on ${{matrix.os}}
107
+ runs-on: ${{matrix.os}}-latest
108
+
109
+ strategy:
110
+ matrix:
111
+ os:
112
+ - ubuntu
113
+
114
+ ruby:
115
+ - "3.4"
116
+
117
+ steps:
118
+ - uses: actions/checkout@v4
119
+ - uses: ruby/setup-ruby@v1
120
+ with:
121
+ ruby-version: ${{matrix.ruby}}
122
+ bundler-cache: true
123
+
124
+ - name: Run tests
125
+ timeout-minutes: 10
126
+ run: bundle exec bake test:types
127
+ ```
@@ -13,13 +13,16 @@ module Decode
13
13
  # - `@attribute [Integer] The person's age.`
14
14
  #
15
15
  class Attribute < Tag
16
- PATTERN = /\A\[(?<type>.*?)\](\s+(?<details>.*?))?\Z/
16
+ # @constant [Regexp] Pattern for matching attribute declarations.
17
+ PATTERN = /\A\[#{Tag.bracketed_content(:type)}\](\s+(?<details>.*?))?\Z/
17
18
 
18
19
  # Build an attribute from a directive and match.
19
20
  # @parameter directive [String] The original directive text.
20
21
  # @parameter match [MatchData] The regex match data.
21
22
  def self.build(directive, match)
22
- node = self.new(directive, match[:type])
23
+ type = match[:type] or raise "Missing type in attribute match!"
24
+
25
+ node = self.new(directive, type)
23
26
 
24
27
  if details = match[:details]
25
28
  node.add(Text.new(details))
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "tag"
7
+ require_relative "text"
8
+
9
+ module Decode
10
+ module Comment
11
+ # Represents a constant type declaration.
12
+ #
13
+ # - `@constant [Regexp] Pattern for matching parameters.`
14
+ #
15
+ class Constant < Tag
16
+ # @constant [Regexp] Pattern for matching constant declarations.
17
+ PATTERN = /\A\[#{Tag.bracketed_content(:type)}\](\s+(?<details>.*?))?\Z/
18
+
19
+ # Build a constant from a directive and regex match.
20
+ # @parameter directive [String] The original directive text.
21
+ # @parameter match [MatchData] The regex match data containing type and details.
22
+ # @returns [Constant] A new constant object.
23
+ def self.build(directive, match)
24
+ type = match[:type] or raise "Missing type in constant match!"
25
+
26
+ node = self.new(directive, type)
27
+
28
+ if details = match[:details]
29
+ node.add(Text.new(details))
30
+ end
31
+
32
+ return node
33
+ end
34
+
35
+ # Initialize a new constant.
36
+ # @parameter directive [String] The directive that generated the tag.
37
+ # @parameter type [String] The type of the constant.
38
+ def initialize(directive, type)
39
+ super(directive)
40
+ @type = type
41
+ end
42
+
43
+ # @attribute [String] The type of the constant.
44
+ attr :type
45
+ end
46
+ end
47
+ end
@@ -8,49 +8,67 @@ module Decode
8
8
  # Represents a node in a comment tree structure.
9
9
  class Node
10
10
  # Initialize the node.
11
- # @parameter children [Array(Node) | Nil]
12
- def initialize(children = nil)
11
+ # @parameter children [Array(Node | Text)?] The initial children array containing both structured nodes and text content.
12
+ def initialize(children)
13
13
  @children = children
14
14
  end
15
15
 
16
16
  # Whether this node has any children nodes.
17
17
  # Ignores {Text} instances.
18
- # @returns [Boolean]
18
+ # @returns [bool]
19
19
  def children?
20
- @children&.any?{|child| child.is_a?(Node)}
20
+ @children&.any?{|child| child.is_a?(Node)} || false
21
21
  end
22
22
 
23
23
  # Add a child node to this node.
24
- # @parameter child [Node] The node to add.
24
+ # @parameter child [Node | Text] The node to add.
25
25
  def add(child)
26
- @children ||= []
27
- @children << child
26
+ if children = @children
27
+ children << child
28
+ else
29
+ @children = [child]
30
+ end
31
+
32
+ return self
28
33
  end
29
34
 
30
- # @attribute [Array(Node | Text) | Nil] Any children of this node.
35
+ # Contains a mix of Node objects (structured comment tags like `@parameter`, `@returns`) and Text objects (plain comment text and tag descriptions).
36
+ # @attribute [Array(Node | Text)?] The children of this node.
31
37
  attr :children
32
38
 
33
39
  # Enumerate all non-text children nodes.
40
+ # @yields {|node| process each node}
41
+ # @parameter node [Node] A structured child node (Text nodes are filtered out).
42
+ # @returns [Enumerator(Node)] Returns an enumerator if no block given.
43
+ # @returns [self] Otherwise returns self.
34
44
  def each(&block)
35
45
  return to_enum unless block_given?
36
46
 
37
47
  @children&.each do |child|
38
48
  yield child if child.is_a?(Node)
39
49
  end
50
+
51
+ return self
40
52
  end
41
53
 
42
54
  # Filter children nodes by class type.
43
55
  # @parameter klass [Class] The class to filter by.
56
+ # @yields {|node| process each filtered node}
57
+ # @parameter node [Object] A child node that is an instance of klass.
58
+ # @returns [Enumerator(Node)] Returns an enumerator if no block given.
59
+ # @returns [self] Otherwise returns self.
44
60
  def filter(klass)
45
61
  return to_enum(:filter, klass) unless block_given?
46
62
 
47
63
  @children&.each do |child|
48
64
  yield child if child.is_a?(klass)
49
65
  end
66
+
67
+ return self
50
68
  end
51
69
 
52
70
  # Any lines of text associated with this node.
53
- # @returns [Array(String) | Nil] The lines of text.
71
+ # @returns [Array(String)?] The lines of text.
54
72
  def text
55
73
  if text = self.extract_text
56
74
  return text if text.any?
@@ -70,11 +88,13 @@ module Decode
70
88
 
71
89
  protected
72
90
 
91
+ # Extract text lines from Text children of this node.
92
+ # @returns [Array(String)?] Array of text lines, or nil if no children.
73
93
  def extract_text
74
94
  if children = @children
75
- @children.select{|child| child.kind_of?(Text)}.map(&:line)
76
- else
77
- nil
95
+ children.filter_map do |child|
96
+ child.line if child.is_a?(Text)
97
+ end
78
98
  end
79
99
  end
80
100
  end
@@ -9,7 +9,7 @@ module Decode
9
9
  module Comment
10
10
  # Describes a method option (keyword argument).
11
11
  #
12
- # - `@option :cached [Boolean] Whether to cache the value.`
12
+ # - `@option :cached [bool] Whether to cache the value.`
13
13
  #
14
14
  class Option < Parameter
15
15
  end
@@ -12,14 +12,18 @@ module Decode
12
12
  # - `@parameter age [Float] The users age.`
13
13
  #
14
14
  class Parameter < Tag
15
- PATTERN = /\A(?<name>.*?)\s+\[(?<type>.*?)\](\s+(?<details>.*?))?\Z/
15
+ # @constant [Regexp] Pattern for matching parameter declarations.
16
+ PATTERN = /\A(?<name>.*?)\s+\[#{Tag.bracketed_content(:type)}\](\s+(?<details>.*?))?\Z/
16
17
 
17
18
  # Build a parameter from a directive and regex match.
18
19
  # @parameter directive [String] The original directive text.
19
20
  # @parameter match [MatchData] The regex match data containing name, type, and details.
20
21
  # @returns [Parameter] A new parameter object.
21
22
  def self.build(directive, match)
22
- node = self.new(directive, match[:name], match[:type])
23
+ name = match[:name] or raise ArgumentError, "Missing name in parameter match!"
24
+ type = match[:type] or raise ArgumentError, "Missing type in parameter match!"
25
+
26
+ node = self.new(directive, name, type)
23
27
 
24
28
  if details = match[:details]
25
29
  node.add(Text.new(details))
@@ -34,10 +34,10 @@ module Decode
34
34
 
35
35
  # Initialize a new RBS pragma.
36
36
  # @parameter directive [String] The directive name.
37
- # @parameter text [String] The RBS type annotation text.
38
- def initialize(directive, text = nil)
37
+ # @parameter text [String?] The RBS type annotation text.
38
+ def initialize(directive, text)
39
39
  super(directive)
40
- @text = text&.strip
40
+ @text = text&.strip || ""
41
41
  end
42
42
 
43
43
  # The RBS type annotation text.
@@ -45,13 +45,13 @@ module Decode
45
45
  attr :text
46
46
 
47
47
  # Check if this is a generic type declaration.
48
- # @returns [Boolean] True if this is a generic declaration.
48
+ # @returns [bool] True if this is a generic declaration.
49
49
  def generic?
50
- @text&.start_with?("generic ")
50
+ @text.start_with?("generic ")
51
51
  end
52
52
 
53
53
  # Extract the generic type parameter name.
54
- # @returns [String | Nil] The generic type parameter name, or nil if not a generic.
54
+ # @returns [String?] The generic type parameter name, or nil if not a generic.
55
55
  def generic_parameter
56
56
  if generic?
57
57
  # Extract the parameter name from "generic T" or "generic T, U"
@@ -61,13 +61,13 @@ module Decode
61
61
  end
62
62
 
63
63
  # Check if this is a method type signature.
64
- # @returns [Boolean] True if this is a method signature.
64
+ # @returns [bool] True if this is a method signature.
65
65
  def method_signature?
66
66
  @text && !generic?
67
67
  end
68
68
 
69
69
  # Get the method type signature text.
70
- # @returns [String | Nil] The method signature text, or nil if not a method signature.
70
+ # @returns [String?] The method signature text, or nil if not a method signature.
71
71
  def method_signature
72
72
  method_signature? ? @text : nil
73
73
  end
@@ -8,7 +8,26 @@ require_relative "node"
8
8
  module Decode
9
9
  module Comment
10
10
  # Represents a documentation tag parsed from a comment directive.
11
+ # Subclasses should define a PATTERN constant for matching their specific syntax.
11
12
  class Tag < Node
13
+ # @constant [Regexp] Abstract pattern constant - subclasses must override this.
14
+ PATTERN = /(?<never_matches_anything>\A\z)/
15
+
16
+ # Abstract method: Build a tag from directive and match data.
17
+ # Subclasses must implement this method.
18
+ # @parameter directive [String] The directive that generated the tag.
19
+ # @parameter match [MatchData] The regex match data.
20
+ # @returns [Tag] A new tag instance.
21
+ def self.build(directive, match)
22
+ raise NotImplementedError, "Subclasses must implement build method"
23
+ end
24
+ # Build a pattern for bracketed content, supporting nested brackets.
25
+ # @parameter name [Symbol] The name of the group.
26
+ # @returns [String] The pattern.
27
+ def self.bracketed_content(name)
28
+ "(?<#{name}>(?:[^\\[\\]]+|\\[\\g<#{name}>\\])*)"
29
+ end
30
+
12
31
  # Match text against the tag pattern.
13
32
  # @parameter text [String] The text to match.
14
33
  def self.match(text)
@@ -10,7 +10,10 @@ module Decode
10
10
  # Represents a collection of documentation tags and their parsing logic.
11
11
  class Tags
12
12
  # Build a tags parser with directive mappings.
13
- # @parameter block [Proc] A block that yields the directives hash.
13
+ # @yields {|directives| directives['directive'] = Class}
14
+ # @parameter directives [Hash(String, _Directive)] The directive mappings hash to configure.
15
+ # @returns [Tags] A new tags parser with the configured directives.
16
+ # @rbs () { (Hash[String, Class]) -> void } -> Tags
14
17
  def self.build
15
18
  directives = Hash.new
16
19
 
@@ -20,7 +23,7 @@ module Decode
20
23
  end
21
24
 
22
25
  # Initialize a new tags parser.
23
- # @parameter directives [Hash(String, Class)] The directive mappings.
26
+ # @parameter directives [Hash(String, _Directive)] The directive mappings.
24
27
  def initialize(directives)
25
28
  @directives = directives
26
29
  end
@@ -32,12 +35,16 @@ module Decode
32
35
  line.start_with?(" " * level) || line.start_with?("\t" * level)
33
36
  end
34
37
 
38
+ # @constant [Regexp] Pattern for matching tag directives in comment lines.
35
39
  PATTERN = /\A\s*@(?<directive>.*?)(\s+(?<remainder>.*?))?\Z/
36
40
 
37
41
  # Parse documentation tags from lines.
38
42
  # @parameter lines [Array(String)] The lines to parse.
39
43
  # @parameter level [Integer] The indentation level.
40
- # @parameter block [Proc] A block to yield parsed tags to.
44
+ # @yields {|node| process parsed node}
45
+ # @parameter node [Node | Text] The parsed node (either a structured tag or plain text).
46
+ # @returns [void] Parses tags from lines and yields them to the block.
47
+ # @rbs (Array[String] lines, ?Integer level) { (Node | Text) -> void } -> void
41
48
  def parse(lines, level = 0, &block)
42
49
  while line = lines.first
43
50
  # Is it at the right indentation level:
@@ -48,9 +55,13 @@ module Decode
48
55
 
49
56
  # Match it against a tag:
50
57
  if match = PATTERN.match(line)
51
- if klass = @directives[match[:directive]]
58
+ directive = match[:directive] #: String
59
+ remainder = match[:remainder] #: String
60
+
61
+ # @type var klass: _Directive?
62
+ if klass = @directives[directive]
52
63
  yield klass.parse(
53
- match[:directive], match[:remainder],
64
+ directive, remainder,
54
65
  lines, self, level
55
66
  )
56
67
  else
@@ -15,6 +15,7 @@ module Decode
15
15
  @line = line
16
16
  end
17
17
 
18
+ # @attribute [String] The text content.
18
19
  attr :line
19
20
 
20
21
  # Traverse the text node.
@@ -13,13 +13,16 @@ module Decode
13
13
  #
14
14
  # Should contain nested parameters.
15
15
  class Yields < Tag
16
+ # @constant [Regexp] Pattern for matching yields declarations.
16
17
  PATTERN = /\A(?<block>{.*?})(\s+(?<details>.*?))?\Z/
17
18
 
18
19
  # Build a yields tag from a directive and match.
19
20
  # @parameter directive [String] The directive name.
20
21
  # @parameter match [MatchData] The regex match data.
21
22
  def self.build(directive, match)
22
- node = self.new(directive, match[:block])
23
+ block = match[:block] or raise "Missing block in yields match!"
24
+
25
+ node = self.new(directive, block)
23
26
 
24
27
  if details = match[:details]
25
28
  node.add(Text.new(details))
@@ -34,6 +37,7 @@ module Decode
34
37
  def initialize(directive, block)
35
38
  super(directive)
36
39
 
40
+ # @type ivar @block: String?
37
41
  @block = block
38
42
  end
39
43