tapioca 0.4.27 → 0.5.3

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +15 -15
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +172 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
  13. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  14. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  15. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  16. data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
  17. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  18. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  19. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  20. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  21. data/lib/tapioca/compilers/dsl/active_support_concern.rb +106 -0
  22. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  23. data/lib/tapioca/compilers/dsl/base.rb +108 -82
  24. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  25. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  26. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  27. data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
  28. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  29. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  30. data/lib/tapioca/compilers/dsl/smart_properties.rb +21 -33
  31. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  32. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  33. data/lib/tapioca/compilers/dsl_compiler.rb +25 -40
  34. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
  35. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  36. data/lib/tapioca/compilers/sorbet.rb +25 -5
  37. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +122 -206
  38. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  39. data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
  40. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  41. data/lib/tapioca/config.rb +3 -0
  42. data/lib/tapioca/config_builder.rb +5 -2
  43. data/lib/tapioca/constant_locator.rb +6 -8
  44. data/lib/tapioca/gemfile.rb +14 -11
  45. data/lib/tapioca/generators/base.rb +61 -0
  46. data/lib/tapioca/generators/dsl.rb +362 -0
  47. data/lib/tapioca/generators/gem.rb +345 -0
  48. data/lib/tapioca/generators/init.rb +79 -0
  49. data/lib/tapioca/generators/require.rb +52 -0
  50. data/lib/tapioca/generators/todo.rb +76 -0
  51. data/lib/tapioca/generators.rb +9 -0
  52. data/lib/tapioca/generic_type_registry.rb +25 -98
  53. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  54. data/lib/tapioca/internal.rb +2 -10
  55. data/lib/tapioca/loader.rb +11 -31
  56. data/lib/tapioca/rbi_ext/model.rb +166 -0
  57. data/lib/tapioca/reflection.rb +138 -0
  58. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  59. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  60. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  61. data/lib/tapioca/version.rb +1 -1
  62. data/lib/tapioca.rb +3 -0
  63. metadata +45 -23
  64. data/lib/tapioca/cli/main.rb +0 -146
  65. data/lib/tapioca/core_ext/class.rb +0 -28
  66. data/lib/tapioca/core_ext/string.rb +0 -18
  67. data/lib/tapioca/generator.rb +0 -633
  68. data/lib/tapioca/rbi/model.rb +0 -405
  69. data/lib/tapioca/rbi/printer.rb +0 -410
  70. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  71. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  72. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  73. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  74. data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "parlour"
5
-
6
4
  begin
7
5
  require "rails"
8
6
  require "action_controller"
@@ -89,13 +87,13 @@ module Tapioca
89
87
  class UrlHelpers < Base
90
88
  extend T::Sig
91
89
 
92
- sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
90
+ sig { override.params(root: RBI::Tree, constant: Module).void }
93
91
  def decorate(root, constant)
94
92
  case constant
95
93
  when GeneratedPathHelpersModule.singleton_class, GeneratedUrlHelpersModule.singleton_class
96
94
  generate_module_for(root, constant)
97
95
  else
98
- root.path(constant) do |mod|
96
+ root.create_path(constant) do |mod|
99
97
  create_mixins_for(mod, constant, GeneratedUrlHelpersModule)
100
98
  create_mixins_for(mod, constant, GeneratedPathHelpersModule)
101
99
  end
@@ -112,9 +110,8 @@ module Tapioca
112
110
  Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module)
113
111
  Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module)
114
112
 
115
- module_enumerator = T.cast(ObjectSpace.each_object(Module), T::Enumerator[Module])
116
- constants = module_enumerator.select do |mod|
117
- next unless Module.instance_method(:name).bind(mod).call
113
+ constants = all_modules.select do |mod|
114
+ next unless name_of(mod)
118
115
 
119
116
  includes_helper?(mod, GeneratedUrlHelpersModule) ||
120
117
  includes_helper?(mod, GeneratedPathHelpersModule) ||
@@ -127,7 +124,7 @@ module Tapioca
127
124
 
128
125
  private
129
126
 
130
- sig { params(root: Parlour::RbiGenerator::Namespace, constant: Module).void }
127
+ sig { params(root: RBI::Tree, constant: Module).void }
131
128
  def generate_module_for(root, constant)
132
129
  root.create_module(T.must(constant.name)) do |mod|
133
130
  mod.create_include("::ActionDispatch::Routing::UrlFor")
@@ -136,14 +133,14 @@ module Tapioca
136
133
  constant.instance_methods(false).each do |method|
137
134
  mod.create_method(
138
135
  method.to_s,
139
- parameters: [Parlour::RbiGenerator::Parameter.new("*args", type: "T.untyped")],
136
+ parameters: [create_rest_param("args", type: "T.untyped")],
140
137
  return_type: "String"
141
138
  )
142
139
  end
143
140
  end
144
141
  end
145
142
 
146
- sig { params(mod: Parlour::RbiGenerator::Namespace, constant: Module, helper_module: Module).void }
143
+ sig { params(mod: RBI::Scope, constant: Module, helper_module: Module).void }
147
144
  def create_mixins_for(mod, constant, helper_module)
148
145
  include_helper = constant.ancestors.include?(helper_module) || NON_DISCOVERABLE_INCLUDERS.include?(constant)
149
146
  extend_helper = constant.singleton_class.ancestors.include?(helper_module)
@@ -20,20 +20,21 @@ module Tapioca
20
20
  sig do
21
21
  params(
22
22
  requested_constants: T::Array[Module],
23
- requested_generators: T::Array[String],
23
+ requested_generators: T::Array[T.class_of(Dsl::Base)],
24
+ excluded_generators: T::Array[T.class_of(Dsl::Base)],
24
25
  error_handler: T.nilable(T.proc.params(error: String).void)
25
26
  ).void
26
27
  end
27
- def initialize(requested_constants:, requested_generators: [], error_handler: nil)
28
+ def initialize(requested_constants:, requested_generators: [], excluded_generators: [], error_handler: nil)
28
29
  @generators = T.let(
29
- gather_generators(requested_generators),
30
+ gather_generators(requested_generators, excluded_generators),
30
31
  T::Enumerable[Dsl::Base]
31
32
  )
32
33
  @requested_constants = requested_constants
33
34
  @error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
34
35
  end
35
36
 
36
- sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
37
+ sig { params(blk: T.proc.params(constant: Module, rbi: RBI::File).void).void }
37
38
  def run(&blk)
38
39
  constants_to_process = gather_constants(requested_constants)
39
40
 
@@ -50,27 +51,27 @@ module Tapioca
50
51
 
51
52
  blk.call(constant, rbi)
52
53
  end
54
+
55
+ generators.flat_map(&:errors).each do |msg|
56
+ report_error(msg)
57
+ end
53
58
  end
54
59
 
55
60
  private
56
61
 
57
- sig { params(requested_generators: T::Array[String]).returns(T.proc.params(klass: Class).returns(T::Boolean)) }
58
- def generator_filter(requested_generators)
59
- return ->(_klass) { true } if requested_generators.empty?
60
-
61
- generators = requested_generators.map(&:downcase)
62
-
63
- proc do |klass|
64
- generator = klass.name&.sub(/^Tapioca::Compilers::Dsl::/, '')&.downcase
65
- generators.include?(generator)
66
- end
62
+ sig do
63
+ params(
64
+ requested_generators: T::Array[T.class_of(Dsl::Base)],
65
+ excluded_generators: T::Array[T.class_of(Dsl::Base)]
66
+ ).returns(T::Enumerable[Dsl::Base])
67
67
  end
68
+ def gather_generators(requested_generators, excluded_generators)
69
+ generator_klasses = ::Tapioca::Reflection.descendants_of(Dsl::Base).select do |klass|
70
+ (requested_generators.empty? || requested_generators.include?(klass)) &&
71
+ !excluded_generators.include?(klass)
72
+ end.sort_by { |klass| T.must(klass.name) }
68
73
 
69
- sig { params(requested_generators: T::Array[String]).returns(T::Enumerable[Dsl::Base]) }
70
- def gather_generators(requested_generators)
71
- generator_filter = generator_filter(requested_generators)
72
-
73
- T.cast(Dsl::Base.descendants.select(&generator_filter).map(&:new), T::Enumerable[Dsl::Base])
74
+ generator_klasses.map(&:new)
74
75
  end
75
76
 
76
77
  sig { params(requested_constants: T::Array[Module]).returns(T::Set[Module]) }
@@ -80,34 +81,18 @@ module Tapioca
80
81
  constants
81
82
  end
82
83
 
83
- sig { params(constant: Module).returns(T.nilable(String)) }
84
+ sig { params(constant: Module).returns(T.nilable(RBI::File)) }
84
85
  def rbi_for_constant(constant)
85
- parlour = Parlour::RbiGenerator.new(sort_namespaces: true)
86
+ file = RBI::File.new(strictness: "true")
86
87
 
87
88
  generators.each do |generator|
88
89
  next unless generator.handles?(constant)
89
- generator.decorate(parlour.root, constant)
90
+ generator.decorate(file.root, constant)
90
91
  end
91
92
 
92
- return if parlour.root.children.empty?
93
-
94
- resolve_conflicts(parlour)
93
+ return if file.root.empty?
95
94
 
96
- parlour.rbi("true").strip
97
- end
98
-
99
- sig { params(parlour: Parlour::RbiGenerator).void }
100
- def resolve_conflicts(parlour)
101
- Parlour::ConflictResolver.new.resolve_conflicts(parlour.root) do |msg, candidates|
102
- error = StringIO.new
103
- error.puts "=== Error ==="
104
- error.puts msg
105
- error.puts "# Candidates"
106
- candidates.each_with_index do |candidate, index|
107
- error.puts " #{index}. #{candidate.describe}"
108
- end
109
- report_error(error.string)
110
- end
95
+ file
111
96
  end
112
97
 
113
98
  sig { params(error: String).returns(T.noreturn) }
@@ -0,0 +1,198 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class DynamicMixinCompiler
5
+ extend T::Sig
6
+ include Tapioca::Reflection
7
+
8
+ sig { returns(T::Array[Module]) }
9
+ attr_reader :dynamic_extends, :dynamic_includes
10
+
11
+ sig { returns(T::Array[Symbol]) }
12
+ attr_reader :class_attribute_readers, :class_attribute_writers, :class_attribute_predicates
13
+
14
+ sig { returns(T::Array[Symbol]) }
15
+ attr_reader :instance_attribute_readers, :instance_attribute_writers, :instance_attribute_predicates
16
+
17
+ sig { params(constant: Module).void }
18
+ def initialize(constant)
19
+ @constant = constant
20
+ mixins_from_modules = {}.compare_by_identity
21
+ class_attribute_readers = T.let([], T::Array[Symbol])
22
+ class_attribute_writers = T.let([], T::Array[Symbol])
23
+ class_attribute_predicates = T.let([], T::Array[Symbol])
24
+
25
+ instance_attribute_readers = T.let([], T::Array[Symbol])
26
+ instance_attribute_writers = T.let([], T::Array[Symbol])
27
+ instance_attribute_predicates = T.let([], T::Array[Symbol])
28
+
29
+ Class.new do
30
+ # Override the `self.include` method
31
+ define_singleton_method(:include) do |mod|
32
+ # Take a snapshot of the list of singleton class ancestors
33
+ # before the actual include
34
+ before = singleton_class.ancestors
35
+ # Call the actual `include` method with the supplied module
36
+ super(mod).tap do
37
+ # Take a snapshot of the list of singleton class ancestors
38
+ # after the actual include
39
+ after = singleton_class.ancestors
40
+ # The difference is the modules that are added to the list
41
+ # of ancestors of the singleton class. Those are all the
42
+ # modules that were `extend`ed due to the `include` call.
43
+ #
44
+ # We record those modules on our lookup table keyed by
45
+ # the included module with the values being all the modules
46
+ # that that module pulls into the singleton class.
47
+ #
48
+ # We need to reverse the order, since the extend order should
49
+ # be the inverse of the ancestor order. That is, earlier
50
+ # extended modules would be later in the ancestor chain.
51
+ mixins_from_modules[mod] = (after - before).reverse!
52
+ end
53
+ rescue Exception # rubocop:disable Lint/RescueException
54
+ # this is a best effort, bail if we can't perform this
55
+ end
56
+
57
+ define_singleton_method(:class_attribute) do |*attrs, **kwargs|
58
+ class_attribute_readers.concat(attrs)
59
+ class_attribute_writers.concat(attrs)
60
+
61
+ instance_predicate = kwargs.fetch(:instance_predicate, true)
62
+ instance_accessor = kwargs.fetch(:instance_accessor, true)
63
+ instance_reader = kwargs.fetch(:instance_reader, instance_accessor)
64
+ instance_writer = kwargs.fetch(:instance_writer, instance_accessor)
65
+
66
+ if instance_reader
67
+ instance_attribute_readers.concat(attrs)
68
+ end
69
+
70
+ if instance_writer
71
+ instance_attribute_writers.concat(attrs)
72
+ end
73
+
74
+ if instance_predicate
75
+ class_attribute_predicates.concat(attrs)
76
+
77
+ if instance_reader
78
+ instance_attribute_predicates.concat(attrs)
79
+ end
80
+ end
81
+
82
+ super(*attrs, **kwargs) if defined?(super)
83
+ end
84
+
85
+ # rubocop:disable Style/MissingRespondToMissing
86
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
87
+ def method_missing(symbol, *args)
88
+ # We need this here so that we can handle any random instance
89
+ # method calls on the fake including class that may be done by
90
+ # the included module during the `self.included` hook.
91
+ end
92
+
93
+ class << self
94
+ extend T::Sig
95
+
96
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
97
+ def method_missing(symbol, *args)
98
+ # Similarly, we need this here so that we can handle any
99
+ # random class method calls on the fake including class
100
+ # that may be done by the included module during the
101
+ # `self.included` hook.
102
+ end
103
+ end
104
+ # rubocop:enable Style/MissingRespondToMissing
105
+ end.include(constant)
106
+
107
+ # The value that corresponds to the original included constant
108
+ # is the list of all dynamically extended modules because of that
109
+ # constant. We grab that value by deleting the key for the original
110
+ # constant.
111
+ @dynamic_extends = T.let(mixins_from_modules.delete(constant) || [], T::Array[Module])
112
+
113
+ # Since we deleted the original constant from the list of keys, all
114
+ # the keys that remain are the ones that are dynamically included modules
115
+ # during the include of the original constant.
116
+ @dynamic_includes = T.let(mixins_from_modules.keys, T::Array[Module])
117
+
118
+ @class_attribute_readers = T.let(class_attribute_readers, T::Array[Symbol])
119
+ @class_attribute_writers = T.let(class_attribute_writers, T::Array[Symbol])
120
+ @class_attribute_predicates = T.let(class_attribute_predicates, T::Array[Symbol])
121
+
122
+ @instance_attribute_readers = T.let(instance_attribute_readers, T::Array[Symbol])
123
+ @instance_attribute_writers = T.let(instance_attribute_writers, T::Array[Symbol])
124
+ @instance_attribute_predicates = T.let(instance_attribute_predicates, T::Array[Symbol])
125
+ end
126
+
127
+ sig { returns(T::Boolean) }
128
+ def empty_attributes?
129
+ @class_attribute_readers.empty? && @class_attribute_writers.empty?
130
+ end
131
+
132
+ sig { params(tree: RBI::Tree).void }
133
+ def compile_class_attributes(tree)
134
+ return if empty_attributes?
135
+
136
+ # Create a synthetic module to hold the generated class methods
137
+ tree << RBI::Module.new("GeneratedClassMethods") do |mod|
138
+ class_attribute_readers.each do |attribute|
139
+ mod << RBI::Method.new(attribute.to_s)
140
+ end
141
+
142
+ class_attribute_writers.each do |attribute|
143
+ mod << RBI::Method.new("#{attribute}=") do |method|
144
+ method << RBI::Param.new("value")
145
+ end
146
+ end
147
+
148
+ class_attribute_predicates.each do |attribute|
149
+ mod << RBI::Method.new("#{attribute}?")
150
+ end
151
+ end
152
+
153
+ # Create a synthetic module to hold the generated instance methods
154
+ tree << RBI::Module.new("GeneratedInstanceMethods") do |mod|
155
+ instance_attribute_readers.each do |attribute|
156
+ mod << RBI::Method.new(attribute.to_s)
157
+ end
158
+
159
+ instance_attribute_writers.each do |attribute|
160
+ mod << RBI::Method.new("#{attribute}=") do |method|
161
+ method << RBI::Param.new("value")
162
+ end
163
+ end
164
+
165
+ instance_attribute_predicates.each do |attribute|
166
+ mod << RBI::Method.new("#{attribute}?")
167
+ end
168
+ end
169
+
170
+ # Add a mixes_in_class_methods and include for the generated modules
171
+ tree << RBI::MixesInClassMethods.new("GeneratedClassMethods")
172
+ tree << RBI::Include.new("GeneratedInstanceMethods")
173
+ end
174
+
175
+ sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
176
+ def compile_mixes_in_class_methods(tree)
177
+ includes = dynamic_includes.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
178
+ includes.each do |mod|
179
+ qname = qualified_name_of(mod)
180
+ tree << RBI::Include.new(T.must(qname))
181
+ end
182
+
183
+ # If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
184
+ # constant itself
185
+ mixed_in_class_methods = dynamic_extends.select { |mod| mod != @constant }
186
+ return [[], []] if mixed_in_class_methods.empty?
187
+
188
+ mixed_in_class_methods.each do |mod|
189
+ qualified_name = qualified_name_of(mod)
190
+ next if qualified_name.nil? || qualified_name.empty?
191
+ tree << RBI::MixesInClassMethods.new(qualified_name)
192
+ end
193
+
194
+ [mixed_in_class_methods, includes]
195
+ rescue
196
+ [[], []] # silence errors
197
+ end
198
+ end
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'spoom'
4
+ require "spoom"
5
5
 
6
6
  module Tapioca
7
7
  module Compilers
@@ -87,7 +87,7 @@ module Tapioca
87
87
  sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
88
88
  def name_in_project?(files, name)
89
89
  files.any? do |file|
90
- File.basename(file, '.rb') == name
90
+ File.basename(file, ".rb") == name
91
91
  end
92
92
  end
93
93
  end
@@ -1,15 +1,25 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'pathname'
5
- require 'shellwords'
4
+ require "pathname"
5
+ require "shellwords"
6
6
 
7
7
  module Tapioca
8
8
  module Compilers
9
9
  module Sorbet
10
- SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
10
+ SORBET_GEM_SPEC = T.let(
11
+ Gem::Specification.find_by_name("sorbet-static"),
12
+ Gem::Specification
13
+ )
14
+ SORBET = T.let(
15
+ Pathname.new(SORBET_GEM_SPEC.full_gem_path) / "libexec" / "sorbet",
16
+ Pathname
17
+ )
11
18
  EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
12
19
 
20
+ FEATURE_REQUIREMENTS = T.let({
21
+ }.freeze, T::Hash[Symbol, Gem::Requirement])
22
+
13
23
  class << self
14
24
  extend(T::Sig)
15
25
 
@@ -20,7 +30,7 @@ module Tapioca
20
30
  sorbet_path,
21
31
  "--quiet",
22
32
  *args,
23
- ].join(' '),
33
+ ].join(" "),
24
34
  err: "/dev/null"
25
35
  ).read
26
36
  end
@@ -31,6 +41,16 @@ module Tapioca
31
41
  sorbet_path = SORBET if sorbet_path.empty?
32
42
  sorbet_path.to_s.shellescape
33
43
  end
44
+
45
+ sig { params(feature: Symbol, version: T.nilable(Gem::Version)).returns(T::Boolean) }
46
+ def supports?(feature, version: nil)
47
+ version = SORBET_GEM_SPEC.version unless version
48
+ requirement = FEATURE_REQUIREMENTS[feature]
49
+
50
+ raise "Invalid Sorbet feature #{feature}" unless requirement
51
+
52
+ requirement.satisfied_by?(version)
53
+ end
34
54
  end
35
55
  end
36
56
  end