tapioca 0.17.1 → 0.17.2
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 +4 -4
- data/README.md +21 -0
- data/lib/ruby_lsp/tapioca/addon.rb +17 -16
- data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +24 -24
- data/lib/tapioca/gem/listeners/methods.rb +23 -15
- data/lib/tapioca/gem/listeners/source_location.rb +8 -4
- data/lib/tapioca/gem/listeners/yard_doc.rb +7 -1
- data/lib/tapioca/gem/pipeline.rb +52 -21
- data/lib/tapioca/internal.rb +6 -0
- data/lib/tapioca/rbs/rewriter.rb +2 -2
- data/lib/tapioca/runtime/reflection.rb +34 -5
- data/lib/tapioca/runtime/source_location.rb +44 -0
- data/lib/tapioca/runtime/trackers/autoload.rb +3 -1
- data/lib/tapioca/runtime/trackers/constant_definition.rb +18 -14
- data/lib/tapioca/runtime/trackers/method_definition.rb +65 -0
- data/lib/tapioca/runtime/trackers/mixin.rb +2 -1
- data/lib/tapioca/runtime/trackers.rb +1 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01200af75555809c4d8c6d4fd32cc61bebdfc11e6f4aec27a99f8d13bd51fa66
|
4
|
+
data.tar.gz: b329558753d7784f88bb7afe1f0030c9424d9907c4b0fcc53b669b8caab6ec2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77841967e1d02af13ed5a1f2215818f4b63b605c359c26b437f0b6129be1d70dc554666a7aea8710acae812bec4ef422ffc0cd7ae797df87eecb2287f6591352
|
7
|
+
data.tar.gz: 7ad4ccd1a11c2a2abcdaa3b1597dd87b124b5da1d800b61736dbe663d1d5c32219de6f491941139441e790834700e64ac6ded2cedcf0d49e91badfd66d9fe76b
|
data/README.md
CHANGED
@@ -52,6 +52,9 @@ Tapioca makes it easy to work with [Sorbet](https://sorbet.org) in your codebase
|
|
52
52
|
* [Writing custom DSL extensions](#writing-custom-dsl-extensions)
|
53
53
|
* [RBI files for missing constants and methods](#rbi-files-for-missing-constants-and-methods)
|
54
54
|
* [Configuration](#configuration)
|
55
|
+
* [Editor Integration](#editor-integration)
|
56
|
+
* [Setup](#setup)
|
57
|
+
* [Features](#features)
|
55
58
|
* [Contributing](#contributing)
|
56
59
|
* [DSL compilers](#dsl-compilers)
|
57
60
|
* [License](#license)
|
@@ -395,6 +398,12 @@ Tapioca also supports pulling annotations from multiple sources:
|
|
395
398
|
$ bin/tapioca annotations --sources https://raw.githubusercontent.com/$USER/$REPO1/$BRANCH https://raw.githubusercontent.com/$USER/$REPO2/$BRANCH
|
396
399
|
```
|
397
400
|
|
401
|
+
You can also specify a local directory path to pull annotations from
|
402
|
+
|
403
|
+
```shell
|
404
|
+
$ bin/tapioca annotations --sources path/to/folder
|
405
|
+
```
|
406
|
+
|
398
407
|
#### Basic authentication
|
399
408
|
|
400
409
|
Private repositories can be used as sources by passing the option `--auth` with an authentication string. For Github, this string is `token $TOKEN` where `$TOKEN` is a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token):
|
@@ -993,6 +1002,18 @@ annotations:
|
|
993
1002
|
```
|
994
1003
|
<!-- END_CONFIG_TEMPLATE -->
|
995
1004
|
|
1005
|
+
## Editor Integration
|
1006
|
+
|
1007
|
+
### Setup
|
1008
|
+
|
1009
|
+
Tapioca supports generating RBIs upon file changes through the [Ruby LSP's add-on system](https://shopify.github.io/ruby-lsp/#add-ons). It's enabled by default on VS Code using [Ruby LSP](https://shopify.github.io/ruby-lsp/#usage), and you can enable it for other editors by supplying the `enabledFeatureFlags` option in LSP configuration with a hash value of `"tapiocaAddon": true`.
|
1010
|
+
|
1011
|
+
If you'd like to disable the Tapioca add-on you can set `tapiocaAddon` to `false` in your LSP configuration. For VS Code this looks like `"rubyLsp.featureFlags": { "tapiocaAddon": false }` in your `settings.json`.
|
1012
|
+
|
1013
|
+
### Features
|
1014
|
+
- DSL RBI generation: When editing a Ruby file, Tapioca will execute `bin/tapioca dsl` with the constants found in your file, e.g. `bin/tapioca dsl MyClass`
|
1015
|
+
- Gem RBI generation: When changes are made to `Gemfile.lock`, Tapioca will execute `bin/tapioca gem` with the updated gem names, e.g. `bin/tapioca gem my_gem`
|
1016
|
+
|
996
1017
|
## Contributing
|
997
1018
|
|
998
1019
|
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/main/CODE_OF_CONDUCT.md) code of conduct.
|
@@ -18,8 +18,6 @@ require "ruby_lsp/tapioca/run_gem_rbi_check"
|
|
18
18
|
module RubyLsp
|
19
19
|
module Tapioca
|
20
20
|
class Addon < ::RubyLsp::Addon
|
21
|
-
extend T::Sig
|
22
|
-
|
23
21
|
#: -> void
|
24
22
|
def initialize
|
25
23
|
super
|
@@ -44,7 +42,7 @@ module RubyLsp
|
|
44
42
|
# Get a handle to the Rails add-on's runtime client. The call to `rails_runner_client` will block this thread
|
45
43
|
# until the server has finished booting, but it will not block the main LSP. This has to happen inside of a
|
46
44
|
# thread
|
47
|
-
addon =
|
45
|
+
addon = ::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0.4.0", "< 0.5") #: as ::RubyLsp::Rails::Addon
|
48
46
|
@rails_runner_client = addon.rails_runner_client
|
49
47
|
@outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}")
|
50
48
|
@rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__))
|
@@ -93,10 +91,11 @@ module RubyLsp
|
|
93
91
|
has_route_change = false #: bool
|
94
92
|
has_fixtures_change = false #: bool
|
95
93
|
needs_compiler_reload = false #: bool
|
94
|
+
index = @index #: as !nil
|
96
95
|
|
97
96
|
constants = changes.flat_map do |change|
|
98
|
-
path = URI(change[:uri]).to_standardized_path
|
99
|
-
next unless file_updated?(change, path)
|
97
|
+
path = URI(change[:uri]).to_standardized_path #: String?
|
98
|
+
next unless path && file_updated?(change, path)
|
100
99
|
|
101
100
|
if File.fnmatch("**/fixtures/**/*.yml{,.erb}", path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
102
101
|
has_fixtures_change = true
|
@@ -115,7 +114,7 @@ module RubyLsp
|
|
115
114
|
next
|
116
115
|
end
|
117
116
|
|
118
|
-
entries =
|
117
|
+
entries = index.entries_for(change[:uri])
|
119
118
|
next unless entries
|
120
119
|
|
121
120
|
entries.filter_map do |entry|
|
@@ -178,6 +177,8 @@ module RubyLsp
|
|
178
177
|
|
179
178
|
#: (Hash[Symbol, untyped] change, String path) -> bool
|
180
179
|
def file_updated?(change, path)
|
180
|
+
queue = @outgoing_queue #: as !nil
|
181
|
+
|
181
182
|
case change[:type]
|
182
183
|
when Constant::FileChangeType::CREATED
|
183
184
|
@file_checksums[path] = Zlib.crc32(File.read(path)).to_s
|
@@ -185,7 +186,7 @@ module RubyLsp
|
|
185
186
|
when Constant::FileChangeType::CHANGED
|
186
187
|
current_checksum = Zlib.crc32(File.read(path)).to_s
|
187
188
|
if @file_checksums[path] == current_checksum
|
188
|
-
|
189
|
+
queue << Notification.window_log_message(
|
189
190
|
"File has not changed. Skipping #{path}",
|
190
191
|
type: Constant::MessageType::INFO,
|
191
192
|
)
|
@@ -196,7 +197,7 @@ module RubyLsp
|
|
196
197
|
when Constant::FileChangeType::DELETED
|
197
198
|
@file_checksums.delete(path)
|
198
199
|
else
|
199
|
-
|
200
|
+
queue << Notification.window_log_message(
|
200
201
|
"Unexpected file change type: #{change[:type]}",
|
201
202
|
type: Constant::MessageType::WARNING,
|
202
203
|
)
|
@@ -207,16 +208,16 @@ module RubyLsp
|
|
207
208
|
|
208
209
|
#: -> void
|
209
210
|
def run_gem_rbi_check
|
210
|
-
|
211
|
+
state = @global_state #: as !nil
|
212
|
+
gem_rbi_check = RunGemRbiCheck.new(state.workspace_path)
|
211
213
|
gem_rbi_check.run
|
212
214
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
gem_rbi_check.stderr,
|
218
|
-
|
219
|
-
) unless gem_rbi_check.stderr.empty?
|
215
|
+
queue = @outgoing_queue #: as !nil
|
216
|
+
queue << Notification.window_log_message(gem_rbi_check.stdout) unless gem_rbi_check.stdout.empty?
|
217
|
+
|
218
|
+
unless gem_rbi_check.stderr.empty?
|
219
|
+
queue << Notification.window_log_message(gem_rbi_check.stderr, type: Constant::MessageType::WARNING)
|
220
|
+
end
|
220
221
|
end
|
221
222
|
end
|
222
223
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "open3"
|
@@ -7,10 +7,10 @@ require "ruby_lsp/tapioca/lockfile_diff_parser"
|
|
7
7
|
module RubyLsp
|
8
8
|
module Tapioca
|
9
9
|
class RunGemRbiCheck
|
10
|
-
|
10
|
+
#: String
|
11
|
+
attr_reader :stdout, :stderr
|
11
12
|
|
12
|
-
|
13
|
-
attr_reader :stderr
|
13
|
+
#: Process::Status?
|
14
14
|
attr_reader :status
|
15
15
|
|
16
16
|
#: (String project_path) -> void
|
@@ -34,11 +34,9 @@ module RubyLsp
|
|
34
34
|
|
35
35
|
private
|
36
36
|
|
37
|
-
attr_reader :project_path
|
38
|
-
|
39
37
|
#: -> bool?
|
40
38
|
def git_repo?
|
41
|
-
_, status = Open3.capture2e("git", "rev-parse", "--is-inside-work-tree", chdir: project_path)
|
39
|
+
_, status = Open3.capture2e("git", "rev-parse", "--is-inside-work-tree", chdir: @project_path)
|
42
40
|
status.success?
|
43
41
|
end
|
44
42
|
|
@@ -49,7 +47,7 @@ module RubyLsp
|
|
49
47
|
|
50
48
|
#: -> Pathname
|
51
49
|
def lockfile
|
52
|
-
@lockfile ||= Pathname(project_path).join("Gemfile.lock") #: Pathname?
|
50
|
+
@lockfile ||= Pathname(@project_path).join("Gemfile.lock") #: Pathname?
|
53
51
|
end
|
54
52
|
|
55
53
|
#: -> String
|
@@ -81,15 +79,16 @@ module RubyLsp
|
|
81
79
|
#: (Array[String] gems) -> void
|
82
80
|
def execute_tapioca_gem_command(gems)
|
83
81
|
Bundler.with_unbundled_env do
|
84
|
-
stdout, stderr, status =
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
82
|
+
stdout, stderr, status = Open3 #: as untyped
|
83
|
+
.capture3(
|
84
|
+
"bundle",
|
85
|
+
"exec",
|
86
|
+
"tapioca",
|
87
|
+
"gem",
|
88
|
+
"--lsp_addon",
|
89
|
+
*gems,
|
90
|
+
chdir: @project_path,
|
91
|
+
)
|
93
92
|
|
94
93
|
log_message(stdout) unless stdout.empty?
|
95
94
|
@stderr = stderr unless stderr.empty?
|
@@ -101,7 +100,7 @@ module RubyLsp
|
|
101
100
|
def remove_rbis(gems)
|
102
101
|
files = Dir.glob(
|
103
102
|
"sorbet/rbi/gems/{#{gems.join(",")}}@*.rbi",
|
104
|
-
base: project_path,
|
103
|
+
base: @project_path,
|
105
104
|
)
|
106
105
|
delete_files(files, "Removed RBIs for")
|
107
106
|
end
|
@@ -117,16 +116,15 @@ module RubyLsp
|
|
117
116
|
|
118
117
|
#: (*untyped flags) -> Array[String]
|
119
118
|
def git_ls_gem_rbis(*flags)
|
120
|
-
|
121
|
-
|
122
|
-
execute_in_project_path(*flags)
|
119
|
+
self #: as untyped # rubocop:disable Style/RedundantSelf
|
120
|
+
.execute_in_project_path("git", "ls-files", *flags, "sorbet/rbi/gems/")
|
123
121
|
.lines
|
124
122
|
.map(&:strip)
|
125
123
|
end
|
126
124
|
|
127
125
|
#: (Array[String] files, String message) -> void
|
128
126
|
def delete_files(files, message)
|
129
|
-
files_to_remove = files.map { |file| File.join(project_path, file) }
|
127
|
+
files_to_remove = files.map { |file| File.join(@project_path, file) }
|
130
128
|
FileUtils.rm(files_to_remove)
|
131
129
|
log_message("#{message}: #{files.join(", ")}") unless files.empty?
|
132
130
|
end
|
@@ -142,10 +140,12 @@ module RubyLsp
|
|
142
140
|
@stdout += "#{message}\n"
|
143
141
|
end
|
144
142
|
|
143
|
+
#: (*String, ?stdin: String?) -> String
|
145
144
|
def execute_in_project_path(*parts, stdin: nil)
|
146
|
-
options = { chdir: project_path }
|
145
|
+
options = { chdir: @project_path }
|
147
146
|
options[:stdin_data] = stdin if stdin
|
148
|
-
stdout_and_stderr, _status =
|
147
|
+
stdout_and_stderr, _status = Open3 #: as untyped
|
148
|
+
.capture2e(*parts, options)
|
149
149
|
stdout_and_stderr
|
150
150
|
end
|
151
151
|
end
|
@@ -56,10 +56,30 @@ module Tapioca
|
|
56
56
|
def compile_method(tree, symbol_name, constant, method, visibility = RBI::Public.new)
|
57
57
|
return unless method
|
58
58
|
return unless method_owned_by_constant?(method, constant)
|
59
|
-
return if @pipeline.symbol_in_payload?(symbol_name) && !@pipeline.method_in_gem?(method)
|
60
59
|
|
61
|
-
|
62
|
-
|
60
|
+
begin
|
61
|
+
signature = signature_of!(method)
|
62
|
+
method = signature.method if signature #: UnboundMethod
|
63
|
+
|
64
|
+
case @pipeline.method_definition_in_gem(method.name, constant)
|
65
|
+
when Pipeline::MethodUnknown
|
66
|
+
# This means that this is a C-method. Thus, we want to
|
67
|
+
# skip it only if the constant is an ignored one, since
|
68
|
+
# that probably means that we've hit a C-method for a
|
69
|
+
# core type.
|
70
|
+
return if @pipeline.symbol_in_payload?(symbol_name)
|
71
|
+
when Pipeline::MethodNotInGem
|
72
|
+
# Do not process this method, if it is not defined by the current gem
|
73
|
+
return
|
74
|
+
end
|
75
|
+
rescue SignatureBlockError => error
|
76
|
+
@pipeline.error_handler.call(<<~MSG)
|
77
|
+
Unable to compile signature for method: #{method.owner}##{method.name}
|
78
|
+
Exception raised when loading signature: #{error.cause.inspect}
|
79
|
+
MSG
|
80
|
+
|
81
|
+
signature = nil
|
82
|
+
end
|
63
83
|
|
64
84
|
method_name = method.name.to_s
|
65
85
|
return unless valid_method_name?(method_name)
|
@@ -192,18 +212,6 @@ module Tapioca
|
|
192
212
|
def ignore?(event)
|
193
213
|
event.is_a?(Tapioca::Gem::ForeignScopeNodeAdded)
|
194
214
|
end
|
195
|
-
|
196
|
-
#: (UnboundMethod method) -> untyped
|
197
|
-
def lookup_signature_of(method)
|
198
|
-
signature_of!(method)
|
199
|
-
rescue LoadError, StandardError => error
|
200
|
-
@pipeline.error_handler.call(<<~MSG)
|
201
|
-
Unable to compile signature for method: #{method.owner}##{method.name}
|
202
|
-
Exception raised when loading signature: #{error.inspect}
|
203
|
-
MSG
|
204
|
-
|
205
|
-
nil
|
206
|
-
end
|
207
215
|
end
|
208
216
|
end
|
209
217
|
end
|
@@ -24,20 +24,24 @@ module Tapioca
|
|
24
24
|
# constants that are defined by multiple gems.
|
25
25
|
locations = Runtime::Trackers::ConstantDefinition.locations_for(event.constant)
|
26
26
|
location = locations.find do |loc|
|
27
|
-
Pathname.new(loc.
|
27
|
+
Pathname.new(loc.file).realpath.to_s.include?(@pipeline.gem.full_gem_path)
|
28
28
|
end
|
29
29
|
|
30
30
|
# The location may still be nil in some situations, like constant aliases (e.g.: MyAlias = OtherConst). These
|
31
31
|
# are quite difficult to attribute a correct location, given that the source location points to the original
|
32
32
|
# constants and not the alias
|
33
|
-
add_source_location_comment(event.node, location.
|
33
|
+
add_source_location_comment(event.node, location.file, location.line) unless location.nil?
|
34
34
|
end
|
35
35
|
|
36
36
|
# @override
|
37
37
|
#: (MethodNodeAdded event) -> void
|
38
38
|
def on_method(event)
|
39
|
-
|
40
|
-
|
39
|
+
definition = @pipeline.method_definition_in_gem(event.method.name, event.constant)
|
40
|
+
|
41
|
+
if Pipeline::MethodInGemWithLocation === definition
|
42
|
+
loc = definition.location
|
43
|
+
add_source_location_comment(event.node, loc.file, loc.line)
|
44
|
+
end
|
41
45
|
end
|
42
46
|
|
43
47
|
#: (RBI::NodeWithComments node, String? file, Integer? line) -> void
|
@@ -16,6 +16,7 @@ module Tapioca
|
|
16
16
|
"warn_indent:",
|
17
17
|
"shareable_constant_value:",
|
18
18
|
"rubocop:",
|
19
|
+
"@requires_ancestor:",
|
19
20
|
] #: Array[String]
|
20
21
|
|
21
22
|
IGNORED_SIG_TAGS = ["param", "return"] #: Array[String]
|
@@ -29,6 +30,11 @@ module Tapioca
|
|
29
30
|
|
30
31
|
private
|
31
32
|
|
33
|
+
#: (String line) -> bool
|
34
|
+
def rbs_comment?(line)
|
35
|
+
line.strip.start_with?(": ", "| ")
|
36
|
+
end
|
37
|
+
|
32
38
|
# @override
|
33
39
|
#: (ConstNodeAdded event) -> void
|
34
40
|
def on_const(event)
|
@@ -60,7 +66,7 @@ module Tapioca
|
|
60
66
|
return [] if /(copyright|license)/i.match?(docstring)
|
61
67
|
|
62
68
|
comments = docstring.lines
|
63
|
-
.reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } }
|
69
|
+
.reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
|
64
70
|
.map! { |line| RBI::Comment.new(line) }
|
65
71
|
|
66
72
|
tags = yard_docs.tags
|
data/lib/tapioca/gem/pipeline.rb
CHANGED
@@ -107,35 +107,66 @@ module Tapioca
|
|
107
107
|
@payload_symbols.include?(symbol_name)
|
108
108
|
end
|
109
109
|
|
110
|
-
# this looks something like:
|
111
|
-
# "(eval at /path/to/file.rb:123)"
|
112
|
-
# and we are just interested in the "/path/to/file.rb" part
|
113
|
-
EVAL_SOURCE_FILE_PATTERN = /\(eval at (.+):\d+\)/ #: Regexp
|
114
|
-
|
115
110
|
#: ((String | Symbol) name) -> bool
|
116
111
|
def constant_in_gem?(name)
|
117
|
-
|
112
|
+
loc = const_source_location(name)
|
118
113
|
|
119
|
-
|
120
|
-
return true
|
121
|
-
# If the source location of the constant is "(eval)", all bets are off.
|
122
|
-
return true if source_file == "(eval)"
|
114
|
+
# If the source location of the constant isn't available or is "(eval)", all bets are off.
|
115
|
+
return true if loc.nil? || loc.file.nil? || loc.file == "(eval)"
|
123
116
|
|
124
|
-
|
125
|
-
|
126
|
-
# file being something like `(eval at /path/to/file.rb:123)`. We try to parse
|
127
|
-
# this string to get the actual source file.
|
128
|
-
source_file = source_file.sub(EVAL_SOURCE_FILE_PATTERN, "\\1")
|
117
|
+
gem.contains_path?(loc.file)
|
118
|
+
end
|
129
119
|
|
130
|
-
|
120
|
+
class MethodDefinitionLookupResult
|
121
|
+
extend T::Helpers
|
122
|
+
abstract!
|
131
123
|
end
|
132
124
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
125
|
+
# The method doesn't seem to exist
|
126
|
+
class MethodUnknown < MethodDefinitionLookupResult; end
|
127
|
+
|
128
|
+
# The method is not defined in the gem
|
129
|
+
class MethodNotInGem < MethodDefinitionLookupResult; end
|
130
|
+
|
131
|
+
# The method probably defined in the gem but doesn't have a source location
|
132
|
+
class MethodInGemWithoutLocation < MethodDefinitionLookupResult; end
|
133
|
+
|
134
|
+
# The method defined in gem and has a source location
|
135
|
+
class MethodInGemWithLocation < MethodDefinitionLookupResult
|
136
|
+
extend T::Sig
|
137
|
+
|
138
|
+
#: Runtime::SourceLocation
|
139
|
+
attr_reader :location
|
140
|
+
|
141
|
+
#: (Runtime::SourceLocation location) -> void
|
142
|
+
def initialize(location)
|
143
|
+
@location = location
|
144
|
+
super()
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#: (Symbol method_name, Module owner) -> MethodDefinitionLookupResult
|
149
|
+
def method_definition_in_gem(method_name, owner)
|
150
|
+
definitions = Tapioca::Runtime::Trackers::MethodDefinition.method_definitions_for(method_name, owner)
|
151
|
+
|
152
|
+
# If the source location of the method isn't available, signal that by returning nil.
|
153
|
+
return MethodUnknown.new if definitions.empty?
|
154
|
+
|
155
|
+
# Look up the first entry that matches a file in the gem.
|
156
|
+
found = definitions.find { |loc| @gem.contains_path?(loc.file) }
|
157
|
+
|
158
|
+
unless found
|
159
|
+
# If the source location of the method is "(eval)", err on the side of caution and include the method.
|
160
|
+
found = definitions.find { |loc| loc.file == "(eval)" }
|
161
|
+
# However, we can just return true to signal that the method should be included.
|
162
|
+
# We can't provide a source location for it, but we want it to be included in the gem RBI.
|
163
|
+
return MethodInGemWithoutLocation.new if found
|
164
|
+
end
|
165
|
+
|
166
|
+
# If we searched but couldn't find a source location in the gem, return false to signal that.
|
167
|
+
return MethodNotInGem.new unless found
|
137
168
|
|
138
|
-
|
169
|
+
MethodInGemWithLocation.new(found)
|
139
170
|
end
|
140
171
|
|
141
172
|
# Helpers
|
data/lib/tapioca/internal.rb
CHANGED
@@ -16,6 +16,12 @@ require "tapioca/runtime/generic_type_registry"
|
|
16
16
|
|
17
17
|
# The rewriter needs to be loaded very early so RBS comments within Tapioca itself are rewritten
|
18
18
|
require "spoom"
|
19
|
+
# Eager load all the autoloads at this point, so that we don't enter into
|
20
|
+
# a weird loop when the autoloads get triggered and we try to require the file.
|
21
|
+
# This is especially important since Prism has a few autoloaded constants that
|
22
|
+
# should NOT be rewritten (since they are needed for the rewriting itself), so
|
23
|
+
# should be loaded as early as possible.
|
24
|
+
Tapioca::Runtime::Trackers::Autoload.eager_load_all!
|
19
25
|
require "tapioca/rbs/rewriter"
|
20
26
|
# ^ Do not change the order of these requires
|
21
27
|
|
data/lib/tapioca/rbs/rewriter.rb
CHANGED
@@ -26,7 +26,7 @@ begin
|
|
26
26
|
module InstructionSequenceMixin
|
27
27
|
#: (String) -> RubyVM::InstructionSequence
|
28
28
|
def load_iseq(path)
|
29
|
-
super
|
29
|
+
super if defined?(super)
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
@@ -49,7 +49,7 @@ RequireHooks.source_transform(patterns: ["**/*.rb"]) do |path, source|
|
|
49
49
|
if source =~ /^\s*#\s*typed: (ignore|false|true|strict|strong|__STDLIB_INTERNAL)/
|
50
50
|
Spoom::Sorbet::Translate.rbs_comments_to_sorbet_sigs(source, file: path)
|
51
51
|
end
|
52
|
-
rescue
|
52
|
+
rescue Spoom::Sorbet::Translate::Error
|
53
53
|
# If we can't translate the RBS comments back into Sorbet's signatures, we just skip the file.
|
54
54
|
source
|
55
55
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require "tapioca/runtime/source_location"
|
5
|
+
|
4
6
|
# On Ruby 3.2 or newer, Class defines an attached_object method that returns the
|
5
7
|
# attached class of a singleton class without iterating ObjectSpace. On older
|
6
8
|
# versions of Ruby, we fall back to iterating ObjectSpace.
|
@@ -125,15 +127,19 @@ module Tapioca
|
|
125
127
|
end
|
126
128
|
end
|
127
129
|
|
130
|
+
SignatureBlockError = Class.new(Tapioca::Error)
|
131
|
+
|
128
132
|
#: ((UnboundMethod | Method) method) -> untyped
|
129
133
|
def signature_of!(method)
|
130
134
|
T::Utils.signature_for_method(method)
|
135
|
+
rescue LoadError, StandardError
|
136
|
+
Kernel.raise SignatureBlockError
|
131
137
|
end
|
132
138
|
|
133
139
|
#: ((UnboundMethod | Method) method) -> untyped
|
134
140
|
def signature_of(method)
|
135
141
|
signature_of!(method)
|
136
|
-
rescue
|
142
|
+
rescue SignatureBlockError
|
137
143
|
nil
|
138
144
|
end
|
139
145
|
|
@@ -169,23 +175,46 @@ module Tapioca
|
|
169
175
|
T.unsafe(result)
|
170
176
|
end
|
171
177
|
|
178
|
+
#: ((String | Symbol) constant_name) -> SourceLocation?
|
179
|
+
def const_source_location(constant_name)
|
180
|
+
return unless Object.respond_to?(:const_source_location)
|
181
|
+
|
182
|
+
file, line = Object.const_source_location(constant_name)
|
183
|
+
|
184
|
+
SourceLocation.from_loc([file, line]) if file && line
|
185
|
+
end
|
186
|
+
|
172
187
|
# Examines the call stack to identify the closest location where a "require" is performed
|
173
188
|
# by searching for the label "<top (required)>" or "block in <class:...>" in the
|
174
189
|
# case of an ActiveSupport.on_load hook. If none is found, it returns the location
|
175
190
|
# labeled "<main>", which is the original call site.
|
176
|
-
#: (Array[Thread::Backtrace::Location]? locations) ->
|
191
|
+
#: (Array[Thread::Backtrace::Location]? locations) -> SourceLocation?
|
177
192
|
def resolve_loc(locations)
|
178
|
-
return
|
193
|
+
return unless locations
|
179
194
|
|
195
|
+
# Find the location of the closest file load, which should give us the location of the file that
|
196
|
+
# triggered the definition.
|
180
197
|
resolved_loc = locations.find do |loc|
|
181
198
|
label = loc.label
|
182
199
|
next unless label
|
183
200
|
|
184
201
|
REQUIRED_FROM_LABELS.include?(label) || label.start_with?("block in <class:")
|
185
202
|
end
|
186
|
-
return
|
203
|
+
return unless resolved_loc
|
204
|
+
|
205
|
+
resolved_loc_path = resolved_loc.absolute_path || resolved_loc.path
|
206
|
+
|
207
|
+
# Find the location of the last frame in this file to get the most accurate line number.
|
208
|
+
resolved_loc = locations.find { |loc| loc.absolute_path == resolved_loc_path }
|
209
|
+
return unless resolved_loc
|
210
|
+
|
211
|
+
# If the last operation was a `require`, and we have no more frames,
|
212
|
+
# we are probably dealing with a C-method.
|
213
|
+
return if locations.first&.label == "require"
|
214
|
+
|
215
|
+
file = resolved_loc.absolute_path || resolved_loc.path || ""
|
187
216
|
|
188
|
-
resolved_loc.
|
217
|
+
SourceLocation.from_loc([file, resolved_loc.lineno])
|
189
218
|
end
|
190
219
|
|
191
220
|
#: (Module constant) -> Set[String]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Runtime
|
6
|
+
class SourceLocation
|
7
|
+
# this looks something like:
|
8
|
+
# "(eval at /path/to/file.rb:123)"
|
9
|
+
# and we are interested in the "/path/to/file.rb" and "123" parts
|
10
|
+
EVAL_SOURCE_FILE_PATTERN = /^\(eval at (?<file>.+):(?<line>\d+)\)/ #: Regexp
|
11
|
+
|
12
|
+
#: String
|
13
|
+
attr_reader :file
|
14
|
+
|
15
|
+
#: Integer
|
16
|
+
attr_reader :line
|
17
|
+
|
18
|
+
def initialize(file:, line:)
|
19
|
+
# Ruby 3.3 adds automatic definition of source location for evals if
|
20
|
+
# `file` and `line` arguments are not provided. This results in the source
|
21
|
+
# file being something like `(eval at /path/to/file.rb:123)`. We try to parse
|
22
|
+
# this string to get the actual source file.
|
23
|
+
eval_pattern_match = EVAL_SOURCE_FILE_PATTERN.match(file)
|
24
|
+
if eval_pattern_match
|
25
|
+
file = eval_pattern_match[:file]
|
26
|
+
line = eval_pattern_match[:line].to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
@file = file
|
30
|
+
@line = line
|
31
|
+
end
|
32
|
+
|
33
|
+
# force all callers to use the from_loc method
|
34
|
+
private_class_method :new
|
35
|
+
|
36
|
+
class << self
|
37
|
+
#: ([String?, Integer?]? loc) -> SourceLocation?
|
38
|
+
def from_loc(loc)
|
39
|
+
new(file: loc.first, line: loc.last) if loc&.first && loc.last
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -43,7 +43,9 @@ module Tapioca
|
|
43
43
|
Kernel.define_method(:abort, NOOP_METHOD)
|
44
44
|
Kernel.define_method(:exit, NOOP_METHOD)
|
45
45
|
|
46
|
-
|
46
|
+
Tapioca.silence_warnings do
|
47
|
+
block.call
|
48
|
+
end
|
47
49
|
ensure
|
48
50
|
Kernel.define_method(:exit, original_exit)
|
49
51
|
Kernel.define_method(:abort, original_abort)
|
@@ -13,12 +13,7 @@ module Tapioca
|
|
13
13
|
extend Reflection
|
14
14
|
extend T::Sig
|
15
15
|
|
16
|
-
|
17
|
-
const :lineno, Integer
|
18
|
-
const :path, String
|
19
|
-
end
|
20
|
-
|
21
|
-
@class_files = {}.compare_by_identity
|
16
|
+
@class_files = {}.compare_by_identity #: Hash[Module, Set[SourceLocation]]
|
22
17
|
|
23
18
|
# Immediately activated upon load. Observes class/module definition.
|
24
19
|
@class_tracepoint = TracePoint.trace(:class) do |tp|
|
@@ -28,14 +23,17 @@ module Tapioca
|
|
28
23
|
|
29
24
|
path = tp.path
|
30
25
|
if File.exist?(path)
|
31
|
-
loc =
|
26
|
+
loc = build_source_location(tp, caller_locations)
|
32
27
|
else
|
33
28
|
caller_location = T.must(caller_locations)
|
34
29
|
.find { |loc| loc.path && File.exist?(loc.path) }
|
35
30
|
|
36
31
|
next unless caller_location
|
37
32
|
|
38
|
-
loc =
|
33
|
+
loc = SourceLocation.from_loc([
|
34
|
+
caller_location.absolute_path || "",
|
35
|
+
caller_location.lineno,
|
36
|
+
])
|
39
37
|
end
|
40
38
|
|
41
39
|
(@class_files[key] ||= Set.new) << loc
|
@@ -47,31 +45,37 @@ module Tapioca
|
|
47
45
|
key = tp.return_value
|
48
46
|
next unless Module === key
|
49
47
|
|
50
|
-
loc =
|
48
|
+
loc = build_source_location(tp, caller_locations)
|
51
49
|
(@class_files[key] ||= Set.new) << loc
|
52
50
|
end
|
53
51
|
|
54
52
|
class << self
|
53
|
+
extend T::Sig
|
54
|
+
|
55
55
|
def disable!
|
56
56
|
@class_tracepoint.disable
|
57
57
|
@creturn_tracepoint.disable
|
58
58
|
super
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
61
|
+
def build_source_location(tp, locations)
|
62
|
+
loc = resolve_loc(locations)
|
63
|
+
file = loc&.file
|
64
|
+
line = loc&.line
|
65
|
+
lineno = file && File.identical?(file, tp.path) ? tp.lineno : (line || 0)
|
64
66
|
|
65
|
-
|
67
|
+
SourceLocation.from_loc([file || "", lineno])
|
66
68
|
end
|
67
69
|
|
68
70
|
# Returns the files in which this class or module was opened. Doesn't know
|
69
71
|
# about situations where the class was opened prior to +require+ing,
|
70
72
|
# or where metaprogramming was used via +eval+, etc.
|
73
|
+
#: (Module klass) -> Set[String]
|
71
74
|
def files_for(klass)
|
72
|
-
locations_for(klass).map(&:
|
75
|
+
locations_for(klass).map(&:file).to_set
|
73
76
|
end
|
74
77
|
|
78
|
+
#: (Module klass) -> Set[SourceLocation]
|
75
79
|
def locations_for(klass)
|
76
80
|
@class_files.fetch(klass, Set.new)
|
77
81
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Runtime
|
6
|
+
module Trackers
|
7
|
+
module MethodDefinition
|
8
|
+
extend Tracker
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
@method_definitions = {}.compare_by_identity #: Hash[Module, Hash[Symbol, Array[SourceLocation]]]
|
12
|
+
|
13
|
+
class << self
|
14
|
+
#: (Symbol method_name, Module owner, Array[Thread::Backtrace::Location] locations) -> void
|
15
|
+
def register(method_name, owner, locations)
|
16
|
+
return unless enabled?
|
17
|
+
# If Sorbet runtime is redefining a method, it sets this to true.
|
18
|
+
# In those cases, we should skip the registration, as the method's original
|
19
|
+
# definition should already be registered.
|
20
|
+
return if T::Private::DeclState.current.skip_on_method_added
|
21
|
+
|
22
|
+
loc = Reflection.resolve_loc(locations)
|
23
|
+
return unless loc
|
24
|
+
|
25
|
+
registrations_for(method_name, owner) << loc
|
26
|
+
end
|
27
|
+
|
28
|
+
#: (Symbol method_name, Module owner) -> Array[SourceLocation]
|
29
|
+
def method_definitions_for(method_name, owner)
|
30
|
+
definitions = registrations_for(method_name, owner)
|
31
|
+
|
32
|
+
if definitions.empty?
|
33
|
+
source_loc = owner.instance_method(method_name).source_location
|
34
|
+
definitions = [SourceLocation.from_loc(source_loc)].compact
|
35
|
+
end
|
36
|
+
|
37
|
+
definitions
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
#: (Symbol method_name, Module owner) -> Array[SourceLocation]
|
43
|
+
def registrations_for(method_name, owner)
|
44
|
+
owner_lookup = (@method_definitions[owner] ||= {})
|
45
|
+
owner_lookup[method_name] ||= []
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Module
|
54
|
+
prepend(Module.new do
|
55
|
+
def singleton_method_added(method_name)
|
56
|
+
Tapioca::Runtime::Trackers::MethodDefinition.register(method_name, singleton_class, caller_locations)
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_added(method_name)
|
61
|
+
Tapioca::Runtime::Trackers::MethodDefinition.register(method_name, self, caller_locations)
|
62
|
+
super
|
63
|
+
end
|
64
|
+
end)
|
65
|
+
end
|
@@ -32,8 +32,9 @@ module Tapioca
|
|
32
32
|
return unless enabled?
|
33
33
|
|
34
34
|
location = Reflection.resolve_loc(caller_locations)
|
35
|
+
return unless location
|
35
36
|
|
36
|
-
register_with_location(constant, mixin, mixin_type, location)
|
37
|
+
register_with_location(constant, mixin, mixin_type, location.file)
|
37
38
|
end
|
38
39
|
|
39
40
|
def resolve_to_attached_class(constant, mixin, mixin_type)
|
data/lib/tapioca/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tapioca
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.17.
|
4
|
+
version: 0.17.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ufuk Kayserilioglu
|
@@ -281,9 +281,11 @@ files:
|
|
281
281
|
- lib/tapioca/runtime/dynamic_mixin_compiler.rb
|
282
282
|
- lib/tapioca/runtime/generic_type_registry.rb
|
283
283
|
- lib/tapioca/runtime/reflection.rb
|
284
|
+
- lib/tapioca/runtime/source_location.rb
|
284
285
|
- lib/tapioca/runtime/trackers.rb
|
285
286
|
- lib/tapioca/runtime/trackers/autoload.rb
|
286
287
|
- lib/tapioca/runtime/trackers/constant_definition.rb
|
288
|
+
- lib/tapioca/runtime/trackers/method_definition.rb
|
287
289
|
- lib/tapioca/runtime/trackers/mixin.rb
|
288
290
|
- lib/tapioca/runtime/trackers/required_ancestor.rb
|
289
291
|
- lib/tapioca/runtime/trackers/tracker.rb
|