sorbet_erb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +37 -0
- data/README.md +62 -0
- data/Rakefile +12 -0
- data/exe/sorbet_erb +6 -0
- data/lib/sorbet_erb/code_extractor.rb +67 -0
- data/lib/sorbet_erb/version.rb +5 -0
- data/lib/sorbet_erb.rb +136 -0
- data/lib/tapioca/dsl/compilers/view_component_slotables.rb +119 -0
- data/sig/sorbet_erb.rbs +4 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4076d96d59afeaf2c9f40b7c6cb176482b78ffc0b48e0d262cf71bdc4f49a8ce
|
4
|
+
data.tar.gz: c697a0a90d4f9526fdac049f722a802f9f09f5c4a30c3c9447db3e3d0a7503bd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1cfc74f6346706dee3bec11195b74ed0328f1e304f084d732b74b255eb8f6c8817aa41b10f9df8a0fa9b5c4d1384caab7285ac54b32ea69fed64e5a553abf4a8
|
7
|
+
data.tar.gz: f751e2301827f3f4b5a027689eb78e62b6adf821d3930688d6ad28a705bb43905a045d8d9cf971af603f5a93b7a3bf44bdb2c91eebf2a04f2f8a5389210ed762
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.0.0
|
3
|
+
|
4
|
+
Style/Documentation:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Style/StringLiterals:
|
8
|
+
Enabled: true
|
9
|
+
EnforcedStyle: single_quotes
|
10
|
+
|
11
|
+
Style/StringLiteralsInInterpolation:
|
12
|
+
Enabled: true
|
13
|
+
EnforcedStyle: double_quotes
|
14
|
+
|
15
|
+
Layout/LineLength:
|
16
|
+
Max: 120
|
17
|
+
|
18
|
+
Metrics/AbcSize:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Metrics/BlockLength:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/ClassLength:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/CyclomaticComplexity:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Metrics/MethodLength:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Metrics/ModuleLength:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Metrics/PerceivedComplexity:
|
37
|
+
Enabled: false
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# SorbetErb
|
2
|
+
|
3
|
+
`sorbet_erb` parses Rails ERB files and extracts the Ruby code so that
|
4
|
+
you can run Sorbet typechecking over them. This assumes you're already
|
5
|
+
using Sorbet and Tapioca together to generate RBI.
|
6
|
+
|
7
|
+
Currently this only supports Rails applications since it generates Ruby
|
8
|
+
scoped with a Rails `ApplicationController`. Feel free to file an issue
|
9
|
+
if you're interested in using this in other contexts.
|
10
|
+
|
11
|
+
### Limitations
|
12
|
+
|
13
|
+
- You must manually specify extra_includes that aren't covered by Tapioca
|
14
|
+
- Rails partials (files beginning with `_`) and Turbo streams must use
|
15
|
+
strict locals. sorbet_erb will skip them if there are no strict locals
|
16
|
+
defined unless you set `skip_missing_locals`.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
This gem isn't published to RubyGems yet, so you need to depend
|
21
|
+
directly on the git repository:
|
22
|
+
|
23
|
+
```
|
24
|
+
gem 'sorbet_erb', git: 'https://github.com/franklinhu/sorbet_erb'
|
25
|
+
```
|
26
|
+
|
27
|
+
After installing the gem, run `bundle binstubs sorbet_erb` to install
|
28
|
+
a helper script under `bin/sorbet_erb`.
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
```
|
33
|
+
bin/sorbet_erb input_dir output_dir
|
34
|
+
```
|
35
|
+
|
36
|
+
You'll most likely want to pass in your Rails app directory as input
|
37
|
+
and use `sorbet/erb` as your output directory. Don't forget to add
|
38
|
+
`sorbet/erb` to your `.gitignore`.
|
39
|
+
|
40
|
+
```
|
41
|
+
bin/sorbet_erb ./app ./sorbet/erb
|
42
|
+
```
|
43
|
+
|
44
|
+
### Sorbet signatures (experimental)
|
45
|
+
Running typechecking in Rails ERB templates is more helpful if there
|
46
|
+
are concrete types for the arguments.
|
47
|
+
|
48
|
+
sorbet_erb supports Sorbet-type signatures as a magic comment.
|
49
|
+
|
50
|
+
```
|
51
|
+
<%# locals_sig: sig { params(x: Integer).void } %>
|
52
|
+
```
|
53
|
+
|
54
|
+
## Development
|
55
|
+
|
56
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
57
|
+
|
58
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
59
|
+
|
60
|
+
## Contributing
|
61
|
+
|
62
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/franklinhu/sorbet_erb.
|
data/Rakefile
ADDED
data/exe/sorbet_erb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'better_html'
|
4
|
+
require 'better_html/parser'
|
5
|
+
|
6
|
+
module SorbetErb
|
7
|
+
class CodeExtractor
|
8
|
+
def initialize; end
|
9
|
+
|
10
|
+
def extract(input)
|
11
|
+
buffer = Parser::Source::Buffer.new('(buffer)')
|
12
|
+
buffer.source = input
|
13
|
+
parser = BetterHtml::Parser.new(buffer)
|
14
|
+
|
15
|
+
p = CodeProcessor.new
|
16
|
+
p.process(parser.ast)
|
17
|
+
[p.output, p.locals, p.locals_sig]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class CodeProcessor
|
22
|
+
include AST::Processor::Mixin
|
23
|
+
|
24
|
+
LOCALS_PREFIX = 'locals:'
|
25
|
+
LOCALS_SIG_PREFIX = 'locals_sig:'
|
26
|
+
|
27
|
+
attr_accessor :output, :locals, :locals_sig
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@output = []
|
31
|
+
@locals = nil
|
32
|
+
@locals_sig = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def handler_missing(node)
|
36
|
+
# Some children may be strings, so only look for AST nodes
|
37
|
+
children = node.children.select { |c| c.is_a?(BetterHtml::AST::Node) }
|
38
|
+
process_all(children)
|
39
|
+
end
|
40
|
+
|
41
|
+
def on_erb(node)
|
42
|
+
indicator_node = node.children.compact.find { |c| c.type == :indicator }
|
43
|
+
code_node = node.children.compact.find { |c| c.type == :code }
|
44
|
+
|
45
|
+
return process(code_node) if indicator_node.nil?
|
46
|
+
|
47
|
+
indicator = indicator_node.children.first
|
48
|
+
case indicator
|
49
|
+
when '#'
|
50
|
+
# Ignore comments if it's not strict locals
|
51
|
+
code_text = code_node.children.first.strip
|
52
|
+
if code_text.start_with?(LOCALS_PREFIX)
|
53
|
+
# No need to parse the locals
|
54
|
+
@locals = code_text.delete_prefix(LOCALS_PREFIX).strip
|
55
|
+
elsif code_text.start_with?(LOCALS_SIG_PREFIX)
|
56
|
+
@locals_sig = code_text.delete_prefix(LOCALS_SIG_PREFIX).strip
|
57
|
+
end
|
58
|
+
else
|
59
|
+
process_all(node.children)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_code(node)
|
64
|
+
@output += node.children
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/sorbet_erb.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'pathname'
|
6
|
+
require 'psych'
|
7
|
+
|
8
|
+
require_relative 'sorbet_erb/code_extractor'
|
9
|
+
require_relative 'sorbet_erb/version'
|
10
|
+
|
11
|
+
module SorbetErb
|
12
|
+
CONFIG_FILE_NAME = '.sorbet_erb.yml'
|
13
|
+
|
14
|
+
DEFAULT_CONFIG = {
|
15
|
+
'input_dirs' => ['app'],
|
16
|
+
'exclude_paths' => [],
|
17
|
+
'output_dir' => 'sorbet/erb',
|
18
|
+
'extra_includes' => [],
|
19
|
+
'extra_body' => '',
|
20
|
+
'skip_missing_locals' => true
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
USAGE = <<~USAGE
|
24
|
+
Usage: sorbet_erb input_dir output_dir
|
25
|
+
input_dir - where to scan for ERB files
|
26
|
+
output_dir - where to write files with Ruby extracted from ERB
|
27
|
+
USAGE
|
28
|
+
|
29
|
+
ERB_TEMPLATE = <<~ERB_TEMPLATE
|
30
|
+
# typed: true
|
31
|
+
class SorbetErb<%= class_suffix %> < ApplicationController
|
32
|
+
extend T::Sig
|
33
|
+
include ActionView::Helpers
|
34
|
+
include ApplicationController::HelperMethods
|
35
|
+
<% extra_includes.each do |i| %>
|
36
|
+
include <%= i %>
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
40
|
+
def local_assigns
|
41
|
+
# Shim for typechecking
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
|
45
|
+
<%= extra_body %>
|
46
|
+
|
47
|
+
<%= locals_sig %>
|
48
|
+
def body<%= locals %>
|
49
|
+
<% lines.each do |line| %>
|
50
|
+
<%= line %>
|
51
|
+
<% end %>
|
52
|
+
end
|
53
|
+
end
|
54
|
+
ERB_TEMPLATE
|
55
|
+
|
56
|
+
def self.extract_rb_from_erb(input_dir, output_dir)
|
57
|
+
config = read_config
|
58
|
+
input_dirs =
|
59
|
+
if input_dir
|
60
|
+
[input_dir]
|
61
|
+
else
|
62
|
+
config.fetch('input_dirs')
|
63
|
+
end
|
64
|
+
exclude_paths = config.fetch('exclude_paths')
|
65
|
+
output_dir ||= config.fetch('output_dir')
|
66
|
+
skip_missing_locals = config.fetch('skip_missing_locals')
|
67
|
+
|
68
|
+
puts 'Clearing output directory'
|
69
|
+
FileUtils.rm_rf(output_dir)
|
70
|
+
|
71
|
+
input_dir_to_paths = input_dirs.flat_map do |d|
|
72
|
+
Dir.glob(File.join(d, '**', '*.erb')).map do |p|
|
73
|
+
[d, p]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
input_dir_to_paths.each do |d, p|
|
77
|
+
pathname = Pathname.new(p)
|
78
|
+
|
79
|
+
next if exclude_paths.any? { |p| p.include?(pathname.to_s) }
|
80
|
+
|
81
|
+
extractor = CodeExtractor.new
|
82
|
+
lines, locals, locals_sig = extractor.extract(File.read(p))
|
83
|
+
|
84
|
+
# Partials and Turbo streams must use strict locals
|
85
|
+
next if requires_defined_locals(pathname.basename.to_s) && locals.nil? && skip_missing_locals
|
86
|
+
|
87
|
+
locals ||= '()'
|
88
|
+
locals_sig ||= ''
|
89
|
+
|
90
|
+
rel_output_dir = File.join(
|
91
|
+
output_dir,
|
92
|
+
pathname.dirname.relative_path_from(d)
|
93
|
+
)
|
94
|
+
FileUtils.mkdir_p(rel_output_dir)
|
95
|
+
|
96
|
+
output_path = File.join(
|
97
|
+
rel_output_dir,
|
98
|
+
"#{pathname.basename}.generated.rb"
|
99
|
+
)
|
100
|
+
erb = ERB.new(ERB_TEMPLATE)
|
101
|
+
File.open(output_path, 'w') do |f|
|
102
|
+
result = erb.result_with_hash(
|
103
|
+
class_suffix: SecureRandom.hex(6),
|
104
|
+
locals: locals,
|
105
|
+
locals_sig: locals_sig,
|
106
|
+
extra_includes: config.fetch('extra_includes'),
|
107
|
+
extra_body: config.fetch('extra_body'),
|
108
|
+
lines: lines
|
109
|
+
)
|
110
|
+
f.write(result)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.read_config
|
116
|
+
path = File.join(Dir.pwd, CONFIG_FILE_NAME)
|
117
|
+
config =
|
118
|
+
if File.exist?(path)
|
119
|
+
Psych.safe_load_file(path)
|
120
|
+
else
|
121
|
+
{}
|
122
|
+
end
|
123
|
+
DEFAULT_CONFIG.merge(config)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.requires_defined_locals(file_name)
|
127
|
+
file_name.start_with?('_') || file_name.end_with?('.turbo_stream.erb')
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.start(argv)
|
131
|
+
input = argv[0]
|
132
|
+
output = argv[1]
|
133
|
+
|
134
|
+
SorbetErb.extract_rb_from_erb(input, output)
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
return unless defined?(ViewComponent)
|
5
|
+
|
6
|
+
require 'sorbet-runtime'
|
7
|
+
require 'tapioca/dsl'
|
8
|
+
|
9
|
+
module Tapioca
|
10
|
+
module Dsl
|
11
|
+
module Compilers
|
12
|
+
# Generates RBI for ViewComponent::Slotable
|
13
|
+
# See https://github.com/ViewComponent/view_component/blob/main/lib/view_component/slotable.rb
|
14
|
+
class ViewComponentSlotables < Tapioca::Dsl::Compiler
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
ConstantType = type_member { { fixed: T.class_of(::ViewComponent::Slotable) } }
|
18
|
+
|
19
|
+
class << self
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
sig { override.returns(T::Enumerable[Module]) }
|
23
|
+
def gather_constants
|
24
|
+
all_classes
|
25
|
+
.select { |c| c < ViewComponent::Slotable && c.name != 'ViewComponent::Base' }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { override.void }
|
30
|
+
def decorate
|
31
|
+
root.create_path(constant) do |klass|
|
32
|
+
T.unsafe(constant).registered_slots.each do |name, config|
|
33
|
+
renderable_type = config[:renderable]
|
34
|
+
renderable = T.let(
|
35
|
+
case renderable_type
|
36
|
+
when String
|
37
|
+
renderable_type
|
38
|
+
when Class
|
39
|
+
T.must(renderable_type.name)
|
40
|
+
else
|
41
|
+
'T.untyped'
|
42
|
+
end,
|
43
|
+
String
|
44
|
+
)
|
45
|
+
|
46
|
+
is_many = T.let(config[:collection], T::Boolean)
|
47
|
+
return_type = renderable
|
48
|
+
|
49
|
+
module_name = 'ViewComponentSlotablesMethodsModule'
|
50
|
+
klass.create_module(module_name) do |mod|
|
51
|
+
generate_instance_methods(mod, name.to_s, return_type, is_many)
|
52
|
+
end
|
53
|
+
klass.create_include(module_name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { params(klass: RBI::Scope, name: String, return_type: String, is_many: T::Boolean).void }
|
59
|
+
def generate_instance_methods(klass, name, return_type, is_many)
|
60
|
+
return_type_maybe_plural =
|
61
|
+
if is_many
|
62
|
+
"T::Enumerable[#{return_type}]"
|
63
|
+
else
|
64
|
+
return_type
|
65
|
+
end
|
66
|
+
|
67
|
+
klass.create_method(name, return_type: return_type_maybe_plural)
|
68
|
+
klass.create_method("#{name}?", return_type: 'T::Boolean')
|
69
|
+
|
70
|
+
klass.create_method(
|
71
|
+
"with_#{name}",
|
72
|
+
parameters: [
|
73
|
+
create_rest_param('args', type: 'T.untyped'),
|
74
|
+
create_block_param(
|
75
|
+
'block',
|
76
|
+
type: "T.nilable(T.proc.params(#{name}: #{return_type_maybe_plural}).returns(T.untyped))"
|
77
|
+
)
|
78
|
+
],
|
79
|
+
return_type: return_type_maybe_plural
|
80
|
+
)
|
81
|
+
|
82
|
+
if is_many
|
83
|
+
# For collection subcomponents, ViewComponent generates methods for the singular version
|
84
|
+
# of the name.
|
85
|
+
singular_name = ActiveSupport::Inflector.singularize(name)
|
86
|
+
|
87
|
+
klass.create_method(
|
88
|
+
"with_#{singular_name}",
|
89
|
+
parameters: [
|
90
|
+
create_rest_param('args', type: 'T.untyped'),
|
91
|
+
create_block_param(
|
92
|
+
'block',
|
93
|
+
type: "T.nilable(T.proc.params(#{singular_name}: #{return_type}).returns(T.untyped))"
|
94
|
+
)
|
95
|
+
],
|
96
|
+
return_type: return_type
|
97
|
+
)
|
98
|
+
klass.create_method(
|
99
|
+
"with_#{singular_name}_content",
|
100
|
+
parameters: [
|
101
|
+
create_param('content', type: 'T.untyped')
|
102
|
+
],
|
103
|
+
return_type: 'T.untyped'
|
104
|
+
)
|
105
|
+
|
106
|
+
else
|
107
|
+
klass.create_method(
|
108
|
+
"with_#{name}_content",
|
109
|
+
parameters: [
|
110
|
+
create_param('content', type: 'T.untyped')
|
111
|
+
],
|
112
|
+
return_type: return_type
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/sig/sorbet_erb.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sorbet_erb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Franklin Hu
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: better_html
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: psych
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tapioca
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.16.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.16.1
|
55
|
+
description: Extracts Ruby code from ERB files so you can run Sorbet over them
|
56
|
+
email:
|
57
|
+
- franklin@thisisfranklin.com
|
58
|
+
executables:
|
59
|
+
- sorbet_erb
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".rubocop.yml"
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- exe/sorbet_erb
|
67
|
+
- lib/sorbet_erb.rb
|
68
|
+
- lib/sorbet_erb/code_extractor.rb
|
69
|
+
- lib/sorbet_erb/version.rb
|
70
|
+
- lib/tapioca/dsl/compilers/view_component_slotables.rb
|
71
|
+
- sig/sorbet_erb.rbs
|
72
|
+
homepage: https://github.com/franklinhu/sorbet_erb
|
73
|
+
licenses: []
|
74
|
+
metadata:
|
75
|
+
allowed_push_host: https://rubygems.org
|
76
|
+
homepage_uri: https://github.com/franklinhu/sorbet_erb
|
77
|
+
source_code_uri: https://github.com/franklinhu/sorbet_erb
|
78
|
+
changelog_uri: https://github.com/franklinhu/sorbet_erb
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 3.0.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.5.16
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Extracts Ruby code from ERB files for Sorbet
|
98
|
+
test_files: []
|