tapioca 0.6.1 → 0.7.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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +13 -2
  3. data/README.md +79 -25
  4. data/Rakefile +10 -14
  5. data/lib/tapioca/cli.rb +66 -80
  6. data/lib/tapioca/{generators/base.rb → commands/command.rb} +17 -10
  7. data/lib/tapioca/{generators → commands}/dsl.rb +59 -45
  8. data/lib/tapioca/{generators → commands}/gem.rb +93 -30
  9. data/lib/tapioca/{generators → commands}/init.rb +9 -13
  10. data/lib/tapioca/{generators → commands}/require.rb +8 -10
  11. data/lib/tapioca/commands/todo.rb +84 -0
  12. data/lib/tapioca/commands.rb +13 -0
  13. data/lib/tapioca/dsl/compiler.rb +185 -0
  14. data/lib/tapioca/{compilers/dsl → dsl/compilers}/aasm.rb +12 -9
  15. data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_controller_helpers.rb +13 -20
  16. data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_mailer.rb +10 -8
  17. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_job.rb +11 -9
  18. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_attributes.rb +32 -24
  19. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_secure_password.rb +10 -12
  20. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_associations.rb +29 -35
  21. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +26 -24
  22. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_enum.rb +14 -12
  23. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_fixtures.rb +10 -8
  24. data/lib/tapioca/dsl/compilers/active_record_relations.rb +712 -0
  25. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_scope.rb +21 -20
  26. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_typed_store.rb +12 -17
  27. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
  28. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +11 -11
  29. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_concern.rb +19 -14
  30. data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_current_attributes.rb +16 -21
  31. data/lib/tapioca/{compilers/dsl → dsl/compilers}/config.rb +10 -8
  32. data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
  33. data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +28 -25
  34. data/lib/tapioca/{compilers/dsl → dsl/compilers}/mixed_in_class_attributes.rb +12 -10
  35. data/lib/tapioca/{compilers/dsl → dsl/compilers}/protobuf.rb +10 -8
  36. data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +13 -14
  37. data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
  38. data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +12 -13
  39. data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
  40. data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +16 -14
  41. data/lib/tapioca/dsl/compilers.rb +31 -0
  42. data/lib/tapioca/{compilers/dsl → dsl}/extensions/frozen_record.rb +2 -2
  43. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +114 -0
  44. data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +29 -0
  45. data/lib/tapioca/{compilers/dsl → dsl/helpers}/param_helper.rb +2 -2
  46. data/lib/tapioca/{compilers/dsl_compiler.rb → dsl/pipeline.rb} +41 -33
  47. data/lib/tapioca/gem/events.rb +120 -0
  48. data/lib/tapioca/gem/listeners/base.rb +48 -0
  49. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +32 -0
  50. data/lib/tapioca/gem/listeners/methods.rb +183 -0
  51. data/lib/tapioca/gem/listeners/mixins.rb +101 -0
  52. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +21 -0
  53. data/lib/tapioca/gem/listeners/sorbet_enums.rb +26 -0
  54. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +29 -0
  55. data/lib/tapioca/gem/listeners/sorbet_props.rb +33 -0
  56. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +23 -0
  57. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +79 -0
  58. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +51 -0
  59. data/lib/tapioca/gem/listeners/subconstants.rb +37 -0
  60. data/lib/tapioca/gem/listeners/yard_doc.rb +96 -0
  61. data/lib/tapioca/gem/listeners.rb +16 -0
  62. data/lib/tapioca/gem/pipeline.rb +365 -0
  63. data/lib/tapioca/gemfile.rb +44 -20
  64. data/lib/tapioca/helpers/cli_helper.rb +16 -8
  65. data/lib/tapioca/helpers/config_helper.rb +113 -0
  66. data/lib/tapioca/helpers/rbi_helper.rb +17 -0
  67. data/lib/tapioca/helpers/shims_helper.rb +87 -0
  68. data/lib/tapioca/helpers/sorbet_helper.rb +57 -0
  69. data/lib/tapioca/helpers/test/dsl_compiler.rb +118 -0
  70. data/lib/tapioca/helpers/test/isolation.rb +1 -1
  71. data/lib/tapioca/helpers/test/template.rb +13 -2
  72. data/lib/tapioca/internal.rb +17 -10
  73. data/lib/tapioca/rbi_ext/model.rb +2 -48
  74. data/lib/tapioca/rbi_formatter.rb +37 -0
  75. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +227 -0
  76. data/lib/tapioca/runtime/generic_type_registry.rb +166 -0
  77. data/lib/tapioca/runtime/loader.rb +123 -0
  78. data/lib/tapioca/runtime/reflection.rb +153 -0
  79. data/lib/tapioca/runtime/trackers/autoload.rb +72 -0
  80. data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -0
  81. data/lib/tapioca/runtime/trackers/mixin.rb +80 -0
  82. data/lib/tapioca/runtime/trackers/required_ancestor.rb +50 -0
  83. data/lib/tapioca/{trackers.rb → runtime/trackers.rb} +4 -3
  84. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +110 -54
  85. data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
  86. data/lib/tapioca/{compilers → static}/requires_compiler.rb +5 -12
  87. data/lib/tapioca/static/symbol_loader.rb +83 -0
  88. data/lib/tapioca/static/symbol_table_parser.rb +63 -0
  89. data/lib/tapioca/version.rb +1 -1
  90. data/lib/tapioca.rb +2 -7
  91. metadata +82 -62
  92. data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -711
  93. data/lib/tapioca/compilers/dsl/base.rb +0 -179
  94. data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
  95. data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -198
  96. data/lib/tapioca/compilers/sorbet.rb +0 -59
  97. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +0 -780
  98. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +0 -90
  99. data/lib/tapioca/compilers/symbol_table_compiler.rb +0 -17
  100. data/lib/tapioca/compilers/todos_compiler.rb +0 -32
  101. data/lib/tapioca/generators/todo.rb +0 -76
  102. data/lib/tapioca/generators.rb +0 -9
  103. data/lib/tapioca/generic_type_registry.rb +0 -149
  104. data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -98
  105. data/lib/tapioca/loader.rb +0 -119
  106. data/lib/tapioca/reflection.rb +0 -151
  107. data/lib/tapioca/trackers/autoload.rb +0 -70
  108. data/lib/tapioca/trackers/constant_definition.rb +0 -42
  109. data/lib/tapioca/trackers/mixin.rb +0 -78
@@ -0,0 +1,87 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module ShimsHelper
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ requires_ancestor { Thor::Shell }
10
+
11
+ sig { params(index: RBI::Index, kind: String, dir: String).void }
12
+ def index_rbis(index, kind, dir)
13
+ return unless Dir.exist?(dir) && !Dir.empty?(dir)
14
+
15
+ say("Loading #{kind} RBIs from #{dir}... ")
16
+ files = Dir.glob("#{dir}/**/*.rbi").sort
17
+
18
+ trees = files.map do |file|
19
+ RBI::Parser.parse_file(file)
20
+ rescue RBI::ParseError => e
21
+ say_error("\nWarning: #{e} (#{e.location})", :yellow)
22
+ end.compact
23
+
24
+ index.visit_all(trees)
25
+ say(" Done", :green)
26
+ end
27
+
28
+ sig { params(index: RBI::Index, shim_rbi_dir: String).returns(T::Hash[String, T::Array[RBI::Node]]) }
29
+ def duplicated_nodes_from_index(index, shim_rbi_dir)
30
+ duplicates = {}
31
+ say("Looking for duplicates... ")
32
+ index.keys.each do |key|
33
+ nodes = index[key]
34
+ next unless shims_have_duplicates?(nodes, shim_rbi_dir)
35
+ duplicates[key] = nodes
36
+ end
37
+ say(" Done", :green)
38
+ duplicates
39
+ end
40
+
41
+ private
42
+
43
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String).returns(T::Boolean) }
44
+ def shims_have_duplicates?(nodes, shim_rbi_dir)
45
+ return false if nodes.size == 1
46
+
47
+ shims = extract_shims(nodes, shim_rbi_dir)
48
+ return false if shims.empty?
49
+
50
+ props = extract_methods_and_attrs(shims)
51
+ return false if props.empty?
52
+
53
+ shims_with_sigs = extract_nodes_with_sigs(props)
54
+ shims_with_sigs.each do |shim|
55
+ shim_sigs = shim.sigs
56
+
57
+ extract_methods_and_attrs(nodes).each do |node|
58
+ next if node == shim
59
+ return true if shim_sigs.all? { |sig| node.sigs.include?(sig) }
60
+ end
61
+
62
+ return false
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ sig { params(nodes: T::Array[RBI::Node], shim_rbi_dir: String).returns(T::Array[RBI::Node]) }
69
+ def extract_shims(nodes, shim_rbi_dir)
70
+ nodes.select do |node|
71
+ node.loc&.file&.start_with?(shim_rbi_dir)
72
+ end
73
+ end
74
+
75
+ sig { params(nodes: T::Array[RBI::Node]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
76
+ def extract_methods_and_attrs(nodes)
77
+ T.cast(nodes.select do |node|
78
+ node.is_a?(RBI::Method) || node.is_a?(RBI::Attr)
79
+ end, T::Array[T.any(RBI::Method, RBI::Attr)])
80
+ end
81
+
82
+ sig { params(nodes: T::Array[T.any(RBI::Method, RBI::Attr)]).returns(T::Array[T.any(RBI::Method, RBI::Attr)]) }
83
+ def extract_nodes_with_sigs(nodes)
84
+ nodes.reject { |node| node.sigs.empty? }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,57 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "pathname"
5
+ require "shellwords"
6
+
7
+ module Tapioca
8
+ module SorbetHelper
9
+ extend T::Sig
10
+
11
+ SORBET_GEM_SPEC = T.let(
12
+ ::Gem::Specification.find_by_name("sorbet-static"),
13
+ ::Gem::Specification
14
+ )
15
+
16
+ SORBET_BIN = T.let(
17
+ Pathname.new(SORBET_GEM_SPEC.full_gem_path) / "libexec" / "sorbet",
18
+ Pathname
19
+ )
20
+
21
+ SORBET_EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
22
+
23
+ FEATURE_REQUIREMENTS = T.let({
24
+ # First tag that includes https://github.com/sorbet/sorbet/pull/4706
25
+ to_ary_nil_support: ::Gem::Requirement.new(">= 0.5.9220"),
26
+ }.freeze, T::Hash[Symbol, ::Gem::Requirement])
27
+
28
+ class CmdResult < T::Struct
29
+ const :out, String
30
+ const :err, String
31
+ const :status, T::Boolean
32
+ end
33
+
34
+ sig { params(sorbet_args: String).returns(CmdResult) }
35
+ def sorbet(*sorbet_args)
36
+ out, err, status = Open3.capture3([sorbet_path, *sorbet_args].join(" "))
37
+ CmdResult.new(out: out, err: err, status: status.success? || false)
38
+ end
39
+
40
+ sig { returns(String) }
41
+ def sorbet_path
42
+ sorbet_path = ENV.fetch(SORBET_EXE_PATH_ENV_VAR, SORBET_BIN)
43
+ sorbet_path = SORBET_BIN if sorbet_path.empty?
44
+ sorbet_path.to_s.shellescape
45
+ end
46
+
47
+ sig { params(feature: Symbol, version: T.nilable(::Gem::Version)).returns(T::Boolean) }
48
+ def sorbet_supports?(feature, version: nil)
49
+ version = SORBET_GEM_SPEC.version unless version
50
+ requirement = FEATURE_REQUIREMENTS[feature]
51
+
52
+ Kernel.raise "Invalid Sorbet feature #{feature}" unless requirement
53
+
54
+ requirement.satisfied_by?(version)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,118 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca/helpers/test/content"
5
+ require "tapioca/helpers/test/isolation"
6
+ require "tapioca/helpers/test/template"
7
+
8
+ module Tapioca
9
+ module Helpers
10
+ module Test
11
+ module DslCompiler
12
+ extend T::Sig
13
+ extend T::Helpers
14
+
15
+ include Isolation
16
+ include Content
17
+ include Template
18
+
19
+ requires_ancestor { Kernel }
20
+
21
+ sig { params(compiler_class: T.class_of(Tapioca::Dsl::Compiler)).void }
22
+ def use_dsl_compiler(compiler_class)
23
+ @context = T.let(CompilerContext.new(compiler_class), T.nilable(CompilerContext))
24
+ end
25
+
26
+ sig { params(compiler_classes: T.class_of(Tapioca::Dsl::Compiler)).void }
27
+ def activate_other_dsl_compilers(*compiler_classes)
28
+ context.activate_other_dsl_compilers(compiler_classes)
29
+ end
30
+
31
+ sig { params(constant_name: T.any(Symbol, String)).returns(String) }
32
+ def rbi_for(constant_name)
33
+ context.rbi_for(constant_name)
34
+ end
35
+
36
+ sig { returns(T::Array[String]) }
37
+ def gathered_constants
38
+ context.gathered_constants
39
+ end
40
+
41
+ sig { returns(T::Array[String]) }
42
+ def generated_errors
43
+ context.errors
44
+ end
45
+
46
+ sig { returns(CompilerContext) }
47
+ def context
48
+ raise "Please call `use_dsl_compiler` before" unless @context
49
+ @context
50
+ end
51
+
52
+ class CompilerContext
53
+ extend T::Sig
54
+
55
+ sig { returns(T.class_of(Tapioca::Dsl::Compiler)) }
56
+ attr_reader :compiler_class
57
+
58
+ sig { returns(T::Array[T.class_of(Tapioca::Dsl::Compiler)]) }
59
+ attr_reader :other_compiler_classes
60
+
61
+ sig { params(compiler_class: T.class_of(Tapioca::Dsl::Compiler)).void }
62
+ def initialize(compiler_class)
63
+ @compiler_class = compiler_class
64
+ @other_compiler_classes = T.let([], T::Array[T.class_of(Tapioca::Dsl::Compiler)])
65
+ @pipeline = T.let(nil, T.nilable(Tapioca::Dsl::Pipeline))
66
+ @errors = T.let([], T::Array[String])
67
+ end
68
+
69
+ sig { params(compiler_classes: T::Array[T.class_of(Tapioca::Dsl::Compiler)]).void }
70
+ def activate_other_dsl_compilers(compiler_classes)
71
+ @other_compiler_classes = compiler_classes
72
+ end
73
+
74
+ sig { returns(T::Array[T.class_of(Tapioca::Dsl::Compiler)]) }
75
+ def activated_compiler_classes
76
+ [compiler_class, *other_compiler_classes]
77
+ end
78
+
79
+ sig { returns(T::Array[String]) }
80
+ def gathered_constants
81
+ compiler_class.processable_constants.map(&:name).compact.sort
82
+ end
83
+
84
+ sig { params(constant_name: T.any(Symbol, String)).returns(String) }
85
+ def rbi_for(constant_name)
86
+ # Make sure this is a constant that we can handle.
87
+ unless gathered_constants.include?(constant_name.to_s)
88
+ raise "`#{constant_name}` is not processable by the `#{compiler_class}` compiler."
89
+ end
90
+
91
+ file = RBI::File.new(strictness: "strong")
92
+ constant = Object.const_get(constant_name)
93
+
94
+ compiler = compiler_class.new(pipeline, file.root, constant)
95
+ compiler.decorate
96
+
97
+ Tapioca::DEFAULT_RBI_FORMATTER.print_file(file)
98
+ end
99
+
100
+ sig { returns(T::Array[String]) }
101
+ def errors
102
+ pipeline.errors
103
+ end
104
+
105
+ private
106
+
107
+ sig { returns(Tapioca::Dsl::Pipeline) }
108
+ def pipeline
109
+ @pipeline ||= Tapioca::Dsl::Pipeline.new(
110
+ requested_constants: [],
111
+ requested_compilers: activated_compiler_classes
112
+ )
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -100,7 +100,7 @@ module Tapioca
100
100
  load_path_args << File.expand_path(p)
101
101
  end
102
102
 
103
- child = IO.popen([env, Gem.ruby, *load_path_args, $PROGRAM_NAME, *ORIG_ARGV, test_opts])
103
+ child = IO.popen([env, ::Gem.ruby, *load_path_args, $PROGRAM_NAME, *ORIG_ARGV, test_opts])
104
104
 
105
105
  begin
106
106
  Process.wait(child.pid)
@@ -7,15 +7,18 @@ module Tapioca
7
7
  module Helpers
8
8
  module Test
9
9
  module Template
10
- include Kernel
11
10
  extend T::Sig
11
+ extend T::Helpers
12
+
13
+ requires_ancestor { Kernel }
14
+
12
15
  ERB_SUPPORTS_KVARGS = T.let(
13
16
  ::ERB.instance_method(:initialize).parameters.assoc(:key), T.nilable([Symbol, Symbol])
14
17
  )
15
18
 
16
19
  sig { params(selector: String).returns(T::Boolean) }
17
20
  def ruby_version(selector)
18
- Gem::Requirement.new(selector).satisfied_by?(Gem::Version.new(RUBY_VERSION))
21
+ ::Gem::Requirement.new(selector).satisfied_by?(::Gem::Version.new(RUBY_VERSION))
19
22
  end
20
23
 
21
24
  sig { params(src: String).returns(String) }
@@ -28,6 +31,14 @@ module Tapioca
28
31
 
29
32
  erb.result(binding)
30
33
  end
34
+
35
+ sig { params(str: String, indent: Integer).returns(String) }
36
+ def indented(str, indent)
37
+ str.lines.map! do |line|
38
+ next line if line.chomp.empty?
39
+ (" " * indent) + line
40
+ end.join
41
+ end
31
42
  end
32
43
  end
33
44
  end
@@ -2,20 +2,27 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "tapioca"
5
- require "tapioca/loader"
5
+ require "tapioca/runtime/reflection"
6
+ require "tapioca/runtime/trackers"
7
+ require "tapioca/runtime/dynamic_mixin_compiler"
8
+ require "tapioca/runtime/loader"
6
9
  require "tapioca/sorbet_ext/generic_name_patch"
7
10
  require "tapioca/sorbet_ext/fixed_hash_patch"
8
- require "tapioca/generic_type_registry"
11
+ require "tapioca/runtime/generic_type_registry"
9
12
  require "tapioca/helpers/cli_helper"
10
13
  require "tapioca/helpers/config_helper"
11
- require "tapioca/generators"
14
+ require "tapioca/helpers/rbi_helper"
15
+ require "tapioca/helpers/shims_helper"
16
+ require "tapioca/helpers/sorbet_helper"
17
+ require "tapioca/commands"
12
18
  require "tapioca/cli"
13
19
  require "tapioca/gemfile"
14
20
  require "tapioca/executor"
15
- require "tapioca/compilers/sorbet"
16
- require "tapioca/compilers/requires_compiler"
17
- require "tapioca/compilers/symbol_table_compiler"
18
- require "tapioca/compilers/symbol_table/symbol_generator"
19
- require "tapioca/compilers/symbol_table/symbol_loader"
20
- require "tapioca/compilers/todos_compiler"
21
- require "tapioca/compilers/dsl_compiler"
21
+ require "tapioca/static/symbol_table_parser"
22
+ require "tapioca/static/symbol_loader"
23
+ require "tapioca/gem/events"
24
+ require "tapioca/gem/listeners"
25
+ require "tapioca/gem/pipeline"
26
+ require "tapioca/dsl/compiler"
27
+ require "tapioca/dsl/pipeline"
28
+ require "tapioca/static/requires_compiler"
@@ -1,59 +1,13 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rbi"
5
-
6
4
  module RBI
7
- class File
8
- extend T::Sig
9
-
10
- sig { returns(String) }
11
- def transformed_string
12
- transform_rbi!
13
- string
14
- end
15
-
16
- sig { void }
17
- def transform_rbi!
18
- root.nest_singleton_methods!
19
- root.nest_non_public_methods!
20
- root.group_nodes!
21
- root.sort_nodes!
22
- end
23
-
24
- sig do
25
- params(
26
- command: String,
27
- reason: T.nilable(String),
28
- display_heading: T::Boolean
29
- ).void
30
- end
31
- def set_file_header(command, reason: nil, display_heading: true)
32
- return unless display_heading
33
- comments << RBI::Comment.new("DO NOT EDIT MANUALLY")
34
- comments << RBI::Comment.new("This is an autogenerated file for #{reason}.") unless reason.nil?
35
- comments << RBI::Comment.new("Please instead update this file by running `#{command}`.")
36
- end
37
-
38
- sig { void }
39
- def set_empty_body_content
40
- comments << RBI::BlankLine.new unless comments.empty?
41
- comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
42
- comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
43
- end
44
-
45
- sig { returns(T::Boolean) }
46
- def empty?
47
- root.empty?
48
- end
49
- end
50
-
51
5
  class Tree
52
6
  extend T::Sig
53
7
 
54
8
  sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
55
9
  def create_path(constant, &block)
56
- constant_name = Tapioca::Reflection.name_of(constant)
10
+ constant_name = Tapioca::Runtime::Reflection.name_of(constant)
57
11
  raise "given constant does not have a name" unless constant_name
58
12
 
59
13
  instance = ::Module.const_get(constant_name)
@@ -137,7 +91,7 @@ module RBI
137
91
 
138
92
  SPECIAL_METHOD_NAMES = T.let(
139
93
  ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
140
- "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"].freeze,
94
+ "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`",].freeze,
141
95
  T::Array[String]
142
96
  )
143
97
 
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ class RBIFormatter < RBI::Formatter
6
+ extend T::Sig
7
+
8
+ sig do
9
+ params(
10
+ file: RBI::File,
11
+ command: String,
12
+ reason: T.nilable(String)
13
+ ).void
14
+ end
15
+ def write_header!(file, command, reason: nil)
16
+ file.comments << RBI::Comment.new("DO NOT EDIT MANUALLY")
17
+ file.comments << RBI::Comment.new("This is an autogenerated file for #{reason}.") unless reason.nil?
18
+ file.comments << RBI::Comment.new("Please instead update this file by running `#{command}`.")
19
+ end
20
+
21
+ sig { params(file: RBI::File).void }
22
+ def write_empty_body_comment!(file)
23
+ file.comments << RBI::BlankLine.new unless file.comments.empty?
24
+ file.comments << RBI::Comment.new("THIS IS AN EMPTY RBI FILE.")
25
+ file.comments << RBI::Comment.new("see https://github.com/Shopify/tapioca/wiki/Manual-Gem-Requires")
26
+ end
27
+ end
28
+
29
+ DEFAULT_RBI_FORMATTER = T.let(RBIFormatter.new(
30
+ add_sig_templates: false,
31
+ group_nodes: true,
32
+ max_line_length: nil,
33
+ nest_singleton_methods: true,
34
+ nest_non_public_methods: true,
35
+ sort_nodes: true
36
+ ), RBIFormatter)
37
+ end
@@ -0,0 +1,227 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Runtime
6
+ class DynamicMixinCompiler
7
+ extend T::Sig
8
+ include Runtime::Reflection
9
+
10
+ sig { returns(T::Array[Module]) }
11
+ attr_reader :dynamic_extends, :dynamic_includes
12
+
13
+ sig { returns(T::Array[Symbol]) }
14
+ attr_reader :class_attribute_readers, :class_attribute_writers, :class_attribute_predicates
15
+
16
+ sig { returns(T::Array[Symbol]) }
17
+ attr_reader :instance_attribute_readers, :instance_attribute_writers, :instance_attribute_predicates
18
+
19
+ sig { params(constant: Module).void }
20
+ def initialize(constant)
21
+ @constant = constant
22
+ mixins_from_modules = {}.compare_by_identity
23
+ class_attribute_readers = T.let([], T::Array[Symbol])
24
+ class_attribute_writers = T.let([], T::Array[Symbol])
25
+ class_attribute_predicates = T.let([], T::Array[Symbol])
26
+
27
+ instance_attribute_readers = T.let([], T::Array[Symbol])
28
+ instance_attribute_writers = T.let([], T::Array[Symbol])
29
+ instance_attribute_predicates = T.let([], T::Array[Symbol])
30
+
31
+ Class.new do
32
+ # Override the `self.include` method
33
+ define_singleton_method(:include) do |mod|
34
+ # Take a snapshot of the list of singleton class ancestors
35
+ # before the actual include
36
+ before = singleton_class.ancestors
37
+ # Call the actual `include` method with the supplied module
38
+ super(mod).tap do
39
+ # Take a snapshot of the list of singleton class ancestors
40
+ # after the actual include
41
+ after = singleton_class.ancestors
42
+ # The difference is the modules that are added to the list
43
+ # of ancestors of the singleton class. Those are all the
44
+ # modules that were `extend`ed due to the `include` call.
45
+ #
46
+ # We record those modules on our lookup table keyed by
47
+ # the included module with the values being all the modules
48
+ # that that module pulls into the singleton class.
49
+ #
50
+ # We need to reverse the order, since the extend order should
51
+ # be the inverse of the ancestor order. That is, earlier
52
+ # extended modules would be later in the ancestor chain.
53
+ mixins_from_modules[mod] = (after - before).reverse!
54
+ end
55
+ rescue Exception # rubocop:disable Lint/RescueException
56
+ # this is a best effort, bail if we can't perform this
57
+ end
58
+
59
+ define_singleton_method(:class_attribute) do |*attrs, **kwargs|
60
+ class_attribute_readers.concat(attrs)
61
+ class_attribute_writers.concat(attrs)
62
+
63
+ instance_predicate = kwargs.fetch(:instance_predicate, true)
64
+ instance_accessor = kwargs.fetch(:instance_accessor, true)
65
+ instance_reader = kwargs.fetch(:instance_reader, instance_accessor)
66
+ instance_writer = kwargs.fetch(:instance_writer, instance_accessor)
67
+
68
+ if instance_reader
69
+ instance_attribute_readers.concat(attrs)
70
+ end
71
+
72
+ if instance_writer
73
+ instance_attribute_writers.concat(attrs)
74
+ end
75
+
76
+ if instance_predicate
77
+ class_attribute_predicates.concat(attrs)
78
+
79
+ if instance_reader
80
+ instance_attribute_predicates.concat(attrs)
81
+ end
82
+ end
83
+
84
+ super(*attrs, **kwargs) if defined?(super)
85
+ end
86
+
87
+ # rubocop:disable Style/MissingRespondToMissing
88
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
89
+ def method_missing(symbol, *args)
90
+ # We need this here so that we can handle any random instance
91
+ # method calls on the fake including class that may be done by
92
+ # the included module during the `self.included` hook.
93
+ end
94
+
95
+ class << self
96
+ extend T::Sig
97
+
98
+ T::Sig::WithoutRuntime.sig { params(symbol: Symbol, args: T.untyped).returns(T.untyped) }
99
+ def method_missing(symbol, *args)
100
+ # Similarly, we need this here so that we can handle any
101
+ # random class method calls on the fake including class
102
+ # that may be done by the included module during the
103
+ # `self.included` hook.
104
+ end
105
+ end
106
+ # rubocop:enable Style/MissingRespondToMissing
107
+ end.include(constant)
108
+
109
+ # The value that corresponds to the original included constant
110
+ # is the list of all dynamically extended modules because of that
111
+ # constant. We grab that value by deleting the key for the original
112
+ # constant.
113
+ @dynamic_extends = T.let(mixins_from_modules.delete(constant) || [], T::Array[Module])
114
+
115
+ # Since we deleted the original constant from the list of keys, all
116
+ # the keys that remain are the ones that are dynamically included modules
117
+ # during the include of the original constant.
118
+ @dynamic_includes = T.let(mixins_from_modules.keys, T::Array[Module])
119
+
120
+ @class_attribute_readers = T.let(class_attribute_readers, T::Array[Symbol])
121
+ @class_attribute_writers = T.let(class_attribute_writers, T::Array[Symbol])
122
+ @class_attribute_predicates = T.let(class_attribute_predicates, T::Array[Symbol])
123
+
124
+ @instance_attribute_readers = T.let(instance_attribute_readers, T::Array[Symbol])
125
+ @instance_attribute_writers = T.let(instance_attribute_writers, T::Array[Symbol])
126
+ @instance_attribute_predicates = T.let(instance_attribute_predicates, T::Array[Symbol])
127
+ end
128
+
129
+ sig { returns(T::Boolean) }
130
+ def empty_attributes?
131
+ @class_attribute_readers.empty? && @class_attribute_writers.empty?
132
+ end
133
+
134
+ sig { params(tree: RBI::Tree).void }
135
+ def compile_class_attributes(tree)
136
+ return if empty_attributes?
137
+
138
+ # Create a synthetic module to hold the generated class methods
139
+ tree << RBI::Module.new("GeneratedClassMethods") do |mod|
140
+ class_attribute_readers.each do |attribute|
141
+ mod << RBI::Method.new(attribute.to_s)
142
+ end
143
+
144
+ class_attribute_writers.each do |attribute|
145
+ mod << RBI::Method.new("#{attribute}=") do |method|
146
+ method << RBI::Param.new("value")
147
+ end
148
+ end
149
+
150
+ class_attribute_predicates.each do |attribute|
151
+ mod << RBI::Method.new("#{attribute}?")
152
+ end
153
+ end
154
+
155
+ # Create a synthetic module to hold the generated instance methods
156
+ tree << RBI::Module.new("GeneratedInstanceMethods") do |mod|
157
+ instance_attribute_readers.each do |attribute|
158
+ mod << RBI::Method.new(attribute.to_s)
159
+ end
160
+
161
+ instance_attribute_writers.each do |attribute|
162
+ mod << RBI::Method.new("#{attribute}=") do |method|
163
+ method << RBI::Param.new("value")
164
+ end
165
+ end
166
+
167
+ instance_attribute_predicates.each do |attribute|
168
+ mod << RBI::Method.new("#{attribute}?")
169
+ end
170
+ end
171
+
172
+ # Add a mixes_in_class_methods and include for the generated modules
173
+ tree << RBI::MixesInClassMethods.new("GeneratedClassMethods")
174
+ tree << RBI::Include.new("GeneratedInstanceMethods")
175
+ end
176
+
177
+ sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
178
+ def compile_mixes_in_class_methods(tree)
179
+ includes = dynamic_includes.map do |mod|
180
+ qname = qualified_name_of(mod)
181
+
182
+ next if qname.nil? || qname.empty?
183
+ next if filtered_mixin?(qname)
184
+
185
+ tree << RBI::Include.new(qname)
186
+
187
+ mod
188
+ end.compact
189
+
190
+ # If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
191
+ # constant itself
192
+ mixed_in_class_methods = dynamic_extends.select do |mod|
193
+ mod != @constant && !module_included_by_another_dynamic_extend?(mod, dynamic_extends)
194
+ end
195
+
196
+ return [[], []] if mixed_in_class_methods.empty?
197
+
198
+ mixed_in_class_methods.each do |mod|
199
+ qualified_name = qualified_name_of(mod)
200
+
201
+ next if qualified_name.nil? || qualified_name.empty?
202
+ next if filtered_mixin?(qualified_name)
203
+
204
+ tree << RBI::MixesInClassMethods.new(qualified_name)
205
+ end
206
+
207
+ [mixed_in_class_methods, includes]
208
+ rescue
209
+ [[], []] # silence errors
210
+ end
211
+
212
+ sig { params(mod: Module, dynamic_extends: T::Array[Module]).returns(T::Boolean) }
213
+ def module_included_by_another_dynamic_extend?(mod, dynamic_extends)
214
+ dynamic_extends.any? do |dynamic_extend|
215
+ mod != dynamic_extend && ancestors_of(dynamic_extend).include?(mod)
216
+ end
217
+ end
218
+
219
+ sig { params(qualified_mixin_name: String).returns(T::Boolean) }
220
+ def filtered_mixin?(qualified_mixin_name)
221
+ # filter T:: namespace mixins that aren't T::Props
222
+ # T::Props and subconstants have semantic value
223
+ qualified_mixin_name.start_with?("::T::") && !qualified_mixin_name.start_with?("::T::Props")
224
+ end
225
+ end
226
+ end
227
+ end