steep 0.9.0 → 0.10.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.
@@ -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