tapioca 0.16.8 → 0.16.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c9011a32f92aa24ce99e2ae84c523d45b390925a30fe43e49c92036c06bbb33d
4
- data.tar.gz: 46e11aa96fc7cb29053f33d2c4a5bc79760d315801f6ba06296e57703bd6d2e6
3
+ metadata.gz: b2d6abd53e84e3b22cc6045597788c98d8f9571f486976baa8db4e5beb6ccd10
4
+ data.tar.gz: 5aa9162275489d2280f07419f74a4243c9ee813f922dd367e6260bae0fa37331
5
5
  SHA512:
6
- metadata.gz: b6d5161d0b4301d62c544cebe5d923867f2530dceb67864a945000998d4eac78737e3ee5eb5ac213b61a7dc00e5e88e92739dcf2fafff900badffdacc8d49315
7
- data.tar.gz: 4f8a54219803ed296644fa9e6fb5312ed7dc7ba57c7f9b89b2bb8a6e25a71920834ed5a0d0d905bc8f1fe26dbcd1a1257bb5d6e203d41ee87adc7d44f5726171
6
+ metadata.gz: 4fd946bacca4a56fe9f9befaa65404393719cacedf4877ce2e8ba0d9d5ab150d9957bea7298783a04e456170086b035b06b42b0312bc2840e1465631259801a1
7
+ data.tar.gz: '0825b9451bed3958b7132008993675de624753d41d5b7b267b50e8b7cf2900f344e5c666ac7726686ebfe9c997592dec6c02b5b2e1d14b144cdbaf837daeac08'
@@ -13,6 +13,7 @@ rescue LoadError
13
13
  end
14
14
 
15
15
  require "zlib"
16
+ require "ruby_lsp/tapioca/run_gem_rbi_check"
16
17
 
17
18
  module RubyLsp
18
19
  module Tapioca
@@ -24,9 +25,10 @@ module RubyLsp
24
25
  super
25
26
 
26
27
  @global_state = T.let(nil, T.nilable(RubyLsp::GlobalState))
27
- @rails_runner_client = T.let(nil, T.nilable(RubyLsp::Rails::RunnerClient))
28
+ @rails_runner_client = T.let(Rails::NullClient.new, RubyLsp::Rails::RunnerClient)
28
29
  @index = T.let(nil, T.nilable(RubyIndexer::Index))
29
30
  @file_checksums = T.let({}, T::Hash[String, String])
31
+ @lockfile_diff = T.let(nil, T.nilable(String))
30
32
  @outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
31
33
  end
32
34
 
@@ -41,7 +43,7 @@ module RubyLsp
41
43
  # Get a handle to the Rails add-on's runtime client. The call to `rails_runner_client` will block this thread
42
44
  # until the server has finished booting, but it will not block the main LSP. This has to happen inside of a
43
45
  # thread
44
- addon = T.cast(::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0.3.17", "< 0.4"), ::RubyLsp::Rails::Addon)
46
+ addon = T.cast(::RubyLsp::Addon.get("Ruby LSP Rails", ">= 0.4.0", "< 0.5"), ::RubyLsp::Rails::Addon)
45
47
  @rails_runner_client = addon.rails_runner_client
46
48
  @outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}")
47
49
  @rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__))
@@ -50,6 +52,8 @@ module RubyLsp
50
52
  request_name: "load_compilers_and_extensions",
51
53
  workspace_path: @global_state.workspace_path,
52
54
  )
55
+
56
+ run_gem_rbi_check
53
57
  rescue IncompatibleApiError
54
58
  # The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking
55
59
  # changes
@@ -71,19 +75,39 @@ module RubyLsp
71
75
 
72
76
  sig { override.returns(String) }
73
77
  def version
74
- "0.1.0"
78
+ "0.1.1"
75
79
  end
76
80
 
77
81
  sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
78
82
  def workspace_did_change_watched_files(changes)
79
83
  return unless T.must(@global_state).enabled_feature?(:tapiocaAddon)
80
- return unless @rails_runner_client # Client is not ready
84
+ return unless @rails_runner_client.connected?
85
+
86
+ has_route_change = T.let(false, T::Boolean)
87
+ has_fixtures_change = T.let(false, T::Boolean)
88
+ needs_compiler_reload = T.let(false, T::Boolean)
81
89
 
82
90
  constants = changes.flat_map do |change|
83
91
  path = URI(change[:uri]).to_standardized_path
84
92
  next if path.end_with?("_test.rb", "_spec.rb")
85
93
  next unless file_updated?(change, path)
86
94
 
95
+ if File.fnmatch?("**/tapioca/**/compilers/**/*.rb", path, File::FNM_PATHNAME)
96
+ needs_compiler_reload = true
97
+ next
98
+ end
99
+
100
+ if File.basename(path) == "routes.rb" || File.fnmatch?("**/routes/**/*.rb", path, File::FNM_PATHNAME)
101
+ has_route_change = true
102
+ next
103
+ end
104
+
105
+ # NOTE: We only get notification for fixtures if ruby-lsp-rails is v0.3.31 or higher
106
+ if File.fnmatch("**/fixtures/**/*.yml{,.erb}", path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
107
+ has_fixtures_change = true
108
+ next
109
+ end
110
+
87
111
  entries = T.must(@index).entries_for(change[:uri])
88
112
  next unless entries
89
113
 
@@ -92,14 +116,33 @@ module RubyLsp
92
116
  end
93
117
  end.compact
94
118
 
95
- return if constants.empty?
119
+ return if constants.empty? && !has_route_change && !has_fixtures_change && !needs_compiler_reload
96
120
 
97
121
  @rails_runner_client.trigger_reload
98
- @rails_runner_client.delegate_notification(
99
- server_addon_name: "Tapioca",
100
- request_name: "dsl",
101
- constants: constants,
102
- )
122
+
123
+ if needs_compiler_reload
124
+ @rails_runner_client.delegate_notification(
125
+ server_addon_name: "Tapioca",
126
+ request_name: "reload_workspace_compilers",
127
+ workspace_path: T.must(@global_state).workspace_path,
128
+ )
129
+ end
130
+
131
+ if has_route_change
132
+ @rails_runner_client.delegate_notification(server_addon_name: "Tapioca", request_name: "route_dsl")
133
+ end
134
+
135
+ if has_fixtures_change
136
+ @rails_runner_client.delegate_notification(server_addon_name: "Tapioca", request_name: "fixtures_dsl")
137
+ end
138
+
139
+ if constants.any?
140
+ @rails_runner_client.delegate_notification(
141
+ server_addon_name: "Tapioca",
142
+ request_name: "dsl",
143
+ constants: constants,
144
+ )
145
+ end
103
146
  end
104
147
 
105
148
  private
@@ -132,6 +175,20 @@ module RubyLsp
132
175
 
133
176
  false
134
177
  end
178
+
179
+ sig { void }
180
+ def run_gem_rbi_check
181
+ gem_rbi_check = RunGemRbiCheck.new(T.must(@global_state).workspace_path)
182
+ gem_rbi_check.run
183
+
184
+ T.must(@outgoing_queue) << Notification.window_log_message(
185
+ gem_rbi_check.stdout,
186
+ ) unless gem_rbi_check.stdout.empty?
187
+ T.must(@outgoing_queue) << Notification.window_log_message(
188
+ gem_rbi_check.stderr,
189
+ type: Constant::MessageType::WARNING,
190
+ ) unless gem_rbi_check.stderr.empty?
191
+ end
135
192
  end
136
193
  end
137
194
  end
@@ -0,0 +1,49 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler"
5
+
6
+ module RubyLsp
7
+ module Tapioca
8
+ class LockfileDiffParser
9
+ GEM_NAME_PATTERN = /[\w\-]+/
10
+ DIFF_LINE_PATTERN = /[+-](.*#{GEM_NAME_PATTERN})\s*\(/
11
+ ADDED_LINE_PATTERN = /^\+.*#{GEM_NAME_PATTERN} \(.*\)/
12
+ REMOVED_LINE_PATTERN = /^-.*#{GEM_NAME_PATTERN} \(.*\)/
13
+
14
+ attr_reader :added_or_modified_gems
15
+ attr_reader :removed_gems
16
+
17
+ def initialize(diff_content, direct_dependencies: nil)
18
+ @diff_content = diff_content.lines
19
+ @current_dependencies = direct_dependencies ||
20
+ Bundler::LockfileParser.new(Bundler.default_lockfile.read).dependencies.keys
21
+ @added_or_modified_gems = parse_added_or_modified_gems
22
+ @removed_gems = parse_removed_gems
23
+ end
24
+
25
+ private
26
+
27
+ def parse_added_or_modified_gems
28
+ @diff_content
29
+ .filter_map { |line| extract_gem(line) if line.match?(ADDED_LINE_PATTERN) }
30
+ .uniq
31
+ end
32
+
33
+ def parse_removed_gems
34
+ @diff_content.filter_map do |line|
35
+ next unless line.match?(REMOVED_LINE_PATTERN)
36
+
37
+ gem = extract_gem(line)
38
+ next if @current_dependencies.include?(gem)
39
+
40
+ gem
41
+ end.uniq
42
+ end
43
+
44
+ def extract_gem(line)
45
+ line.match(DIFF_LINE_PATTERN)[1].strip
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,153 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "open3"
5
+ require "ruby_lsp/tapioca/lockfile_diff_parser"
6
+
7
+ module RubyLsp
8
+ module Tapioca
9
+ class RunGemRbiCheck
10
+ extend T::Sig
11
+
12
+ attr_reader :stdout
13
+ attr_reader :stderr
14
+ attr_reader :status
15
+
16
+ sig { params(project_path: String).void }
17
+ def initialize(project_path)
18
+ @project_path = project_path
19
+ @stdout = T.let("", String)
20
+ @stderr = T.let("", String)
21
+ @status = T.let(nil, T.nilable(Process::Status))
22
+ end
23
+
24
+ sig { void }
25
+ def run
26
+ return log_message("Not a git repository") unless git_repo?
27
+
28
+ cleanup_orphaned_rbis
29
+
30
+ if lockfile_changed?
31
+ generate_gem_rbis
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :project_path
38
+
39
+ sig { returns(T.nilable(T::Boolean)) }
40
+ def git_repo?
41
+ _, status = Open3.capture2e("git", "rev-parse", "--is-inside-work-tree", chdir: project_path)
42
+ status.success?
43
+ end
44
+
45
+ sig { returns(T::Boolean) }
46
+ def lockfile_changed?
47
+ !lockfile_diff.empty?
48
+ end
49
+
50
+ sig { returns(Pathname) }
51
+ def lockfile
52
+ @lockfile ||= T.let(Pathname(project_path).join("Gemfile.lock"), T.nilable(Pathname))
53
+ end
54
+
55
+ sig { returns(String) }
56
+ def lockfile_diff
57
+ @lockfile_diff ||= T.let(read_lockfile_diff, T.nilable(String))
58
+ end
59
+
60
+ sig { returns(String) }
61
+ def read_lockfile_diff
62
+ return "" unless lockfile.exist?
63
+
64
+ execute_in_project_path("git", "diff", lockfile.to_s).strip
65
+ end
66
+
67
+ sig { void }
68
+ def generate_gem_rbis
69
+ parser = Tapioca::LockfileDiffParser.new(@lockfile_diff)
70
+ removed_gems = parser.removed_gems
71
+ added_or_modified_gems = parser.added_or_modified_gems
72
+
73
+ if added_or_modified_gems.any?
74
+ log_message("Identified lockfile changes, attempting to generate gem RBIs...")
75
+ execute_tapioca_gem_command(added_or_modified_gems)
76
+ elsif removed_gems.any?
77
+ remove_rbis(removed_gems)
78
+ end
79
+ end
80
+
81
+ sig { params(gems: T::Array[String]).void }
82
+ def execute_tapioca_gem_command(gems)
83
+ Bundler.with_unbundled_env do
84
+ stdout, stderr, status = T.unsafe(Open3).capture3(
85
+ "bundle",
86
+ "exec",
87
+ "tapioca",
88
+ "gem",
89
+ "--lsp_addon",
90
+ *gems,
91
+ chdir: project_path,
92
+ )
93
+
94
+ log_message(stdout) unless stdout.empty?
95
+ @stderr = stderr unless stderr.empty?
96
+ @status = status
97
+ end
98
+ end
99
+
100
+ sig { params(gems: T::Array[String]).void }
101
+ def remove_rbis(gems)
102
+ files = Dir.glob(
103
+ "sorbet/rbi/gems/{#{gems.join(",")}}@*.rbi",
104
+ base: project_path,
105
+ )
106
+ delete_files(files, "Removed RBIs for")
107
+ end
108
+
109
+ sig { void }
110
+ def cleanup_orphaned_rbis
111
+ untracked_files = git_ls_gem_rbis("--others", "--exclude-standard")
112
+ deleted_files = git_ls_gem_rbis("--deleted")
113
+
114
+ delete_files(untracked_files, "Deleted untracked RBIs")
115
+ restore_files(deleted_files, "Restored deleted RBIs")
116
+ end
117
+
118
+ sig { params(flags: T.untyped).returns(T::Array[String]) }
119
+ def git_ls_gem_rbis(*flags)
120
+ flags = T.unsafe(["git", "ls-files", *flags, "sorbet/rbi/gems/"])
121
+
122
+ execute_in_project_path(*flags)
123
+ .lines
124
+ .map(&:strip)
125
+ end
126
+
127
+ sig { params(files: T::Array[String], message: String).void }
128
+ def delete_files(files, message)
129
+ files_to_remove = files.map { |file| File.join(project_path, file) }
130
+ FileUtils.rm(files_to_remove)
131
+ log_message("#{message}: #{files.join(", ")}") unless files.empty?
132
+ end
133
+
134
+ sig { params(files: T::Array[String], message: String).void }
135
+ def restore_files(files, message)
136
+ execute_in_project_path("git", "checkout", "--pathspec-from-file=-", stdin: files.join("\n"))
137
+ log_message("#{message}: #{files.join(", ")}") unless files.empty?
138
+ end
139
+
140
+ sig { params(message: String).void }
141
+ def log_message(message)
142
+ @stdout += "#{message}\n"
143
+ end
144
+
145
+ def execute_in_project_path(*parts, stdin: nil)
146
+ options = { chdir: project_path }
147
+ options[:stdin_data] = stdin if stdin
148
+ stdout_and_stderr, _status = T.unsafe(Open3).capture2e(*parts, options)
149
+ stdout_and_stderr
150
+ end
151
+ end
152
+ end
153
+ end
@@ -2,6 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "tapioca/internal"
5
+ require "tapioca/dsl/compilers/url_helpers"
6
+ require "tapioca/dsl/compilers/active_record_fixtures"
5
7
 
6
8
  module RubyLsp
7
9
  module Tapioca
@@ -12,27 +14,62 @@ module RubyLsp
12
14
 
13
15
  def execute(request, params)
14
16
  case request
17
+ when "reload_workspace_compilers"
18
+ with_notification_wrapper("reload_workspace_compilers", "Reloading DSL compilers") do
19
+ @loader&.reload_custom_compilers
20
+ end
15
21
  when "load_compilers_and_extensions"
16
22
  # Load DSL extensions and compilers ahead of time, so that we don't have to pay the price of invoking
17
23
  # `Gem.find_files` on every execution, which is quite expensive
18
- ::Tapioca::Loaders::Dsl.new(
24
+ @loader = ::Tapioca::Loaders::Dsl.new(
19
25
  tapioca_path: ::Tapioca::TAPIOCA_DIR,
20
26
  eager_load: false,
21
27
  app_root: params[:workspace_path],
22
28
  halt_upon_load_error: false,
23
- ).load_dsl_extensions_and_compilers
29
+ )
30
+ @loader.load_dsl_extensions_and_compilers
24
31
  when "dsl"
25
32
  fork do
26
- dsl(params)
33
+ with_notification_wrapper("dsl", "Generating DSL RBIs") do
34
+ dsl(params[:constants])
35
+ end
36
+ end
37
+ when "route_dsl"
38
+ fork do
39
+ with_notification_wrapper("route_dsl", "Generating route DSL RBIs") do
40
+ constants = ::Tapioca::Dsl::Compilers::UrlHelpers.gather_constants
41
+ dsl(constants.map(&:name), "--only=Tapioca::Dsl::Compilers::UrlHelpers", "ActiveSupportConcern")
42
+ end
43
+ end
44
+ when "fixtures_dsl"
45
+ fork do
46
+ with_notification_wrapper("fixture_dsl", "Generating fixture DSL RBIs") do
47
+ constants = ::Tapioca::Dsl::Compilers::ActiveRecordFixtures.gather_constants
48
+ dsl(constants.map(&:name), "--only=Tapioca::Dsl::Compilers::ActiveRecordFixtures")
49
+ end
27
50
  end
28
51
  end
29
52
  end
30
53
 
31
54
  private
32
55
 
33
- def dsl(params)
56
+ def with_notification_wrapper(request_name, title, &block)
57
+ with_progress(request_name, title) do
58
+ with_notification_error_handling(request_name, &block)
59
+ end
60
+ end
61
+
62
+ def dsl(constants, *args)
34
63
  load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests
35
- ::Tapioca::Cli.start(["dsl", "--lsp_addon", "--workers=1"] + params[:constants])
64
+
65
+ # Order here is important to avoid having Thor confuse arguments. Do not put an array argument at the end before
66
+ # the list of constants
67
+ arguments = ["dsl"]
68
+ arguments.concat(args)
69
+ arguments.push("--lsp_addon", "--workers=1")
70
+ arguments.concat(constants)
71
+
72
+ ::Tapioca::Cli.start(arguments)
36
73
  end
37
74
  end
38
75
  end
@@ -374,7 +374,7 @@ module Tapioca
374
374
 
375
375
  klass.create_method(
376
376
  "size",
377
- return_type: "Integer",
377
+ return_type: "T::Hash[T.untyped, Integer]",
378
378
  )
379
379
 
380
380
  CALCULATION_METHODS.each do |method_name|
@@ -57,7 +57,7 @@ module Tapioca
57
57
  # we can clear the gem version if the gem is the same one we are processing
58
58
  version = "" if gem == @pipeline.gem
59
59
 
60
- uri = URI::Source.build(
60
+ uri = SourceURI.build(
61
61
  gem_name: gem.name,
62
62
  gem_version: version,
63
63
  path: path.to_s,
@@ -3,8 +3,8 @@
3
3
 
4
4
  require "uri/file"
5
5
 
6
- module URI
7
- class Source < URI::File
6
+ module Tapioca
7
+ class SourceURI < URI::File
8
8
  extend T::Sig
9
9
 
10
10
  COMPONENT = T.let(
@@ -37,7 +37,7 @@ module URI
37
37
  gem_version: T.nilable(String),
38
38
  path: String,
39
39
  line_number: T.nilable(String),
40
- ).returns(URI::Source)
40
+ ).returns(T.attached_class)
41
41
  end
42
42
  def build(gem_name:, gem_version:, path:, line_number:)
43
43
  super(
@@ -45,6 +45,27 @@ module Tapioca
45
45
  load_dsl_compilers
46
46
  end
47
47
 
48
+ sig { void }
49
+ def reload_custom_compilers
50
+ # Remove all loaded custom compilers
51
+ ::Tapioca::Dsl::Compiler.descendants.each do |compiler|
52
+ name = compiler.name
53
+ next unless name && @custom_compiler_paths.include?(Module.const_source_location(name)&.first)
54
+
55
+ *parts, unqualified_name = name.split("::")
56
+
57
+ if parts.empty?
58
+ Object.send(:remove_const, unqualified_name)
59
+ else
60
+ parts.join("::").safe_constantize.send(:remove_const, unqualified_name)
61
+ end
62
+ end
63
+
64
+ # Remove from $LOADED_FEATURES each workspace compiler file and then re-load
65
+ @custom_compiler_paths.each { |path| $LOADED_FEATURES.delete(path) }
66
+ load_custom_dsl_compilers
67
+ end
68
+
48
69
  protected
49
70
 
50
71
  sig do
@@ -57,6 +78,7 @@ module Tapioca
57
78
  @eager_load = eager_load
58
79
  @app_root = app_root
59
80
  @halt_upon_load_error = halt_upon_load_error
81
+ @custom_compiler_paths = T.let([], T::Array[String])
60
82
  end
61
83
 
62
84
  sig { void }
@@ -89,12 +111,7 @@ module Tapioca
89
111
  end
90
112
 
91
113
  # Load all custom compilers from the project
92
- Dir.glob([
93
- "#{@tapioca_path}/generators/**/*.rb", # TODO: Here for backcompat, remove later
94
- "#{@tapioca_path}/compilers/**/*.rb",
95
- ]).each do |compiler|
96
- require File.expand_path(compiler)
97
- end
114
+ load_custom_dsl_compilers
98
115
 
99
116
  say("Done", :green)
100
117
  end
@@ -112,6 +129,17 @@ module Tapioca
112
129
 
113
130
  say("Done", :green)
114
131
  end
132
+
133
+ private
134
+
135
+ sig { void }
136
+ def load_custom_dsl_compilers
137
+ @custom_compiler_paths = Dir.glob([
138
+ "#{@tapioca_path}/generators/**/*.rb", # TODO: Here for backcompat, remove later
139
+ "#{@tapioca_path}/compilers/**/*.rb",
140
+ ])
141
+ @custom_compiler_paths.each { |compiler| require File.expand_path(compiler) }
142
+ end
115
143
  end
116
144
  end
117
145
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Tapioca
5
- VERSION = "0.16.8"
5
+ VERSION = "0.16.9"
6
6
  end
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.16.8
4
+ version: 0.16.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
@@ -10,7 +10,7 @@ authors:
10
10
  - Peter Zhu
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2025-01-15 00:00:00.000000000 Z
13
+ date: 2025-02-04 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: benchmark
@@ -149,6 +149,8 @@ files:
149
149
  - README.md
150
150
  - exe/tapioca
151
151
  - lib/ruby_lsp/tapioca/addon.rb
152
+ - lib/ruby_lsp/tapioca/lockfile_diff_parser.rb
153
+ - lib/ruby_lsp/tapioca/run_gem_rbi_check.rb
152
154
  - lib/ruby_lsp/tapioca/server_addon.rb
153
155
  - lib/tapioca.rb
154
156
  - lib/tapioca/bundler_ext/auto_require_hook.rb
@@ -297,7 +299,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
297
299
  - !ruby/object:Gem::Version
298
300
  version: '0'
299
301
  requirements: []
300
- rubygems_version: 3.6.2
302
+ rubygems_version: 3.6.3
301
303
  specification_version: 4
302
304
  summary: A Ruby Interface file generator for gems, core types and the Ruby standard
303
305
  library