steep 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,188 @@
1
+ module Steep
2
+ module Drivers
3
+ class Watch
4
+ class Options
5
+ attr_accessor :fallback_any_is_error
6
+ attr_accessor :allow_missing_definitions
7
+
8
+ def initialize
9
+ self.fallback_any_is_error = false
10
+ self.allow_missing_definitions = true
11
+ end
12
+ end
13
+
14
+ attr_reader :source_dirs
15
+ attr_reader :signature_dirs
16
+ attr_reader :stdout
17
+ attr_reader :stderr
18
+ attr_reader :options
19
+ attr_reader :queue
20
+
21
+ include Utils::EachSignature
22
+
23
+ def initialize(source_dirs:, signature_dirs:, stdout:, stderr:)
24
+ @source_dirs = source_dirs
25
+ @signature_dirs = signature_dirs
26
+ @stdout = stdout
27
+ @stderr = stderr
28
+ @options = Options.new
29
+ @queue = Thread::Queue.new
30
+ end
31
+
32
+ def project_options
33
+ Project::Options.new.tap do |opt|
34
+ opt.fallback_any_is_error = options.fallback_any_is_error
35
+ opt.allow_missing_definitions = options.allow_missing_definitions
36
+ end
37
+ end
38
+
39
+ def source_listener
40
+ @source_listener ||= yield_self do
41
+ Listen.to(*source_dirs.map(&:to_s), only: /\.rb$/) do |modified, added, removed|
42
+ queue << [:source, modified, added, removed]
43
+ end
44
+ end
45
+ end
46
+
47
+ def signature_listener
48
+ @signature_listener ||= yield_self do
49
+ Listen.to(*signature_dirs.map(&:to_s), only: /\.rbi$/) do |modified, added, removed|
50
+ queue << [:signature, modified, added, removed]
51
+ end
52
+ end
53
+ end
54
+
55
+ def type_check_thread(project)
56
+ Thread.new do
57
+ until queue.closed?
58
+ begin
59
+ events = []
60
+ events << queue.deq
61
+ until queue.empty?
62
+ events << queue.deq(nonblock: true)
63
+ end
64
+
65
+ events.compact.each do |name, modified, added, removed|
66
+ case name
67
+ when :source
68
+ (modified + added).each do |name|
69
+ path = Pathname(name).relative_path_from(Pathname.pwd)
70
+ file = project.source_files[path] || Project::SourceFile.new(path: path, options: project_options)
71
+ file.content = path.read
72
+ project.source_files[path] = file
73
+ end
74
+
75
+ removed.each do |name|
76
+ path = Pathname(name).relative_path_from(Pathname.pwd)
77
+ project.source_files.delete(path)
78
+ end
79
+
80
+ when :signature
81
+ (modified + added).each do |name|
82
+ path = Pathname(name).relative_path_from(Pathname.pwd)
83
+ file = project.signature_files[path] || Project::SignatureFile.new(path: path)
84
+ file.content = path.read
85
+ project.signature_files[path] = file
86
+ end
87
+
88
+ removed.each do |name|
89
+ path = Pathname(name).relative_path_from(Pathname.pwd)
90
+ project.signature_files.delete(path)
91
+ end
92
+ end
93
+ end
94
+
95
+ begin
96
+ project.type_check
97
+ rescue Racc::ParseError => exn
98
+ stderr.puts exn.message
99
+ project.clear
100
+ end
101
+ end
102
+ end
103
+ rescue ClosedQueueError
104
+ # nop
105
+ end
106
+ end
107
+
108
+ class WatchListener < Project::NullListener
109
+ attr_reader :stdout
110
+ attr_reader :stderr
111
+
112
+ def initialize(stdout:, stderr:, verbose:)
113
+ @stdout = stdout
114
+ @stderr = stderr
115
+ end
116
+
117
+ def check(project:)
118
+ yield.tap do
119
+ if project.success?
120
+ if project.has_type_error?
121
+ stdout.puts "Detected #{project.errors.size} errors... 🔥"
122
+ else
123
+ stdout.puts "No error detected. 🎉"
124
+ end
125
+ else
126
+ stdout.puts "Type checking failed... 🔥"
127
+ end
128
+ end
129
+ end
130
+
131
+ def type_check_source(project:, file:)
132
+ yield.tap do
133
+ case
134
+ when file.source.is_a?(Source) && file.errors
135
+ file.errors.each do |error|
136
+ error.print_to stdout
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def load_signature(project:)
143
+ # @type var project: Project
144
+ yield.tap do
145
+ case sig = project.signature
146
+ when Project::SignatureHasError
147
+ when Project::SignatureHasSyntaxError
148
+ sig.errors.each do |path, exn|
149
+ stdout.puts "#{path} has a syntax error: #{exn.inspect}"
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ def run(block: true)
157
+ project = Project.new(WatchListener.new(stdout: stdout, stderr: stderr, verbose: false))
158
+
159
+ source_dirs.each do |path|
160
+ each_file_in_path(".rb", path) do |file_path|
161
+ file = Project::SourceFile.new(path: file_path, options: options)
162
+ file.content = file_path.read
163
+ project.source_files[file_path] = file
164
+ end
165
+ end
166
+
167
+ signature_dirs.each do |path|
168
+ each_file_in_path(".rbi", path) do |file_path|
169
+ file = Project::SignatureFile.new(path: file_path)
170
+ file.content = file_path.read
171
+ project.signature_files[file_path] = file
172
+ end
173
+ end
174
+
175
+ project.type_check
176
+
177
+ source_listener.start
178
+ signature_listener.start
179
+ t = type_check_thread(project)
180
+
181
+ binding.pry(quiet: true) if block
182
+
183
+ queue.close
184
+ t.join
185
+ end
186
+ end
187
+ end
188
+ end
data/lib/steep/errors.rb CHANGED
@@ -518,7 +518,7 @@ module Steep
518
518
  end
519
519
 
520
520
  def to_s
521
- "#{location_to_str}: UnexpectedKeyword: #{unexpected_keywords.join(", ")}"
521
+ "#{location_to_str}: UnexpectedKeyword: #{unexpected_keywords.to_a.join(", ")}"
522
522
  end
523
523
  end
524
524
 
@@ -531,7 +531,7 @@ module Steep
531
531
  end
532
532
 
533
533
  def to_s
534
- "#{location_to_str}: MissingKeyword: #{missing_keywords.join(", ")}"
534
+ "#{location_to_str}: MissingKeyword: #{missing_keywords.to_a.join(", ")}"
535
535
  end
536
536
  end
537
537
  end
@@ -0,0 +1,119 @@
1
+ module Steep
2
+ class Project
3
+ class SourceFile
4
+ attr_reader :options
5
+ attr_reader :path
6
+ attr_reader :content
7
+ attr_reader :content_updated_at
8
+
9
+ attr_reader :source
10
+ attr_reader :typing
11
+ attr_reader :last_type_checked_at
12
+
13
+ def initialize(path:, options:)
14
+ @path = path
15
+ @options = options
16
+ self.content = ""
17
+ end
18
+
19
+ def content=(content)
20
+ @content_updated_at = Time.now
21
+ @content = content
22
+ end
23
+
24
+ def requires_type_check?
25
+ if last = last_type_checked_at
26
+ last < content_updated_at
27
+ else
28
+ true
29
+ end
30
+ end
31
+
32
+ def invalidate
33
+ @source = nil
34
+ @typing = nil
35
+ @last_type_checked_at = nil
36
+ end
37
+
38
+ def parse
39
+ _ = @source =
40
+ begin
41
+ Source.parse(content, path: path.to_s, labeling: ASTUtils::Labeling.new)
42
+ rescue ::Parser::SyntaxError => exn
43
+ Steep.logger.warn { "Syntax error on #{path}: #{exn.inspect}" }
44
+ exn
45
+ end
46
+ end
47
+
48
+ def errors
49
+ typing&.errors&.reject do |error|
50
+ case
51
+ when error.is_a?(Errors::FallbackAny)
52
+ !options.fallback_any_is_error
53
+ when error.is_a?(Errors::MethodDefinitionMissing)
54
+ options.allow_missing_definitions
55
+ end
56
+ end
57
+ end
58
+
59
+ def type_check(check)
60
+ case source = self.source
61
+ when Source
62
+ @typing = Typing.new
63
+
64
+ annotations = source.annotations(block: source.node, builder: check.builder, current_module: AST::Namespace.root)
65
+
66
+ const_env = TypeInference::ConstantEnv.new(builder: check.builder, context: nil)
67
+ type_env = TypeInference::TypeEnv.build(annotations: annotations,
68
+ subtyping: check,
69
+ const_env: const_env,
70
+ signatures: check.builder.signatures)
71
+
72
+ construction = TypeConstruction.new(
73
+ checker: check,
74
+ annotations: annotations,
75
+ source: source,
76
+ self_type: AST::Builtin::Object.instance_type,
77
+ block_context: nil,
78
+ module_context: TypeConstruction::ModuleContext.new(
79
+ instance_type: nil,
80
+ module_type: nil,
81
+ implement_name: nil,
82
+ current_namespace: AST::Namespace.root,
83
+ const_env: const_env,
84
+ class_name: nil
85
+ ),
86
+ method_context: nil,
87
+ typing: typing,
88
+ break_context: nil,
89
+ type_env: type_env
90
+ )
91
+
92
+ construction.synthesize(source.node)
93
+
94
+ @last_type_checked_at = Time.now
95
+ end
96
+ end
97
+ end
98
+
99
+ class SignatureFile
100
+ attr_reader :path
101
+ attr_reader :content
102
+ attr_reader :content_updated_at
103
+
104
+ def initialize(path:)
105
+ @path = path
106
+ self.content = ""
107
+ end
108
+
109
+ def parse
110
+ Parser.parse_signature(content, name: path)
111
+ end
112
+
113
+ def content=(content)
114
+ @content_updated_at = Time.now
115
+ @content = content
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,53 @@
1
+ module Steep
2
+ class Project
3
+ class NullListener
4
+ def parse_signature(project:, file:)
5
+ yield
6
+ end
7
+
8
+ def parse_source(project:, file:)
9
+ yield
10
+ end
11
+
12
+ def check(project:)
13
+ yield
14
+ end
15
+
16
+ def validate_signature(project:)
17
+ yield
18
+ end
19
+
20
+ def type_check_source(project:, file:)
21
+ yield
22
+ end
23
+
24
+ def clear_project(project:)
25
+ yield
26
+ end
27
+
28
+ def load_signature(project:)
29
+ yield
30
+ end
31
+ end
32
+
33
+ class SyntaxErrorRaisingListener < NullListener
34
+ def load_signature(project:)
35
+ yield.tap do
36
+ case signature = project.signature
37
+ when SignatureHasSyntaxError
38
+ raise signature.errors.values[0]
39
+ end
40
+ end
41
+ end
42
+
43
+ def parse_source(project:, file:)
44
+ yield.tap do
45
+ case source = file.source
46
+ when ::Parser::SyntaxError
47
+ raise source
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ module Steep
2
+ class Project
3
+ class Options
4
+ attr_accessor :fallback_any_is_error
5
+ attr_accessor :allow_missing_definitions
6
+
7
+ def initialize
8
+ self.fallback_any_is_error = false
9
+ self.allow_missing_definitions = true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,231 @@
1
+ module Steep
2
+ class Project
3
+ class SignatureLoaded
4
+ attr_reader :check
5
+ attr_reader :loaded_at
6
+ attr_reader :file_paths
7
+
8
+ def initialize(check:, loaded_at:, file_paths:)
9
+ @check = check
10
+ @loaded_at = loaded_at
11
+ @file_paths = file_paths
12
+ end
13
+ end
14
+
15
+ class SignatureHasSyntaxError
16
+ attr_reader :errors
17
+
18
+ def initialize(errors:)
19
+ @errors = errors
20
+ end
21
+ end
22
+
23
+ class SignatureHasError
24
+ attr_reader :errors
25
+
26
+ def initialize(errors:)
27
+ @errors = errors
28
+ end
29
+ end
30
+
31
+ attr_reader :source_files
32
+ attr_reader :signature_files
33
+ attr_reader :listener
34
+
35
+ attr_reader :signature
36
+
37
+ def initialize(listener = nil)
38
+ @listener = listener || NullListener.new
39
+ @source_files = {}
40
+ @signature_files = {}
41
+ end
42
+
43
+ def clear
44
+ listener.clear_project project: self do
45
+ @signature = nil
46
+ source_files.each_value do |file|
47
+ file.invalidate
48
+ end
49
+ end
50
+ end
51
+
52
+ def type_check(force_signatures: false, force_sources: false)
53
+ listener.check(project: self) do
54
+ should_reload_signature = force_signatures || signature_updated?
55
+ reload_signature if should_reload_signature
56
+
57
+ case sig = signature
58
+ when SignatureLoaded
59
+ each_updated_source(force: force_sources || should_reload_signature) do |file|
60
+ file.invalidate
61
+
62
+ listener.parse_source(project: self, file: file) do
63
+ file.parse()
64
+ end
65
+
66
+ listener.type_check_source(project: self, file: file) do
67
+ file.type_check(sig.check)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def success?
75
+ signature.is_a?(SignatureLoaded) &&
76
+ source_files.all? {|_, file| file.source.is_a?(Source) && file.typing }
77
+ end
78
+
79
+ def has_type_error?
80
+ source_files.any? do |_, file|
81
+ file.errors&.any?
82
+ end
83
+ end
84
+
85
+ def errors
86
+ source_files.flat_map do |_, file|
87
+ file.errors || []
88
+ end
89
+ end
90
+
91
+ # @type method each_updated_source: (?force: bool) ?{ (SourceFile) -> any } -> any
92
+ def each_updated_source(force: false)
93
+ if block_given?
94
+ source_files.each_value do |file|
95
+ if force || file.requires_type_check?
96
+ yield file
97
+ end
98
+ end
99
+ else
100
+ enum_for :each_updated_source, force: force
101
+ end
102
+ end
103
+
104
+ def signature_updated?
105
+ case sig = signature
106
+ when SignatureLoaded
107
+ signature_files.keys != sig.file_paths ||
108
+ signature_files.any? {|_, file| file.content_updated_at >= sig.loaded_at }
109
+ else
110
+ true
111
+ end
112
+ end
113
+
114
+ def reload_signature
115
+ @signature = nil
116
+
117
+ env = AST::Signature::Env.new
118
+ builder = Interface::Builder.new(signatures: env)
119
+ check = Subtyping::Check.new(builder: builder)
120
+
121
+ # @type var syntax_errors: Hash<Pathname, any>
122
+ syntax_errors = {}
123
+
124
+ listener.load_signature(project: self) do
125
+ signature_files.each_value do |file|
126
+ sigs = listener.parse_signature(project: self, file: file) do
127
+ file.parse
128
+ end
129
+
130
+ sigs.each do |sig|
131
+ env.add sig
132
+ end
133
+ rescue Racc::ParseError => exn
134
+ Steep.logger.warn { "Syntax error on #{file.path}: #{exn.inspect}" }
135
+ syntax_errors[file.path] = exn
136
+ end
137
+
138
+ if syntax_errors.empty?
139
+ listener.validate_signature(project: self) do
140
+ errors = validate_signature(check)
141
+ @signature = if errors.empty?
142
+ SignatureLoaded.new(check: check, loaded_at: Time.now, file_paths: signature_files.keys)
143
+ else
144
+ SignatureHasError.new(errors: errors)
145
+ end
146
+ end
147
+ else
148
+ @signature = SignatureHasSyntaxError.new(errors: syntax_errors)
149
+ end
150
+ end
151
+ end
152
+
153
+ def validate_signature(check)
154
+ errors = []
155
+
156
+ builder = check.builder
157
+
158
+ check.builder.signatures.each do |sig|
159
+ Steep.logger.debug { "Validating signature: #{sig.inspect}" }
160
+
161
+ case sig
162
+ when AST::Signature::Interface
163
+ yield_self do
164
+ instance_interface = builder.build_interface(sig.name)
165
+
166
+ args = instance_interface.params.map {|var| AST::Types::Var.fresh(var) }
167
+ instance_type = AST::Types::Name::Interface.new(name: sig.name, args: args)
168
+
169
+ instance_interface.instantiate(type: instance_type,
170
+ args: args,
171
+ instance_type: instance_type,
172
+ module_type: nil).validate(check)
173
+ end
174
+
175
+ when AST::Signature::Module
176
+ yield_self do
177
+ instance_interface = builder.build_instance(sig.name)
178
+ instance_args = instance_interface.params.map {|var| AST::Types::Var.fresh(var) }
179
+
180
+ module_interface = builder.build_module(sig.name)
181
+ module_args = module_interface.params.map {|var| AST::Types::Var.fresh(var) }
182
+
183
+ instance_type = AST::Types::Name::Instance.new(name: sig.name, args: instance_args)
184
+ module_type = AST::Types::Name::Module.new(name: sig.name)
185
+
186
+ Steep.logger.debug { "Validating instance methods..." }
187
+ instance_interface.instantiate(type: instance_type,
188
+ args: instance_args,
189
+ instance_type: instance_type,
190
+ module_type: module_type).validate(check)
191
+
192
+ Steep.logger.debug { "Validating class methods..." }
193
+ module_interface.instantiate(type: module_type,
194
+ args: module_args,
195
+ instance_type: instance_type,
196
+ module_type: module_type).validate(check)
197
+ end
198
+
199
+ when AST::Signature::Class
200
+ yield_self do
201
+ instance_interface = builder.build_instance(sig.name)
202
+ instance_args = instance_interface.params.map {|var| AST::Types::Var.fresh(var) }
203
+
204
+ module_interface = builder.build_class(sig.name, constructor: true)
205
+ module_args = module_interface.params.map {|var| AST::Types::Var.fresh(var) }
206
+
207
+ instance_type = AST::Types::Name::Instance.new(name: sig.name, args: instance_args)
208
+ module_type = AST::Types::Name::Class.new(name: sig.name, constructor: true)
209
+
210
+ Steep.logger.debug { "Validating instance methods..." }
211
+ instance_interface.instantiate(type: instance_type,
212
+ args: instance_args,
213
+ instance_type: instance_type,
214
+ module_type: module_type).validate(check)
215
+
216
+ Steep.logger.debug { "Validating class methods..." }
217
+ module_interface.instantiate(type: module_type,
218
+ args: module_args,
219
+ instance_type: instance_type,
220
+ module_type: module_type).validate(check)
221
+ end
222
+ end
223
+
224
+ rescue => exn
225
+ errors << exn
226
+ end
227
+
228
+ errors
229
+ end
230
+ end
231
+ end