steep 0.42.0 → 0.43.0

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