steep 0.42.0 → 0.43.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/lib/steep.rb +4 -3
  4. data/lib/steep/annotation_parser.rb +10 -2
  5. data/lib/steep/cli.rb +1 -0
  6. data/lib/steep/diagnostic/ruby.rb +15 -6
  7. data/lib/steep/diagnostic/signature.rb +28 -11
  8. data/lib/steep/drivers/annotations.rb +1 -3
  9. data/lib/steep/drivers/check.rb +17 -7
  10. data/lib/steep/drivers/diagnostic_printer.rb +4 -0
  11. data/lib/steep/drivers/langserver.rb +1 -0
  12. data/lib/steep/drivers/print_project.rb +1 -1
  13. data/lib/steep/drivers/stats.rb +125 -105
  14. data/lib/steep/drivers/utils/driver_helper.rb +35 -0
  15. data/lib/steep/drivers/validate.rb +1 -1
  16. data/lib/steep/drivers/watch.rb +12 -10
  17. data/lib/steep/index/signature_symbol_provider.rb +20 -6
  18. data/lib/steep/project/target.rb +4 -4
  19. data/lib/steep/server/interaction_worker.rb +2 -3
  20. data/lib/steep/server/master.rb +621 -170
  21. data/lib/steep/server/type_check_worker.rb +127 -13
  22. data/lib/steep/server/worker_process.rb +7 -4
  23. data/lib/steep/services/completion_provider.rb +2 -2
  24. data/lib/steep/services/hover_content.rb +5 -4
  25. data/lib/steep/services/path_assignment.rb +6 -8
  26. data/lib/steep/services/signature_service.rb +43 -9
  27. data/lib/steep/services/type_check_service.rb +184 -138
  28. data/lib/steep/signature/validator.rb +17 -9
  29. data/lib/steep/source.rb +21 -18
  30. data/lib/steep/subtyping/constraints.rb +2 -2
  31. data/lib/steep/type_construction.rb +223 -125
  32. data/lib/steep/type_inference/block_params.rb +1 -1
  33. data/lib/steep/type_inference/context.rb +22 -0
  34. data/lib/steep/type_inference/logic.rb +1 -1
  35. data/lib/steep/type_inference/logic_type_interpreter.rb +3 -3
  36. data/lib/steep/version.rb +1 -1
  37. data/smoke/implements/b.rb +13 -0
  38. data/smoke/implements/b.rbs +12 -0
  39. data/smoke/regression/issue_328.rb +1 -0
  40. data/smoke/regression/issue_328.rbs +0 -0
  41. data/smoke/regression/issue_332.rb +11 -0
  42. data/smoke/regression/issue_332.rbs +19 -0
  43. data/smoke/regression/masgn.rb +4 -0
  44. data/smoke/regression/test_expectations.yml +29 -0
  45. data/smoke/regression/thread.rb +7 -0
  46. data/smoke/super/test_expectations.yml +2 -12
  47. data/steep.gemspec +2 -2
  48. metadata +40 -20
@@ -3,10 +3,14 @@ module Steep
3
3
  class TypeCheckWorker < BaseWorker
4
4
  attr_reader :project, :assignment, :service
5
5
  attr_reader :commandline_args
6
+ attr_reader :current_type_check_guid
6
7
 
7
- TypeCheckJob = Class.new
8
8
  WorkspaceSymbolJob = Struct.new(:query, :id, keyword_init: true)
9
9
  StatsJob = Struct.new(:id, keyword_init: true)
10
+ StartTypeCheckJob = Struct.new(:guid, :changes, keyword_init: true)
11
+ TypeCheckCodeJob = Struct.new(:guid, :path, keyword_init: true)
12
+ ValidateAppSignatureJob = Struct.new(:guid, :path, keyword_init: true)
13
+ ValidateLibrarySignatureJob = Struct.new(:guid, :path, keyword_init: true)
10
14
 
11
15
  include ChangeBuffer
12
16
 
@@ -14,22 +18,21 @@ module Steep
14
18
  super(project: project, reader: reader, writer: writer)
15
19
 
16
20
  @assignment = assignment
17
- @service = Services::TypeCheckService.new(project: project, assignment: assignment)
21
+ @service = Services::TypeCheckService.new(project: project)
18
22
  @buffered_changes = {}
19
23
  @mutex = Mutex.new()
20
24
  @queue = Queue.new
21
25
  @commandline_args = commandline_args
26
+ @current_type_check_guid = nil
22
27
  end
23
28
 
24
29
  def handle_request(request)
25
30
  case request[:method]
26
31
  when "initialize"
27
32
  load_files(project: project, commandline_args: commandline_args)
28
- queue << TypeCheckJob.new()
29
33
  writer.write({ id: request[:id], result: nil})
30
34
  when "textDocument/didChange"
31
35
  collect_changes(request)
32
- queue << TypeCheckJob.new()
33
36
  when "workspace/symbol"
34
37
  query = request[:params][:query]
35
38
  queue << WorkspaceSymbolJob.new(id: request[:id], query: query)
@@ -38,31 +41,134 @@ module Steep
38
41
  when "steep/stats"
39
42
  queue << StatsJob.new(id: request[:id])
40
43
  end
44
+ when "$/typecheck/start"
45
+ params = request[:params]
46
+ enqueue_typecheck_jobs(params)
47
+ end
48
+ end
49
+
50
+ def enqueue_typecheck_jobs(params)
51
+ guid = params[:guid]
52
+
53
+ @current_type_check_guid = guid
54
+
55
+ pop_buffer() do |changes|
56
+ Steep.logger.info { "Enqueueing StartTypeCheckJob for guid=#{guid}" }
57
+ queue << StartTypeCheckJob.new(guid: guid, changes: changes)
58
+ end
59
+
60
+ priority_paths = Set.new(params[:priority_uris].map {|uri| Pathname(URI.parse(uri).path) })
61
+ library_paths = params[:library_uris].map {|uri| Pathname(URI.parse(uri).path) }
62
+ signature_paths = params[:signature_uris].map {|uri| Pathname(URI.parse(uri).path) }
63
+ code_paths = params[:code_uris].map {|uri| Pathname(URI.parse(uri).path) }
64
+
65
+ library_paths.each do |path|
66
+ if priority_paths.include?(path)
67
+ Steep.logger.info { "Enqueueing ValidateLibrarySignatureJob for guid=#{guid}, path=#{path}" }
68
+ queue << ValidateLibrarySignatureJob.new(guid: guid, path: path)
69
+ end
70
+ end
71
+
72
+ code_paths.each do |path|
73
+ if priority_paths.include?(path)
74
+ Steep.logger.info { "Enqueueing TypeCheckCodeJob for guid=#{guid}, path=#{path}" }
75
+ queue << TypeCheckCodeJob.new(guid: guid, path: path)
76
+ end
77
+ end
78
+
79
+ signature_paths.each do |path|
80
+ if priority_paths.include?(path)
81
+ Steep.logger.info { "Enqueueing ValidateAppSignatureJob for guid=#{guid}, path=#{path}" }
82
+ queue << ValidateAppSignatureJob.new(guid: guid, path: path)
83
+ end
84
+ end
85
+
86
+ library_paths.each do |path|
87
+ unless priority_paths.include?(path)
88
+ Steep.logger.info { "Enqueueing ValidateLibrarySignatureJob for guid=#{guid}, path=#{path}" }
89
+ queue << ValidateLibrarySignatureJob.new(guid: guid, path: path)
90
+ end
91
+ end
92
+
93
+ code_paths.each do |path|
94
+ unless priority_paths.include?(path)
95
+ Steep.logger.info { "Enqueueing TypeCheckCodeJob for guid=#{guid}, path=#{path}" }
96
+ queue << TypeCheckCodeJob.new(guid: guid, path: path)
97
+ end
98
+ end
99
+
100
+ signature_paths.each do |path|
101
+ unless priority_paths.include?(path)
102
+ Steep.logger.info { "Enqueueing ValidateAppSignatureJob for guid=#{guid}, path=#{path}" }
103
+ queue << ValidateAppSignatureJob.new(guid: guid, path: path)
104
+ end
41
105
  end
42
106
  end
43
107
 
44
108
  def handle_job(job)
45
109
  case job
46
- when TypeCheckJob
47
- pop_buffer() do |changes|
48
- break if changes.empty?
110
+ when StartTypeCheckJob
111
+ Steep.logger.info { "Processing StartTypeCheckJob for guid=#{job.guid}" }
112
+ service.update(changes: job.changes)
113
+
114
+ when ValidateAppSignatureJob
115
+ if job.guid == current_type_check_guid
116
+ Steep.logger.info { "Processing ValidateAppSignature for guid=#{job.guid}, path=#{job.path}" }
117
+ service.validate_signature(path: project.relative_path(job.path)) do |path, diagnostics|
118
+ formatter = Diagnostic::LSPFormatter.new()
49
119
 
50
- formatter = Diagnostic::LSPFormatter.new()
120
+ writer.write(
121
+ method: :"textDocument/publishDiagnostics",
122
+ params: LSP::Interface::PublishDiagnosticsParams.new(
123
+ uri: URI.parse(job.path.to_s).tap {|uri| uri.scheme = "file"},
124
+ diagnostics: diagnostics.map {|diagnostic| formatter.format(diagnostic) }.uniq
125
+ )
126
+ )
127
+ end
51
128
 
52
- service.update(changes: changes) do |path, diagnostics|
129
+ typecheck_progress(path: job.path, guid: job.guid)
130
+ end
131
+
132
+ when ValidateLibrarySignatureJob
133
+ if job.guid == current_type_check_guid
134
+ Steep.logger.info { "Processing ValidateLibrarySignature for guid=#{job.guid}, path=#{job.path}" }
135
+ service.validate_signature(path: job.path) do |path, diagnostics|
136
+ formatter = Diagnostic::LSPFormatter.new()
137
+
138
+ writer.write(
139
+ method: :"textDocument/publishDiagnostics",
140
+ params: LSP::Interface::PublishDiagnosticsParams.new(
141
+ uri: URI.parse(job.path.to_s).tap {|uri| uri.scheme = "file"},
142
+ diagnostics: diagnostics.map {|diagnostic| formatter.format(diagnostic) }.uniq
143
+ )
144
+ )
145
+ end
146
+
147
+ typecheck_progress(path: job.path, guid: job.guid)
148
+ end
149
+
150
+ when TypeCheckCodeJob
151
+ if job.guid == current_type_check_guid
152
+ Steep.logger.info { "Processing TypeCheckCodeJob for guid=#{job.guid}, path=#{job.path}" }
153
+ service.typecheck_source(path: project.relative_path(job.path)) do |path, diagnostics|
53
154
  if target = project.target_for_source_path(path)
54
155
  diagnostics = diagnostics.select {|diagnostic| target.options.error_to_report?(diagnostic) }
55
156
  end
56
157
 
158
+ formatter = Diagnostic::LSPFormatter.new()
159
+
57
160
  writer.write(
58
161
  method: :"textDocument/publishDiagnostics",
59
162
  params: LSP::Interface::PublishDiagnosticsParams.new(
60
- uri: URI.parse(project.absolute_path(path).to_s).tap {|uri| uri.scheme = "file"},
163
+ uri: URI.parse(job.path.to_s).tap {|uri| uri.scheme = "file"},
61
164
  diagnostics: diagnostics.map {|diagnostic| formatter.format(diagnostic) }.uniq
62
165
  )
63
166
  )
64
167
  end
168
+
169
+ typecheck_progress(path: job.path, guid: job.guid)
65
170
  end
171
+
66
172
  when WorkspaceSymbolJob
67
173
  writer.write(
68
174
  id: job.id,
@@ -76,14 +182,21 @@ module Steep
76
182
  end
77
183
  end
78
184
 
185
+ def typecheck_progress(guid:, path:)
186
+ writer.write(
187
+ method: "$/typecheck/progress",
188
+ params: { guid: guid, path: path }
189
+ )
190
+ end
191
+
79
192
  def workspace_symbol_result(query)
80
193
  Steep.measure "Generating workspace symbol list for query=`#{query}`" do
81
194
  indexes = project.targets.map {|target| service.signature_services[target.name].latest_rbs_index }
82
195
 
83
- provider = Index::SignatureSymbolProvider.new()
196
+ provider = Index::SignatureSymbolProvider.new(project: project, assignment: assignment)
84
197
  provider.indexes.push(*indexes)
85
198
 
86
- symbols = provider.query_symbol(query, assignment: assignment)
199
+ symbols = provider.query_symbol(query)
87
200
 
88
201
  symbols.map do |symbol|
89
202
  LSP::Interface::SymbolInformation.new(
@@ -110,8 +223,9 @@ module Steep
110
223
 
111
224
  project.targets.each.with_object([]) do |target, stats|
112
225
  service.source_files.each_value do |file|
113
- next unless assignment =~ file.path
114
226
  next unless target.possible_source_file?(file.path)
227
+ absolute_path = project.absolute_path(file.path)
228
+ next unless assignment =~ absolute_path
115
229
 
116
230
  stats << calculator.calc_stats(target, file: file)
117
231
  end
@@ -7,16 +7,18 @@ module Steep
7
7
 
8
8
  attr_reader :name
9
9
  attr_reader :wait_thread
10
+ attr_reader :index
10
11
 
11
- def initialize(reader:, writer:, stderr:, wait_thread:, name:)
12
+ def initialize(reader:, writer:, stderr:, wait_thread:, name:, index: nil)
12
13
  @reader = reader
13
14
  @writer = writer
14
15
  @stderr = stderr
15
16
  @wait_thread = wait_thread
16
17
  @name = name
18
+ @index = index
17
19
  end
18
20
 
19
- def self.spawn_worker(type, name:, steepfile:, options: [], delay_shutdown: false)
21
+ def self.spawn_worker(type, name:, steepfile:, options: [], delay_shutdown: false, index: nil)
20
22
  log_level = %w(debug info warn error fatal unknown)[Steep.logger.level]
21
23
  command = case type
22
24
  when :interaction
@@ -37,7 +39,7 @@ module Steep
37
39
  writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdin)
38
40
  reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdout)
39
41
 
40
- new(reader: reader, writer: writer, stderr: stderr, wait_thread: thread, name: name)
42
+ new(reader: reader, writer: writer, stderr: stderr, wait_thread: thread, name: name, index: index)
41
43
  end
42
44
 
43
45
  def self.spawn_typecheck_workers(steepfile:, args:, count: [Etc.nprocessors - 1, 1].max, delay_shutdown: false)
@@ -46,7 +48,8 @@ module Steep
46
48
  name: "typecheck@#{i}",
47
49
  steepfile: steepfile,
48
50
  options: ["--max-index=#{count}", "--index=#{i}", *args],
49
- delay_shutdown: delay_shutdown)
51
+ delay_shutdown: delay_shutdown,
52
+ index: i)
50
53
  end
51
54
  end
52
55
 
@@ -82,7 +82,7 @@ module Steep
82
82
  end
83
83
 
84
84
  rescue Parser::SyntaxError => exn
85
- Steep.logger.error "recovering syntax error: #{exn.inspect}"
85
+ Steep.logger.info "recovering syntax error: #{exn.inspect}"
86
86
  case possible_trigger
87
87
  when "."
88
88
  source_text[index-1] = " "
@@ -143,7 +143,7 @@ module Steep
143
143
 
144
144
  when node.type == :lvar && at_end?(position, of: node.loc)
145
145
  # foo ← (lvar)
146
- local_variable_items_for_context(context, position: position, prefix: node.children[0].name.to_s, items: items)
146
+ local_variable_items_for_context(context, position: position, prefix: node.children[0].to_s, items: items)
147
147
 
148
148
  when node.type == :send && node.children[0] && at_end?(position, of: node.loc.selector)
149
149
  # foo.ba ←
@@ -43,6 +43,7 @@ module Steep
43
43
  def typecheck(target, path:, content:, line:, column:)
44
44
  subtyping = service.signature_services[target.name].current_subtyping or return
45
45
  source = Source.parse(content, path: path, factory: subtyping.factory)
46
+ source = source.without_unrelated_defs(line: line, column: column)
46
47
  Services::TypeCheckService.type_check(source: source, subtyping: subtyping)
47
48
  rescue
48
49
  nil
@@ -62,15 +63,15 @@ module Steep
62
63
  when :lvar
63
64
  var_name = node.children[0]
64
65
  context = typing.context_at(line: line, column: column)
65
- var_type = context.lvar_env[var_name.name] || AST::Types::Any.new(location: nil)
66
+ var_type = context.lvar_env[var_name] || AST::Types::Any.new(location: nil)
66
67
 
67
- VariableContent.new(node: node, name: var_name.name, type: var_type, location: node.location.name)
68
+ VariableContent.new(node: node, name: var_name, type: var_type, location: node.location.name)
68
69
  when :lvasgn
69
70
  var_name, rhs = node.children
70
71
  context = typing.context_at(line: line, column: column)
71
- type = context.lvar_env[var_name.name] || typing.type_of(node: rhs)
72
+ type = context.lvar_env[var_name] || typing.type_of(node: rhs)
72
73
 
73
- VariableContent.new(node: node, name: var_name.name, type: type, location: node.location.name)
74
+ VariableContent.new(node: node, name: var_name, type: type, location: node.location.name)
74
75
  when :send
75
76
  receiver, method_name, *_ = node.children
76
77
 
@@ -14,15 +14,13 @@ module Steep
14
14
  end
15
15
 
16
16
  def =~(path)
17
- path = path.to_s
17
+ (cache[path] ||= self.class.index_for(path: path.to_s, max_index: max_index)) == index
18
+ end
19
+
20
+ alias === =~
18
21
 
19
- if cache.key?(path)
20
- cache[path]
21
- else
22
- value = Digest::MD5.hexdigest(path).hex % max_index == index
23
- cache[path] = value
24
- value
25
- end
22
+ def self.index_for(path:, max_index:)
23
+ Digest::MD5.hexdigest(path).hex % max_index
26
24
  end
27
25
  end
28
26
  end
@@ -71,11 +71,17 @@ module Steep
71
71
  new(env: env)
72
72
  end
73
73
 
74
+ def env_rbs_paths
75
+ @env_rbs_paths ||= latest_env.buffers.each.with_object(Set[]) do |buffer, set|
76
+ set << Pathname(buffer.name)
77
+ end
78
+ end
79
+
74
80
  def each_rbs_path(&block)
75
81
  if block
76
- latest_env.buffers.each do |buffer|
77
- unless files.key?(buffer.name)
78
- yield Pathname(buffer.name)
82
+ env_rbs_paths.each do |path|
83
+ unless files.key?(path)
84
+ yield path
79
85
  end
80
86
  end
81
87
 
@@ -133,6 +139,12 @@ module Steep
133
139
 
134
140
  update[path] = begin
135
141
  FileStatus.new(path: path, content: content, decls: RBS::Parser.parse_signature(buffer))
142
+ rescue ArgumentError => exn
143
+ error = Diagnostic::Signature::UnexpectedError.new(
144
+ message: exn.message,
145
+ location: RBS::Location.new(buffer: buffer, start_pos: 0, end_pos: content.size)
146
+ )
147
+ FileStatus.new(path: path, content: content, decls: error)
136
148
  rescue RBS::ParsingError => exn
137
149
  FileStatus.new(path: path, content: content, decls: exn)
138
150
  end
@@ -148,13 +160,18 @@ module Steep
148
160
  paths = Set.new(updates.each_key)
149
161
  paths.merge(pending_changed_paths)
150
162
 
151
- if updates.each_value.any? {|file| file.decls.is_a?(RBS::ParsingError) }
163
+ if updates.each_value.any? {|file| !file.decls.is_a?(Array) }
152
164
  diagnostics = []
153
165
 
154
166
  updates.each_value do |file|
155
- if file.decls.is_a?(RBS::ParsingError)
156
- # factory is not used here because the error is a syntax error.
157
- diagnostics << Diagnostic::Signature.from_rbs_error(file.decls, factory: nil)
167
+ unless file.decls.is_a?(Array)
168
+ diagnostic = if file.decls.is_a?(Diagnostic::Signature::Base)
169
+ file.decls
170
+ else
171
+ # factory is not used here because the error is a syntax error.
172
+ Diagnostic::Signature.from_rbs_error(file.decls, factory: nil)
173
+ end
174
+ diagnostics << diagnostic
158
175
  end
159
176
  end
160
177
 
@@ -206,7 +223,8 @@ module Steep
206
223
 
207
224
  Steep.measure "Loading new decls" do
208
225
  updated_files.each_value do |content|
209
- if content.decls.is_a?(RBS::ErrorBase)
226
+ case decls = content.decls
227
+ when RBS::ErrorBase
210
228
  errors << content.decls
211
229
  else
212
230
  begin
@@ -300,13 +318,29 @@ module Steep
300
318
  def type_names(paths:, env:)
301
319
  env.declarations.each.with_object(Set[]) do |decl, set|
302
320
  if decl.location
303
- if paths.include?(decl.location.buffer.name)
321
+ if paths.include?(Pathname(decl.location.buffer.name))
304
322
  type_name_from_decl(decl, set: set)
305
323
  end
306
324
  end
307
325
  end
308
326
  end
309
327
 
328
+ def const_decls(paths:, env:)
329
+ env.constant_decls.filter do |_, entry|
330
+ if location = entry.decl.location
331
+ paths.include?(Pathname(location.buffer.name))
332
+ end
333
+ end
334
+ end
335
+
336
+ def global_decls(paths:, env: latest_env)
337
+ env.global_decls.filter do |_, entry|
338
+ if location = entry.decl.location
339
+ paths.include?(Pathname(location.buffer.name))
340
+ end
341
+ end
342
+ end
343
+
310
344
  def type_name_from_decl(decl, set:)
311
345
  case decl
312
346
  when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module, RBS::AST::Declarations::Interface
@@ -2,7 +2,6 @@ module Steep
2
2
  module Services
3
3
  class TypeCheckService
4
4
  attr_reader :project
5
- attr_reader :assignment
6
5
  attr_reader :signature_validation_diagnostics
7
6
  attr_reader :source_files
8
7
  attr_reader :signature_services
@@ -50,27 +49,54 @@ module Steep
50
49
  end
51
50
  end
52
51
 
53
- def initialize(project:, assignment:)
54
- @project = project
55
- @assignment = assignment
52
+ class TargetRequest
53
+ attr_reader :target
54
+ attr_reader :source_paths
56
55
 
57
- @signature_validation_diagnostics = {}
58
- @source_files = {}
59
- @signature_services = project.targets.each.with_object({}) do |target, hash|
60
- loader = Project::Target.construct_env_loader(options: target.options)
61
- hash[target.name] = SignatureService.load_from(loader)
56
+ def initialize(target:)
57
+ @target = target
58
+ @source_paths = Set[]
59
+ @signature_updated = false
62
60
  end
63
61
 
64
- @no_type_checking = false
65
- end
62
+ def signature_updated!(value = true)
63
+ @signature_updated = value
64
+ self
65
+ end
66
+
67
+ def signature_updated?
68
+ @signature_updated
69
+ end
70
+
71
+ def empty?
72
+ !signature_updated? && source_paths.empty?
73
+ end
74
+
75
+ def ==(other)
76
+ other.is_a?(TargetRequest) &&
77
+ other.target == target &&
78
+ other.source_paths == source_paths &&
79
+ other.signature_updated? == signature_updated?
80
+ end
66
81
 
67
- def no_type_checking!
68
- @no_type_checking = true
69
- self
82
+ alias eql? ==
83
+
84
+ def hash
85
+ self.class.hash ^ target.hash ^ source_paths.hash ^ @signature_updated.hash
86
+ end
70
87
  end
71
88
 
72
- def no_type_checking?
73
- @no_type_checking
89
+ def initialize(project:)
90
+ @project = project
91
+
92
+ @source_files = {}
93
+ @signature_services = project.targets.each.with_object({}) do |target, hash|
94
+ loader = Project::Target.construct_env_loader(options: target.options, project: project)
95
+ hash[target.name] = SignatureService.load_from(loader)
96
+ end
97
+ @signature_validation_diagnostics = project.targets.each.with_object({}) do |target, hash|
98
+ hash[target.name] = {}
99
+ end
74
100
  end
75
101
 
76
102
  def signature_diagnostics
@@ -80,24 +106,18 @@ module Steep
80
106
  service = signature_services[target.name]
81
107
 
82
108
  service.each_rbs_path do |path|
83
- if assignment =~ path
84
- signature_diagnostics[path] ||= []
85
- end
109
+ signature_diagnostics[path] ||= []
86
110
  end
87
111
 
88
112
  case service.status
89
113
  when SignatureService::SyntaxErrorStatus, SignatureService::AncestorErrorStatus
90
114
  service.status.diagnostics.group_by {|diag| Pathname(diag.location.buffer.name) }.each do |path, diagnostics|
91
- if assignment =~ path
92
- signature_diagnostics[path].push(*diagnostics)
93
- end
115
+ signature_diagnostics[path].push(*diagnostics)
94
116
  end
95
117
  when SignatureService::LoadedStatus
96
118
  validation_diagnostics = signature_validation_diagnostics[target.name] || {}
97
119
  validation_diagnostics.each do |path, diagnostics|
98
- if assignment =~ path
99
- signature_diagnostics[path].push(*diagnostics)
100
- end
120
+ signature_diagnostics[path].push(*diagnostics)
101
121
  end
102
122
  end
103
123
  end
@@ -127,166 +147,192 @@ module Steep
127
147
  end
128
148
  end
129
149
 
130
- def update(changes:, &block)
131
- updated_targets = Steep.measure "#update_signature" do
132
- update_signature(changes: changes, &block)
150
+ def update(changes:)
151
+ requests = project.targets.each_with_object({}.compare_by_identity) do |target, hash|
152
+ hash[target] = TargetRequest.new(target: target)
133
153
  end
134
- Steep.measure2 "Type check target" do |sampler|
135
- project.targets.each do |target|
136
- sampler.sample target.name.to_s do
137
- update_target(target: target, changes: changes, updated: updated_targets.include?(target), &block)
138
- end
139
- end
154
+
155
+ Steep.measure "#update_signature" do
156
+ update_signature(changes: changes, requests: requests)
140
157
  end
158
+
159
+ Steep.measure "#update_sources" do
160
+ update_sources(changes: changes, requests: requests)
161
+ end
162
+
163
+ requests.transform_keys(&:name).reject {|_, request| request.empty? }
141
164
  end
142
165
 
143
- def update_signature(changes:, &block)
144
- Steep.logger.tagged "#update_signature" do
145
- updated_targets = []
166
+ def update_and_check(changes:, assignment:, &block)
167
+ requests = update(changes: changes)
146
168
 
147
- Steep.measure2 "Update signatures" do |sampler|
148
- project.targets.each do |target|
149
- sampler.sample target.name.to_s do
150
- signature_service = signature_services[target.name]
151
- signature_changes = changes.filter {|path, _| target.possible_signature_file?(path) }
169
+ signatures = requests.each_value.with_object(Set[]) do |request, sigs|
170
+ if request.signature_updated?
171
+ service = signature_services[request.target.name]
172
+ sigs.merge(service.each_rbs_path)
173
+ end
174
+ end
152
175
 
153
- unless signature_changes.empty?
154
- updated_targets << target
155
- signature_service.update(signature_changes)
156
- end
157
- end
176
+ signatures.each do |path|
177
+ if assignment =~ path
178
+ validate_signature(path: path, &block)
179
+ end
180
+ end
181
+
182
+ requests.each_value do |request|
183
+ request.source_paths.each do |path|
184
+ if assignment =~ path
185
+ typecheck_source(path: path, target: request.target, &block)
158
186
  end
159
187
  end
188
+ end
189
+ end
160
190
 
161
- Steep.measure "signature validations" do
162
- accumulated_diagnostics = {}
191
+ def validate_signature(path:, &block)
192
+ Steep.logger.tagged "#validate_signature(path=#{path})" do
193
+ Steep.measure "validation" do
194
+ accumulated_diagnostics = []
163
195
 
164
- updated_targets.each do |target|
196
+ project.targets.each do |target|
165
197
  service = signature_services[target.name]
166
198
 
167
- next if no_type_checking?
199
+ next unless target.possible_signature_file?(path) || service.env_rbs_paths.include?(path)
168
200
 
169
201
  case service.status
170
- when SignatureService::SyntaxErrorStatus, SignatureService::AncestorErrorStatus
171
- service.status.diagnostics.group_by {|diag| Pathname(diag.location.buffer.name) }.each do |path, diagnostics|
172
- if assignment =~ path
173
- array = accumulated_diagnostics[path] ||= []
174
- array.push(*diagnostics)
175
- yield [path, array]
176
- end
202
+ when SignatureService::SyntaxErrorStatus
203
+ diagnostics = service.status.diagnostics.select do |diag|
204
+ Pathname(diag.location.buffer.name) == path &&
205
+ (diag.is_a?(Diagnostic::Signature::SyntaxError) || diag.is_a?(Diagnostic::Signature::UnexpectedError))
206
+ end
207
+ accumulated_diagnostics.push(*diagnostics)
208
+ unless diagnostics.empty?
209
+ yield [path, accumulated_diagnostics]
177
210
  end
211
+
212
+ when SignatureService::AncestorErrorStatus
213
+ diagnostics = service.status.diagnostics.select {|diag| Pathname(diag.location.buffer.name) == path }
214
+ accumulated_diagnostics.push(*diagnostics)
215
+ yield [path, accumulated_diagnostics]
216
+
178
217
  when SignatureService::LoadedStatus
179
218
  validator = Signature::Validator.new(checker: service.current_subtyping)
180
- paths = service.each_rbs_path.select {|path| assignment =~ path }.to_set
181
- type_names = service.type_names(paths: paths, env: service.latest_env).to_set
182
-
183
- Steep.measure2 "Validating #{type_names.size} types from #{paths.size} files" do |sampler|
184
- type_names.each do |type_name|
185
- sampler.sample type_name.to_s do
186
- case
187
- when type_name.class?
188
- validator.validate_one_class(type_name)
189
- when type_name.interface?
190
- validator.validate_one_interface(type_name)
191
- when type_name.alias?
192
- validator.validate_one_alias(type_name)
219
+ type_names = service.type_names(paths: Set[path], env: service.latest_env).to_set
220
+
221
+ unless type_names.empty?
222
+ Steep.measure2 "Validating #{type_names.size} types" do |sampler|
223
+ type_names.each do |type_name|
224
+ sampler.sample(type_name.to_s) do
225
+ case
226
+ when type_name.class?
227
+ validator.validate_one_class(type_name)
228
+ when type_name.interface?
229
+ validator.validate_one_interface(type_name)
230
+ when type_name.alias?
231
+ validator.validate_one_alias(type_name)
232
+ end
193
233
  end
194
234
  end
195
235
  end
196
236
  end
197
237
 
198
- Steep.measure "Validating const and globals" do
199
- validator.validate_const()
200
- validator.validate_global()
238
+ const_decls = service.const_decls(paths: Set[path], env: service.latest_env)
239
+ unless const_decls.empty?
240
+ Steep.measure2 "Validating #{const_decls.size} constants" do |sampler|
241
+ const_decls.each do |name, entry|
242
+ sampler.sample(name.to_s) do
243
+ validator.validate_one_constant(name, entry)
244
+ end
245
+ end
246
+ end
201
247
  end
202
248
 
203
- target_diagnostics = validator.each_error.group_by {|error| Pathname(error.location.buffer.name) }
204
- signature_validation_diagnostics[target.name] = target_diagnostics
205
-
206
- paths.each do |path|
207
- array = (accumulated_diagnostics[path] ||= [])
208
- if ds = target_diagnostics[path]
209
- array.push(*ds)
249
+ global_decls = service.global_decls(paths: Set[path])
250
+ unless global_decls.empty?
251
+ Steep.measure2 "Validating #{global_decls.size} globals" do |sampler|
252
+ global_decls.each do |name, entry|
253
+ sampler.sample(name.to_s) do
254
+ validator.validate_one_global(name, entry)
255
+ end
256
+ end
210
257
  end
211
- yield [path, array]
212
258
  end
259
+
260
+ diagnostics = validator.each_error.select {|error| Pathname(error.location.buffer.name) == path }
261
+ accumulated_diagnostics.push(*diagnostics)
262
+ yield [path, accumulated_diagnostics]
213
263
  end
264
+
265
+ signature_validation_diagnostics[target.name][path] = diagnostics
214
266
  end
215
267
  end
216
-
217
- updated_targets
218
268
  end
219
269
  end
220
270
 
221
- def update_target(changes:, target:, updated:, &block)
222
- Steep.logger.tagged "#update_target" do
223
- contents = {}
224
-
225
- if updated
226
- source_files.each do |path, file|
227
- if target.possible_source_file?(path)
228
- contents[path] = file.content
229
- end
271
+ def typecheck_source(path:, target: project.target_for_source_path(path), &block)
272
+ Steep.logger.tagged "#typecheck_source(path=#{path})" do
273
+ Steep.measure "typecheck" do
274
+ signature_service = signature_services[target.name]
275
+ subtyping = signature_service.current_subtyping
276
+
277
+ if subtyping
278
+ text = source_files[path].content
279
+ file = type_check_file(target: target, subtyping: subtyping, path: path, text: text)
280
+ yield [file.path, file.diagnostics]
281
+ source_files[path] = file
230
282
  end
283
+ end
284
+ end
285
+ end
231
286
 
232
- changes.each do |path, changes|
233
- if target.possible_source_file?(path)
234
- text = contents[path] || ""
235
- contents[path] = changes.inject(text) {|text, change| change.apply_to(text) }
236
- end
237
- end
238
- else
239
- changes.each do |path, changes|
240
- if target.possible_source_file?(path)
241
- text = source_files[path]&.content || ""
242
- contents[path] = changes.inject(text) {|text, change| change.apply_to(text) }
243
- end
287
+ def update_signature(changes:, requests:)
288
+ Steep.logger.tagged "#update_signature" do
289
+ project.targets.each do |target|
290
+ signature_service = signature_services[target.name]
291
+ signature_changes = changes.filter {|path, _| target.possible_signature_file?(path) }
292
+
293
+ unless signature_changes.empty?
294
+ requests[target].signature_updated!
295
+ signature_service.update(signature_changes)
244
296
  end
245
297
  end
298
+ end
299
+ end
246
300
 
247
- signature_service = signature_services[target.name]
248
- subtyping = signature_service.current_subtyping
301
+ def update_sources(changes:, requests:)
302
+ requests.each_value do |request|
303
+ source_files
304
+ .select {|path, file| request.target.possible_source_file?(path) }
305
+ .each do |path, file|
306
+ (changes[path] ||= []).prepend(ContentChange.string(file.content))
307
+ end
308
+ end
249
309
 
250
- Steep.measure2 "Type check sources" do |sampler|
251
- contents.each do |path, text|
252
- if assignment =~ path
253
- if subtyping
254
- file = sampler.sample(path.to_s) do
255
- type_check_file(target: target, subtyping: subtyping, path: path, text: text)
256
- end
257
- yield [file.path, file.diagnostics]
258
- else
259
- if source_files.key?(path)
260
- file = source_files[path]&.update_content(text)
261
- else
262
- file = SourceFile.no_data(path: path, content: text)
263
- yield [file.path, []]
264
- end
265
- end
310
+ changes.each do |path, changes|
311
+ target = project.target_for_source_path(path)
266
312
 
267
- source_files[path] = file
268
- end
269
- end
313
+ if target
314
+ file = source_files[path] || SourceFile.no_data(path: path, content: "")
315
+ content = changes.inject(file.content) {|text, change| change.apply_to(text) }
316
+ source_files[path] = file.update_content(content)
317
+ requests[target].source_paths << path
270
318
  end
271
319
  end
272
320
  end
273
321
 
274
322
  def type_check_file(target:, subtyping:, path:, text:)
275
323
  Steep.logger.tagged "#type_check_file(#{path}@#{target.name})" do
276
- if no_type_checking?
277
- SourceFile.no_data(path: path, content: text)
278
- else
279
- source = Source.parse(text, path: path, factory: subtyping.factory)
280
- typing = TypeCheckService.type_check(source: source, subtyping: subtyping)
281
- SourceFile.with_typing(path: path, content: text, node: source.node, typing: typing)
282
- end
324
+ source = Source.parse(text, path: path, factory: subtyping.factory)
325
+ typing = TypeCheckService.type_check(source: source, subtyping: subtyping)
326
+ SourceFile.with_typing(path: path, content: text, node: source.node, typing: typing)
283
327
  end
284
328
  rescue AnnotationParser::SyntaxError => exn
285
- SourceFile.no_data(path: path, content: text)
286
- # SourceFile.with_syntax_error(path: path, content: text, error: exn)
287
- rescue ::Parser::SyntaxError, EncodingError => exn
288
- SourceFile.no_data(path: path, content: text)
289
- # SourceFile.with_syntax_error(path: path, content: text, error: exn)
329
+ error = Diagnostic::Ruby::SyntaxError.new(message: exn.message, location: exn.location)
330
+ SourceFile.with_syntax_error(path: path, content: text, error: error)
331
+ rescue ::Parser::SyntaxError => exn
332
+ error = Diagnostic::Ruby::SyntaxError.new(message: exn.message, location: exn.diagnostic.location)
333
+ SourceFile.with_syntax_error(path: path, content: text, error: error)
334
+ rescue EncodingError => exn
335
+ SourceFile.no_data(path: path, content: "")
290
336
  end
291
337
 
292
338
  def self.type_check(source:, subtyping:)