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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/steep.rb +4 -3
- data/lib/steep/annotation_parser.rb +10 -2
- data/lib/steep/cli.rb +1 -0
- data/lib/steep/diagnostic/ruby.rb +15 -6
- data/lib/steep/diagnostic/signature.rb +28 -11
- data/lib/steep/drivers/annotations.rb +1 -3
- data/lib/steep/drivers/check.rb +17 -7
- data/lib/steep/drivers/diagnostic_printer.rb +4 -0
- data/lib/steep/drivers/langserver.rb +1 -0
- data/lib/steep/drivers/print_project.rb +1 -1
- data/lib/steep/drivers/stats.rb +125 -105
- data/lib/steep/drivers/utils/driver_helper.rb +35 -0
- data/lib/steep/drivers/validate.rb +1 -1
- data/lib/steep/drivers/watch.rb +12 -10
- data/lib/steep/index/signature_symbol_provider.rb +20 -6
- data/lib/steep/project/target.rb +4 -4
- data/lib/steep/server/interaction_worker.rb +2 -3
- data/lib/steep/server/master.rb +621 -170
- data/lib/steep/server/type_check_worker.rb +127 -13
- data/lib/steep/server/worker_process.rb +7 -4
- data/lib/steep/services/completion_provider.rb +2 -2
- data/lib/steep/services/hover_content.rb +5 -4
- data/lib/steep/services/path_assignment.rb +6 -8
- data/lib/steep/services/signature_service.rb +43 -9
- data/lib/steep/services/type_check_service.rb +184 -138
- data/lib/steep/signature/validator.rb +17 -9
- data/lib/steep/source.rb +21 -18
- data/lib/steep/subtyping/constraints.rb +2 -2
- data/lib/steep/type_construction.rb +223 -125
- data/lib/steep/type_inference/block_params.rb +1 -1
- data/lib/steep/type_inference/context.rb +22 -0
- data/lib/steep/type_inference/logic.rb +1 -1
- data/lib/steep/type_inference/logic_type_interpreter.rb +3 -3
- data/lib/steep/version.rb +1 -1
- data/smoke/implements/b.rb +13 -0
- data/smoke/implements/b.rbs +12 -0
- data/smoke/regression/issue_328.rb +1 -0
- data/smoke/regression/issue_328.rbs +0 -0
- data/smoke/regression/issue_332.rb +11 -0
- data/smoke/regression/issue_332.rbs +19 -0
- data/smoke/regression/masgn.rb +4 -0
- data/smoke/regression/test_expectations.yml +29 -0
- data/smoke/regression/thread.rb +7 -0
- data/smoke/super/test_expectations.yml +2 -12
- data/steep.gemspec +2 -2
- 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
|
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
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
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.
|
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].
|
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
|
66
|
+
var_type = context.lvar_env[var_name] || AST::Types::Any.new(location: nil)
|
66
67
|
|
67
|
-
VariableContent.new(node: node, name: var_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
|
72
|
+
type = context.lvar_env[var_name] || typing.type_of(node: rhs)
|
72
73
|
|
73
|
-
VariableContent.new(node: node, name: var_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
|
17
|
+
(cache[path] ||= self.class.index_for(path: path.to_s, max_index: max_index)) == index
|
18
|
+
end
|
19
|
+
|
20
|
+
alias === =~
|
18
21
|
|
19
|
-
|
20
|
-
|
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
|
-
|
77
|
-
unless files.key?(
|
78
|
-
yield
|
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?(
|
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
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
52
|
+
class TargetRequest
|
53
|
+
attr_reader :target
|
54
|
+
attr_reader :source_paths
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
73
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
131
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
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
|
144
|
-
|
145
|
-
updated_targets = []
|
166
|
+
def update_and_check(changes:, assignment:, &block)
|
167
|
+
requests = update(changes: changes)
|
146
168
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
162
|
-
|
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
|
-
|
196
|
+
project.targets.each do |target|
|
165
197
|
service = signature_services[target.name]
|
166
198
|
|
167
|
-
next
|
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
|
171
|
-
service.status.diagnostics.
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
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
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
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
|
222
|
-
Steep.logger.tagged "#
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
248
|
-
|
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
|
-
|
251
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
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
|
-
|
286
|
-
|
287
|
-
rescue ::Parser::SyntaxError
|
288
|
-
|
289
|
-
|
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:)
|