tapioca 0.16.8 → 0.16.9

Sign up to get free protection for your applications and to get access to all the features.
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