steep 0.15.0 → 0.16.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.
@@ -15,110 +15,122 @@ module Steep
15
15
  @queue = Thread::Queue.new
16
16
  end
17
17
 
18
- def listener
19
- @listener ||= begin
20
- Steep.logger.info "Watching #{dirs.join(", ")}..."
21
- Listen.to(*dirs.map(&:to_s)) do |modified, added, removed|
22
- Steep.logger.tagged "watch" do
23
- Steep.logger.info "Received file system updates: modified=[#{modified.join(",")}], added=[#{added.join(",")}], removed=[#{removed.join(",")}]"
24
- end
25
- queue << [modified, added, removed]
26
- end
18
+ def run()
19
+ if dirs.empty?
20
+ stdout.puts "Specify directories to watch"
21
+ return 1
27
22
  end
28
- end
29
23
 
30
- def type_check_loop(project)
31
- until queue.closed?
32
- stdout.puts "🚥 Waiting for updates..."
24
+ project = load_config()
33
25
 
34
- events = []
35
- events << queue.deq
36
- until queue.empty?
37
- events << queue.deq(nonblock: true)
38
- end
26
+ loader = Project::FileLoader.new(project: project)
27
+ loader.load_sources([])
28
+ loader.load_signatures()
39
29
 
40
- events.compact.each do |modified, added, removed|
41
- modified.each do |name|
42
- path = Pathname(name).relative_path_from(Pathname.pwd)
30
+ client_read, server_write = IO.pipe
31
+ server_read, client_write = IO.pipe
43
32
 
44
- project.targets.each do |target|
45
- target.update_source path, path.read if target.source_file?(path)
46
- target.update_signature path, path.read if target.signature_file?(path)
47
- end
48
- end
33
+ client_reader = LanguageServer::Protocol::Transport::Io::Reader.new(client_read)
34
+ client_writer = LanguageServer::Protocol::Transport::Io::Writer.new(client_write)
49
35
 
50
- added.each do |name|
51
- path = Pathname(name).relative_path_from(Pathname.pwd)
36
+ server_reader = LanguageServer::Protocol::Transport::Io::Reader.new(server_read)
37
+ server_writer = LanguageServer::Protocol::Transport::Io::Writer.new(server_write)
52
38
 
53
- project.targets.each do |target|
54
- target.add_source path, path.read if target.possible_source_file?(path)
55
- target.add_signature path, path.read if target.possible_signature_file?(path)
56
- end
57
- end
39
+ interaction_worker = Server::InteractionWorker.spawn_worker(:interaction, name: "interaction", steepfile: project.steepfile_path)
40
+ signature_worker = Server::WorkerProcess.spawn_worker(:signature, name: "signature", steepfile: project.steepfile_path)
41
+ code_workers = Server::WorkerProcess.spawn_code_workers(steepfile: project.steepfile_path)
58
42
 
59
- removed.each do |name|
60
- path = Pathname(name).relative_path_from(Pathname.pwd)
43
+ master = Server::Master.new(
44
+ project: project,
45
+ reader: server_reader,
46
+ writer: server_writer,
47
+ interaction_worker: interaction_worker,
48
+ signature_worker: signature_worker,
49
+ code_workers: code_workers
50
+ )
61
51
 
62
- project.targets.each do |target|
63
- target.remove_source path if target.source_file?(path)
64
- target.remove_signature path if target.signature_file?(path)
65
- end
52
+ main_thread = Thread.start do
53
+ master.start()
54
+ end
55
+ main_thread.abort_on_exception = true
56
+
57
+ client_writer.write(method: "initialize", id: 0)
58
+
59
+ Steep.logger.info "Watching #{dirs.join(", ")}..."
60
+ listener = Listen.to(*dirs.map(&:to_s)) do |modified, added, removed|
61
+ stdout.puts "🔬 Type checking updated files..."
62
+
63
+ version = Time.now.to_i
64
+ Steep.logger.tagged "watch" do
65
+ Steep.logger.info "Received file system updates: modified=[#{modified.join(",")}], added=[#{added.join(",")}], removed=[#{removed.join(",")}]"
66
+
67
+ (modified + added).each do |path|
68
+ client_writer.write(
69
+ method: "textDocument/didChange",
70
+ params: {
71
+ textDocument: {
72
+ uri: "file://#{path}",
73
+ version: version
74
+ },
75
+ contentChanges: [
76
+ {
77
+ text: Pathname(path).read
78
+ }
79
+ ]
80
+ }
81
+ )
66
82
  end
67
- end
68
83
 
69
- stdout.puts "🔬 Type checking..."
70
- type_check project
71
- print_project_result project
72
- end
73
- rescue ClosedQueueError
74
- # nop
75
- end
84
+ removed.each do |path|
85
+ client_writer.write(
86
+ method: "textDocument/didChange",
87
+ params: {
88
+ textDocument: {
89
+ uri: "file://#{path}",
90
+ version: version
91
+ },
92
+ contentChanges: [
93
+ {
94
+ text: ""
95
+ }
96
+ ]
97
+ }
98
+ )
99
+ end
100
+ end
101
+ end.tap(&:start)
76
102
 
77
- def print_project_result(project)
78
- project.targets.each do |target|
79
- Steep.logger.tagged "target=#{target.name}" do
80
- case (status = target.status)
81
- when Project::Target::SignatureSyntaxErrorStatus
82
- printer = SignatureErrorPrinter.new(stdout: stdout, stderr: stderr)
83
- printer.print_syntax_errors(status.errors)
84
- when Project::Target::SignatureValidationErrorStatus
85
- printer = SignatureErrorPrinter.new(stdout: stdout, stderr: stderr)
86
- printer.print_semantic_errors(status.errors)
87
- when Project::Target::TypeCheckStatus
88
- status.type_check_sources.each do |source_file|
89
- source_file.errors.each do |error|
90
- error.print_to stdout
103
+ begin
104
+ stdout.puts "👀 Watching directories, Ctrl-C to stop."
105
+ client_reader.read do |response|
106
+ case response[:method]
107
+ when "textDocument/publishDiagnostics"
108
+ uri = URI.parse(response[:params][:uri])
109
+ path = project.relative_path(Pathname(uri.path))
110
+
111
+ diagnostics = response[:params][:diagnostics]
112
+
113
+ unless diagnostics.empty?
114
+ diagnostics.each do |diagnostic|
115
+ start = diagnostic[:range][:start]
116
+ loc = "#{start[:line]+1}:#{start[:character]}"
117
+ message = diagnostic[:message].chomp.lines.join(" ")
118
+
119
+ stdout.puts "#{path}:#{loc}: #{message}"
91
120
  end
92
121
  end
93
122
  end
94
123
  end
95
- end
96
- end
97
-
98
- def run()
99
- if dirs.empty?
100
- stdout.puts "Specify directories to watch"
101
- return 1
102
- end
103
-
104
- project = load_config()
105
-
106
- loader = Project::FileLoader.new(project: project)
107
- loader.load_sources([])
108
- loader.load_signatures()
109
-
110
- type_check project
111
- print_project_result project
112
-
113
- listener.start
114
-
115
- stdout.puts "👀 Watching directories, Ctrl-C to stop."
116
- begin
117
- type_check_loop project
118
124
  rescue Interrupt
119
- # bye
125
+ stdout.puts "Shutting down workers..."
126
+ client_writer.write({ method: :shutdown, id: 10000 })
127
+ client_writer.write({ method: :exit })
128
+ client_writer.io.close()
120
129
  end
121
130
 
131
+ listener.stop
132
+ main_thread.join
133
+
122
134
  0
123
135
  end
124
136
  end
@@ -0,0 +1,51 @@
1
+ module Steep
2
+ module Drivers
3
+ class Worker
4
+ attr_reader :stdout, :stderr, :stdin
5
+
6
+ attr_accessor :steepfile_path
7
+ attr_accessor :worker_type
8
+ attr_accessor :worker_name
9
+
10
+ include Utils::DriverHelper
11
+
12
+ def initialize(stdout:, stderr:, stdin:)
13
+ @stdout = stdout
14
+ @stderr = stderr
15
+ @stdin = stdin
16
+ end
17
+
18
+ def run()
19
+ Steep.logger.tagged("#{worker_type}:#{worker_name}") do
20
+ project = load_config()
21
+
22
+ loader = Project::FileLoader.new(project: project)
23
+ loader.load_sources([])
24
+ loader.load_signatures()
25
+
26
+ reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdin)
27
+ writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdout)
28
+
29
+ worker = case worker_type
30
+ when :code
31
+ Server::CodeWorker.new(project: project, reader: reader, writer: writer)
32
+ when :signature
33
+ Server::SignatureWorker.new(project: project, reader: reader, writer: writer)
34
+ when :interaction
35
+ Server::InteractionWorker.new(project: project, reader: reader, writer: writer)
36
+ else
37
+ raise "Unknown worker type: #{worker_type}"
38
+ end
39
+
40
+ Steep.logger.info "Starting #{worker_type} worker..."
41
+
42
+ worker.run()
43
+ rescue Interrupt
44
+ Steep.logger.info "Shutting down by interrupt..."
45
+ end
46
+
47
+ 0
48
+ end
49
+ end
50
+ end
51
+ end
@@ -47,8 +47,10 @@ module Steep
47
47
  position = Position.new(line: line, column: column)
48
48
 
49
49
  begin
50
- Steep.measure "type_check!" do
51
- type_check!(source_text)
50
+ Steep.logger.tagged "completion_provider#run(line: #{line}, column: #{column})" do
51
+ Steep.measure "type_check!" do
52
+ type_check!(source_text)
53
+ end
52
54
  end
53
55
 
54
56
  Steep.measure "completion item collection" do
@@ -96,6 +96,7 @@ module Steep
96
96
  timestamp: Time.now
97
97
  )
98
98
  rescue => exn
99
+ Steep.log_error(exn)
99
100
  @status = TypeCheckErrorStatus.new(error: exn)
100
101
  end
101
102
 
@@ -33,9 +33,12 @@ module Steep
33
33
  end
34
34
 
35
35
  def content_for(path:, line:, column:)
36
- source_file = project.targets.map {|target| target.source_files[path] }.compact[0]
36
+ target = project.targets.find {|target| target.source_file?(path) }
37
+
38
+ if target
39
+ source_file = target.source_files[path]
40
+ target.type_check(target_sources: [source_file], validate_signatures: false)
37
41
 
38
- if source_file
39
42
  case (status = source_file.status)
40
43
  when SourceFile::TypeCheckStatus
41
44
  node, *parents = status.source.find_nodes(line: line, column: column)
@@ -67,24 +67,30 @@ module Steep
67
67
  end
68
68
 
69
69
  def possible_source_file?(path)
70
- self.class.test_pattern(source_patterns, path) &&
71
- !self.class.test_pattern(ignore_patterns, path)
70
+ self.class.test_pattern(source_patterns, path, ext: ".rb") &&
71
+ !self.class.test_pattern(ignore_patterns, path, ext: ".rb")
72
72
  end
73
73
 
74
74
  def possible_signature_file?(path)
75
- self.class.test_pattern(signature_patterns, path)
75
+ self.class.test_pattern(signature_patterns, path, ext: ".rbs")
76
76
  end
77
77
 
78
- def self.test_pattern(patterns, path)
78
+ def self.test_pattern(patterns, path, ext:)
79
79
  patterns.any? do |pattern|
80
80
  p = pattern.end_with?(File::Separator) ? pattern : pattern + File::Separator
81
- path.to_s.start_with?(p) || File.fnmatch(pattern, path.to_s)
81
+ (path.to_s.start_with?(p) && path.extname == ext) || File.fnmatch(pattern, path.to_s)
82
82
  end
83
83
  end
84
84
 
85
- def type_check
86
- load_signatures do |env, check, timestamp|
87
- run_type_check(env, check, timestamp)
85
+ def type_check(target_sources: source_files.values, validate_signatures: true)
86
+ Steep.logger.tagged "target#type_check(target_sources: [#{target_sources.map(&:path).join(", ")}], validate_signatures: #{validate_signatures})" do
87
+ Steep.measure "load signature and type check" do
88
+ load_signatures(validate: validate_signatures) do |env, check, timestamp|
89
+ Steep.measure "type checking #{target_sources.size} files" do
90
+ run_type_check(env, check, timestamp, target_sources: target_sources)
91
+ end
92
+ end
93
+ end
88
94
  end
89
95
  end
90
96
 
@@ -100,7 +106,7 @@ module Steep
100
106
  end
101
107
  end
102
108
 
103
- def load_signatures
109
+ def load_signatures(validate:)
104
110
  timestamp = case status
105
111
  when TypeCheckStatus
106
112
  status.timestamp
@@ -133,16 +139,20 @@ module Steep
133
139
  factory = AST::Types::Factory.new(builder: definition_builder)
134
140
  check = Subtyping::Check.new(factory: factory)
135
141
 
136
- validator = Signature::Validator.new(checker: check)
137
- validator.validate()
138
-
139
- if validator.no_error?
140
- yield env, check, Time.now
142
+ if validate
143
+ validator = Signature::Validator.new(checker: check)
144
+ validator.validate()
145
+
146
+ if validator.no_error?
147
+ yield env, check, Time.now
148
+ else
149
+ @status = SignatureValidationErrorStatus.new(
150
+ errors: validator.each_error.to_a,
151
+ timestamp: Time.now
152
+ )
153
+ end
141
154
  else
142
- @status = SignatureValidationErrorStatus.new(
143
- errors: validator.each_error.to_a,
144
- timestamp: Time.now
145
- )
155
+ yield env, check, Time.now
146
156
  end
147
157
  end
148
158
 
@@ -160,10 +170,10 @@ module Steep
160
170
  end
161
171
  end
162
172
 
163
- def run_type_check(env, check, timestamp)
173
+ def run_type_check(env, check, timestamp, target_sources: source_files.values)
164
174
  type_check_sources = []
165
175
 
166
- source_files.each_value do |file|
176
+ target_sources.each do |file|
167
177
  if file.type_check(check, timestamp)
168
178
  type_check_sources << file
169
179
  end
data/lib/steep/project.rb CHANGED
@@ -1,17 +1,21 @@
1
1
  module Steep
2
2
  class Project
3
3
  attr_reader :targets
4
- attr_reader :base_dir
4
+ attr_reader :steepfile_path
5
5
 
6
- def initialize(base_dir:)
6
+ def initialize(steepfile_path:)
7
7
  @targets = []
8
- @base_dir = base_dir
8
+ @steepfile_path = steepfile_path
9
9
 
10
- unless base_dir.absolute?
11
- raise "Project#initialize(base_dir:): base_dir should be absolute path"
10
+ unless steepfile_path.absolute?
11
+ raise "Project#initialize(steepfile_path:): steepfile_path should be absolute path"
12
12
  end
13
13
  end
14
14
 
15
+ def base_dir
16
+ steepfile_path.parent
17
+ end
18
+
15
19
  def relative_path(path)
16
20
  path.relative_path_from(base_dir)
17
21
  end
@@ -0,0 +1,56 @@
1
+ module Steep
2
+ module Server
3
+ class BaseWorker
4
+ LSP = LanguageServer::Protocol
5
+
6
+ include Utils
7
+
8
+ attr_reader :project
9
+ attr_reader :reader, :writer
10
+
11
+ def initialize(project:, reader:, writer:)
12
+ @project = project
13
+ @reader = reader
14
+ @writer = writer
15
+ end
16
+
17
+ def handle_request(request)
18
+ # process request
19
+ end
20
+
21
+ def handle_job(job)
22
+ # process async job
23
+ end
24
+
25
+ def run
26
+ tags = Steep.logger.formatter.current_tags.dup
27
+ thread = Thread.new do
28
+ Steep.logger.formatter.push_tags(*tags)
29
+ Steep.logger.tagged "background" do
30
+ while job = queue.pop
31
+ handle_job(job)
32
+ end
33
+ end
34
+ end
35
+
36
+ Steep.logger.tagged "frontend" do
37
+ begin
38
+ reader.read do |request|
39
+ case request[:method]
40
+ when "shutdown"
41
+ # nop
42
+ when "exit"
43
+ break
44
+ else
45
+ handle_request(request)
46
+ end
47
+ end
48
+ ensure
49
+ queue << nil
50
+ thread.join
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,151 @@
1
+ module Steep
2
+ module Server
3
+ class CodeWorker < BaseWorker
4
+ LSP = LanguageServer::Protocol
5
+
6
+ include Utils
7
+
8
+ attr_reader :target_files
9
+ attr_reader :queue
10
+
11
+ def initialize(project:, reader:, writer:, queue: Queue.new)
12
+ super(project: project, reader: reader, writer: writer)
13
+
14
+ @target_files = {}
15
+ @queue = queue
16
+ end
17
+
18
+ def enqueue_type_check(target:, path:, version: target_files[path])
19
+ Steep.logger.info "Enqueueing type check: #{path}(#{version})@#{target.name}..."
20
+ target_files[path] = version
21
+ queue << [path, version, target]
22
+ end
23
+
24
+ def each_type_check_subject(path:, version:)
25
+ case
26
+ when !(updated_targets = project.targets.select {|target| target.signature_file?(path) }).empty?
27
+ updated_targets.each do |target|
28
+ target_files.each_key do |path|
29
+ if target.source_file?(path)
30
+ yield target, path, target_files[path]
31
+ end
32
+ end
33
+ end
34
+
35
+ when target = project.targets.find {|target| target.source_file?(path) }
36
+ if target_files.key?(path)
37
+ yield target, path, version
38
+ end
39
+ end
40
+ end
41
+
42
+ def typecheck_file(path, target)
43
+ Steep.logger.info "Starting type checking: #{path}@#{target.name}..."
44
+
45
+ source = target.source_files[path]
46
+ target.type_check(target_sources: [source], validate_signatures: false)
47
+
48
+ Steep.logger.info "Finished type checking: #{path}@#{target.name}"
49
+
50
+ diagnostics = source_diagnostics(source)
51
+
52
+ writer.write(
53
+ method: :"textDocument/publishDiagnostics",
54
+ params: LSP::Interface::PublishDiagnosticsParams.new(
55
+ uri: URI.parse(project.absolute_path(path).to_s).tap {|uri| uri.scheme = "file"},
56
+ diagnostics: diagnostics
57
+ )
58
+ )
59
+ end
60
+
61
+ def source_diagnostics(source)
62
+ case status = source.status
63
+ when Project::SourceFile::ParseErrorStatus
64
+ []
65
+ when Project::SourceFile::AnnotationSyntaxErrorStatus
66
+ [
67
+ LSP::Interface::Diagnostic.new(
68
+ message: "Annotation syntax error: #{status.error.cause.message}",
69
+ severity: LSP::Constant::DiagnosticSeverity::ERROR,
70
+ range: LSP::Interface::Range.new(
71
+ start: LSP::Interface::Position.new(
72
+ line: status.location.start_line - 1,
73
+ character: status.location.start_column
74
+ ),
75
+ end: LSP::Interface::Position.new(
76
+ line: status.location.end_line - 1,
77
+ character: status.location.end_column
78
+ )
79
+ )
80
+ )
81
+ ]
82
+ when Project::SourceFile::TypeCheckStatus
83
+ status.typing.errors.map do |error|
84
+ loc = error.location_to_str
85
+
86
+ LSP::Interface::Diagnostic.new(
87
+ message: StringIO.new.tap {|io| error.print_to(io) }.string.gsub(/\A#{loc}: /, "").chomp,
88
+ severity: LSP::Constant::DiagnosticSeverity::ERROR,
89
+ range: LSP::Interface::Range.new(
90
+ start: LSP::Interface::Position.new(
91
+ line: error.node.loc.line - 1,
92
+ character: error.node.loc.column
93
+ ),
94
+ end: LSP::Interface::Position.new(
95
+ line: error.node.loc.last_line - 1,
96
+ character: error.node.loc.last_column
97
+ )
98
+ )
99
+ )
100
+ end
101
+ when Project::SourceFile::TypeCheckErrorStatus
102
+ []
103
+ end
104
+ end
105
+
106
+ def handle_request(request)
107
+ case request[:method]
108
+ when "initialize"
109
+ # Don't respond to initialize request, but start type checking.
110
+ project.targets.each do |target|
111
+ target.source_files.each_key do |path|
112
+ if target_files.key?(path)
113
+ enqueue_type_check(target: target, path: path, version: target_files[path])
114
+ end
115
+ end
116
+ end
117
+
118
+ when "workspace/executeCommand"
119
+ if request[:params][:command] == "steep/registerSourceToWorker"
120
+ paths = request[:params][:arguments].map {|arg| source_path(URI.parse(arg)) }
121
+ paths.each do |path|
122
+ target_files[path] = 0
123
+ end
124
+ end
125
+
126
+ when "textDocument/didChange"
127
+ update_source(request) do |path, version|
128
+ if target_files.key?(path)
129
+ target_files[path] = version
130
+ end
131
+ end
132
+
133
+ path = source_path(URI.parse(request[:params][:textDocument][:uri]))
134
+ version = request[:params][:textDocument][:version]
135
+ each_type_check_subject(path: path, version: version) do |target, path, version|
136
+ enqueue_type_check(target: target, path: path, version: version)
137
+ end
138
+ end
139
+ end
140
+
141
+ def handle_job(job)
142
+ path, version, target = job
143
+ if !version || target_files[path] == version
144
+ typecheck_file(path, target)
145
+ else
146
+ Steep.logger.info "Skipping type check: #{path}@#{target.name}, queued version=#{version}, latest version=#{target_files[path]}"
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end