steep 0.43.1 → 0.46.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +8 -0
  3. data/.github/workflows/ruby.yml +4 -2
  4. data/.gitignore +0 -1
  5. data/CHANGELOG.md +41 -0
  6. data/Gemfile +0 -1
  7. data/Gemfile.lock +77 -0
  8. data/bin/output_test.rb +8 -2
  9. data/lib/steep/ast/builtin.rb +7 -1
  10. data/lib/steep/ast/types/factory.rb +19 -25
  11. data/lib/steep/cli.rb +7 -1
  12. data/lib/steep/diagnostic/lsp_formatter.rb +59 -6
  13. data/lib/steep/diagnostic/ruby.rb +188 -60
  14. data/lib/steep/diagnostic/signature.rb +34 -0
  15. data/lib/steep/drivers/check.rb +3 -0
  16. data/lib/steep/drivers/init.rb +10 -3
  17. data/lib/steep/drivers/utils/driver_helper.rb +15 -0
  18. data/lib/steep/drivers/validate.rb +1 -1
  19. data/lib/steep/drivers/watch.rb +3 -0
  20. data/lib/steep/equatable.rb +21 -0
  21. data/lib/steep/index/source_index.rb +55 -5
  22. data/lib/steep/interface/block.rb +4 -0
  23. data/lib/steep/interface/function.rb +798 -579
  24. data/lib/steep/project/dsl.rb +105 -33
  25. data/lib/steep/project/options.rb +12 -53
  26. data/lib/steep/project/target.rb +21 -8
  27. data/lib/steep/server/interaction_worker.rb +239 -20
  28. data/lib/steep/server/master.rb +22 -1
  29. data/lib/steep/server/type_check_worker.rb +74 -9
  30. data/lib/steep/services/file_loader.rb +26 -19
  31. data/lib/steep/services/goto_service.rb +322 -0
  32. data/lib/steep/services/hover_content.rb +132 -80
  33. data/lib/steep/services/type_check_service.rb +25 -0
  34. data/lib/steep/source.rb +7 -10
  35. data/lib/steep/type_construction.rb +496 -518
  36. data/lib/steep/type_inference/block_params.rb +2 -5
  37. data/lib/steep/type_inference/method_params.rb +483 -0
  38. data/lib/steep/type_inference/send_args.rb +610 -128
  39. data/lib/steep/typing.rb +46 -21
  40. data/lib/steep/version.rb +1 -1
  41. data/lib/steep.rb +4 -1
  42. data/sample/Steepfile +10 -3
  43. data/sig/steep/type_inference/send_args.rbs +42 -0
  44. data/smoke/alias/Steepfile +2 -1
  45. data/smoke/and/Steepfile +2 -1
  46. data/smoke/array/Steepfile +2 -1
  47. data/smoke/array/test_expectations.yml +3 -3
  48. data/smoke/block/Steepfile +2 -2
  49. data/smoke/block/c.rb +0 -1
  50. data/smoke/case/Steepfile +2 -1
  51. data/smoke/class/Steepfile +2 -1
  52. data/smoke/class/test_expectations.yml +12 -15
  53. data/smoke/const/Steepfile +2 -1
  54. data/smoke/const/test_expectations.yml +0 -10
  55. data/smoke/diagnostics/Steepfile +2 -1
  56. data/smoke/diagnostics/a.rbs +0 -4
  57. data/smoke/diagnostics/different_method_parameter_kind.rb +9 -0
  58. data/smoke/diagnostics/method_arity_mismatch.rb +2 -2
  59. data/smoke/diagnostics/method_parameter_mismatch.rb +10 -0
  60. data/smoke/diagnostics/test_expectations.yml +108 -57
  61. data/smoke/diagnostics-rbs/Steepfile +1 -1
  62. data/smoke/diagnostics-rbs/mixin-class-error.rbs +6 -0
  63. data/smoke/diagnostics-rbs/test_expectations.yml +12 -0
  64. data/smoke/diagnostics-rbs-duplicated/Steepfile +2 -1
  65. data/smoke/diagnostics-ruby-unsat/Steepfile +6 -0
  66. data/smoke/diagnostics-ruby-unsat/a.rbs +3 -0
  67. data/smoke/diagnostics-ruby-unsat/test_expectations.yml +27 -0
  68. data/smoke/{diagnostics → diagnostics-ruby-unsat}/unsatisfiable_constraint.rb +0 -1
  69. data/smoke/dstr/Steepfile +2 -1
  70. data/smoke/ensure/Steepfile +2 -1
  71. data/smoke/ensure/test_expectations.yml +3 -3
  72. data/smoke/enumerator/Steepfile +2 -1
  73. data/smoke/enumerator/test_expectations.yml +1 -1
  74. data/smoke/extension/Steepfile +2 -1
  75. data/smoke/hash/Steepfile +2 -1
  76. data/smoke/hello/Steepfile +2 -1
  77. data/smoke/if/Steepfile +2 -1
  78. data/smoke/implements/Steepfile +2 -1
  79. data/smoke/initialize/Steepfile +2 -1
  80. data/smoke/integer/Steepfile +2 -1
  81. data/smoke/interface/Steepfile +2 -1
  82. data/smoke/kwbegin/Steepfile +2 -1
  83. data/smoke/lambda/Steepfile +2 -1
  84. data/smoke/literal/Steepfile +2 -1
  85. data/smoke/literal/test_expectations.yml +2 -2
  86. data/smoke/map/Steepfile +2 -1
  87. data/smoke/method/Steepfile +2 -1
  88. data/smoke/method/test_expectations.yml +11 -10
  89. data/smoke/module/Steepfile +2 -1
  90. data/smoke/regexp/Steepfile +2 -1
  91. data/smoke/regression/Steepfile +2 -1
  92. data/smoke/regression/issue_372.rb +8 -0
  93. data/smoke/regression/issue_372.rbs +4 -0
  94. data/smoke/regression/test_expectations.yml +0 -12
  95. data/smoke/rescue/Steepfile +2 -1
  96. data/smoke/rescue/test_expectations.yml +3 -3
  97. data/smoke/self/Steepfile +2 -1
  98. data/smoke/skip/Steepfile +2 -1
  99. data/smoke/stdout/Steepfile +2 -1
  100. data/smoke/super/Steepfile +2 -1
  101. data/smoke/toplevel/Steepfile +2 -1
  102. data/smoke/toplevel/test_expectations.yml +3 -3
  103. data/smoke/tsort/Steepfile +4 -5
  104. data/smoke/tsort/test_expectations.yml +2 -2
  105. data/smoke/type_case/Steepfile +2 -1
  106. data/smoke/unexpected/Steepfile +2 -1
  107. data/smoke/yield/Steepfile +2 -1
  108. data/steep.gemspec +2 -2
  109. metadata +24 -10
@@ -7,21 +7,21 @@ module Steep
7
7
  attr_reader :libraries
8
8
  attr_reader :signatures
9
9
  attr_reader :ignored_sources
10
- attr_reader :vendor_dir
11
- attr_reader :strictness_level
12
- attr_reader :typing_option_hash
10
+ attr_reader :stdlib_root
11
+ attr_reader :core_root
13
12
  attr_reader :repo_paths
13
+ attr_reader :code_diagnostics_config
14
14
 
15
- def initialize(name, sources: [], libraries: [], signatures: [], ignored_sources: [], repo_paths: [])
15
+ def initialize(name, sources: [], libraries: [], signatures: [], ignored_sources: [], repo_paths: [], code_diagnostics_config: {})
16
16
  @name = name
17
17
  @sources = sources
18
18
  @libraries = libraries
19
19
  @signatures = signatures
20
20
  @ignored_sources = ignored_sources
21
- @vendor_dir = nil
22
- @strictness_level = :default
23
- @typing_option_hash = {}
21
+ @core_root = nil
22
+ @stdlib_root = nil
24
23
  @repo_paths = []
24
+ @code_diagnostics_config = code_diagnostics_config
25
25
  end
26
26
 
27
27
  def initialize_copy(other)
@@ -30,10 +30,10 @@ module Steep
30
30
  @libraries = other.libraries.dup
31
31
  @signatures = other.signatures.dup
32
32
  @ignored_sources = other.ignored_sources.dup
33
- @vendor_dir = other.vendor_dir
34
- @strictness_level = other.strictness_level
35
- @typing_option_hash = other.typing_option_hash
36
33
  @repo_paths = other.repo_paths.dup
34
+ @core_root = other.core_root
35
+ @stdlib_root = other.stdlib_root
36
+ @code_diagnostics_config = other.code_diagnostics_config.dup
37
37
  end
38
38
 
39
39
  def check(*args)
@@ -48,9 +48,52 @@ module Steep
48
48
  libraries.push(*args)
49
49
  end
50
50
 
51
- def typing_options(level = @strictness_level, **hash)
52
- @strictness_level = level
53
- @typing_option_hash = hash
51
+ def typing_options(level = nil, **hash)
52
+ Steep.logger.error "#typing_options is deprecated and has no effect as of version 0.46.0. Update your Steepfile as follows for (almost) equivalent setting:"
53
+
54
+ messages = []
55
+
56
+ messages << "# D = Steep::Diagnostic # Define a constant to shorten namespace"
57
+
58
+ case level
59
+ when :strict
60
+ messages << "configure_code_diagnostics(D::Ruby.strict) # :strict"
61
+ when :default
62
+ messages << "configure_code_diagnostics(D::Ruby.default) # :default"
63
+ when :lenient
64
+ messages << "configure_code_diagnostics(D::Ruby.lenient) # :lenient"
65
+ end
66
+
67
+ messages.each do |msg|
68
+ Steep.logger.error " #{msg}"
69
+ end
70
+
71
+ config = []
72
+
73
+ if hash[:allow_missing_definitions]
74
+ config << "hash[D::Ruby::MethodDefinitionMissing] = nil # allow_missing_definitions"
75
+ end
76
+
77
+ if hash[:allow_fallback_any]
78
+ config << "hash[D::Ruby::FallbackAny] = nil # allow_fallback_any"
79
+ end
80
+
81
+ if hash[:allow_unknown_constant_assignment]
82
+ config << "hash[D::Ruby::UnknownConstantAssigned] = nil # allow_unknown_constant_assignment"
83
+ end
84
+
85
+ if hash[:allow_unknown_method_calls]
86
+ config << "hash[D::Ruby::NoMethod] = nil # allow_unknown_method_calls"
87
+ end
88
+
89
+ unless config.empty?
90
+ Steep.logger.error " configure_code_diagnostics do |hash|"
91
+ config.each do |c|
92
+ Steep.logger.error " #{c}"
93
+ end
94
+ Steep.logger.error " end"
95
+ end
96
+
54
97
  end
55
98
 
56
99
  def signature(*args)
@@ -68,20 +111,50 @@ module Steep
68
111
  end
69
112
 
70
113
  def no_builtin!(value = true)
71
- Steep.logger.error "`no_builtin!` in Steepfile is deprecated and ignored. Use `vendor` instead."
114
+ Steep.logger.error "`#no_builtin!` in Steepfile is deprecated and ignored. Use `#stdlib_path` instead."
72
115
  end
73
116
 
74
117
  def vendor(dir = "vendor/sigs", stdlib: nil, gems: nil)
75
- if stdlib || gems
76
- Steep.logger.warn { "#vendor with stdlib: or gems: keyword is deprecated." }
77
- end
118
+ Steep.logger.error "`#vendor` in Steepfile is deprecated and ignored. Use `#stdlib_path` instead."
119
+ end
78
120
 
79
- @vendor_dir = Pathname(dir)
121
+ def stdlib_path(core_root:, stdlib_root:)
122
+ @core_root = core_root ? Pathname(core_root) : core_root
123
+ @stdlib_root = stdlib_root ? Pathname(stdlib_root) : stdlib_root
80
124
  end
81
125
 
82
126
  def repo_path(*paths)
83
127
  @repo_paths.push(*paths.map {|s| Pathname(s) })
84
128
  end
129
+
130
+ # Configure the code diagnostics printing setup.
131
+ #
132
+ # Yields a hash, and the update the hash in the block.
133
+ #
134
+ # ```rb
135
+ # D = Steep::Diagnostic
136
+ #
137
+ # configure_code_diagnostics do |hash|
138
+ # # Assign one of :error, :warning, :information, :hint or :nil to error classes.
139
+ # hash[D::Ruby::UnexpectedPositionalArgument] = :error
140
+ # end
141
+ # ```
142
+ #
143
+ # Passing a hash is also allowed.
144
+ #
145
+ # ```rb
146
+ # D = Steep::Diagnostic
147
+ #
148
+ # configure_code_diagnostics(D::Ruby.lenient)
149
+ # ```
150
+ #
151
+ def configure_code_diagnostics(hash = nil)
152
+ if hash
153
+ code_diagnostics_config.merge!(hash)
154
+ end
155
+
156
+ yield code_diagnostics_config if block_given?
157
+ end
85
158
  end
86
159
 
87
160
  attr_reader :project
@@ -107,7 +180,9 @@ module Steep
107
180
  end
108
181
 
109
182
  def self.parse(project, code, filename: "Steepfile")
110
- self.new(project: project).instance_eval(code, filename)
183
+ Steep.logger.tagged filename do
184
+ self.new(project: project).instance_eval(code, filename)
185
+ end
111
186
  end
112
187
 
113
188
  def target(name, template: nil, &block)
@@ -115,10 +190,12 @@ module Steep
115
190
  self.class.templates[template]&.dup&.update(name: name) or
116
191
  raise "Unknown template: #{template}, available templates: #{@@templates.keys.join(", ")}"
117
192
  else
118
- TargetDSL.new(name)
193
+ TargetDSL.new(name, code_diagnostics_config: Diagnostic::Ruby.default.dup)
119
194
  end
120
195
 
121
- target.instance_eval(&block) if block_given?
196
+ Steep.logger.tagged "target=#{name}" do
197
+ target.instance_eval(&block) if block_given?
198
+ end
122
199
 
123
200
  source_pattern = Pattern.new(patterns: target.sources, ignores: target.ignored_sources, ext: ".rb")
124
201
  signature_pattern = Pattern.new(patterns: target.signatures, ext: ".rbs")
@@ -129,18 +206,13 @@ module Steep
129
206
  signature_pattern: signature_pattern,
130
207
  options: Options.new.tap do |options|
131
208
  options.libraries.push(*target.libraries)
132
- options.repository_paths.push(*target.repo_paths)
133
- options.vendor_path = target.vendor_dir
134
-
135
- case target.strictness_level
136
- when :strict
137
- options.apply_strict_typing_options!
138
- when :lenient
139
- options.apply_lenient_typing_options!
140
- end
141
-
142
- options.merge!(target.typing_option_hash)
143
- end
209
+ options.paths = Options::PathOptions.new(
210
+ core_root: target.core_root,
211
+ stdlib_root: target.stdlib_root,
212
+ repo_paths: target.repo_paths
213
+ )
214
+ end,
215
+ code_diagnostics_config: target.code_diagnostics_config
144
216
  ).tap do |target|
145
217
  project.targets << target
146
218
  end
@@ -1,63 +1,22 @@
1
1
  module Steep
2
2
  class Project
3
3
  class Options
4
- attr_accessor :allow_fallback_any
5
- attr_accessor :allow_missing_definitions
6
- attr_accessor :allow_unknown_constant_assignment
7
- attr_accessor :allow_unknown_method_calls
8
- attr_accessor :vendor_path
9
- attr_reader :libraries
10
- attr_reader :repository_paths
11
-
12
- def initialize
13
- apply_default_typing_options!
14
- self.vendor_path = nil
15
-
16
- @libraries = []
17
- @repository_paths = []
18
- end
19
-
20
- def apply_default_typing_options!
21
- self.allow_fallback_any = true
22
- self.allow_missing_definitions = true
23
- self.allow_unknown_constant_assignment = false
24
- self.allow_unknown_method_calls = false
25
- end
26
-
27
- def apply_strict_typing_options!
28
- self.allow_fallback_any = false
29
- self.allow_missing_definitions = false
30
- self.allow_unknown_constant_assignment = false
31
- self.allow_unknown_method_calls = false
32
- end
33
-
34
- def apply_lenient_typing_options!
35
- self.allow_fallback_any = true
36
- self.allow_missing_definitions = true
37
- self.allow_unknown_constant_assignment = true
38
- self.allow_unknown_method_calls = true
39
- end
4
+ PathOptions = Struct.new(:core_root, :stdlib_root, :repo_paths, keyword_init: true) do
5
+ def customized_stdlib?
6
+ stdlib_root != nil
7
+ end
40
8
 
41
- def error_to_report?(error)
42
- case
43
- when error.is_a?(Diagnostic::Ruby::FallbackAny)
44
- !allow_fallback_any
45
- when error.is_a?(Diagnostic::Ruby::MethodDefinitionMissing)
46
- !allow_missing_definitions
47
- when error.is_a?(Diagnostic::Ruby::NoMethod)
48
- !allow_unknown_method_calls
49
- when error.is_a?(Diagnostic::Ruby::UnknownConstantAssigned)
50
- !allow_unknown_constant_assignment
51
- else
52
- true
9
+ def customized_core?
10
+ core_root != nil
53
11
  end
54
12
  end
55
13
 
56
- def merge!(hash)
57
- self.allow_fallback_any = hash[:allow_fallback_any] if hash.key?(:allow_fallback_any)
58
- self.allow_missing_definitions = hash[:allow_missing_definitions] if hash.key?(:allow_missing_definitions)
59
- self.allow_unknown_constant_assignment = hash[:allow_unknown_constant_assignment] if hash.key?(:allow_unknown_constant_assignment)
60
- self.allow_unknown_method_calls = hash[:allow_unknown_method_calls] if hash.key?(:allow_unknown_method_calls)
14
+ attr_reader :libraries
15
+ attr_accessor :paths
16
+
17
+ def initialize
18
+ @paths = PathOptions.new(repo_paths: [])
19
+ @libraries = []
61
20
  end
62
21
  end
63
22
  end
@@ -6,12 +6,14 @@ module Steep
6
6
 
7
7
  attr_reader :source_pattern
8
8
  attr_reader :signature_pattern
9
+ attr_reader :code_diagnostics_config
9
10
 
10
- def initialize(name:, options:, source_pattern:, signature_pattern:)
11
+ def initialize(name:, options:, source_pattern:, signature_pattern:, code_diagnostics_config:)
11
12
  @name = name
12
13
  @options = options
13
14
  @source_pattern = source_pattern
14
15
  @signature_pattern = signature_pattern
16
+ @code_diagnostics_config = code_diagnostics_config
15
17
 
16
18
  @source_files = {}
17
19
  @signature_files = {}
@@ -30,16 +32,27 @@ module Steep
30
32
  end
31
33
 
32
34
  def self.construct_env_loader(options:, project:)
33
- repo = RBS::Repository.new(no_stdlib: options.vendor_path)
34
- options.repository_paths.each do |path|
35
+ repo = RBS::Repository.new(no_stdlib: options.paths.customized_stdlib?)
36
+
37
+ if options.paths.stdlib_root
38
+ repo.add(project.absolute_path(options.paths.stdlib_root))
39
+ end
40
+
41
+ options.paths.repo_paths.each do |path|
35
42
  repo.add(project.absolute_path(path))
36
43
  end
37
44
 
38
- loader = RBS::EnvironmentLoader.new(
39
- core_root: options.vendor_path ? nil : RBS::EnvironmentLoader::DEFAULT_CORE_ROOT,
40
- repository: repo
41
- )
42
- loader.add(path: options.vendor_path) if options.vendor_path
45
+ core_root_path =
46
+ if options.paths.customized_core?
47
+ if options.paths.core_root
48
+ project.absolute_path(options.paths.core_root)
49
+ end
50
+ else
51
+ RBS::EnvironmentLoader::DEFAULT_CORE_ROOT
52
+ end
53
+
54
+ loader = RBS::EnvironmentLoader.new(core_root: core_root_path, repository: repo)
55
+
43
56
  options.libraries.each do |lib|
44
57
  name, version = lib.split(/:/, 2)
45
58
  loader.add(library: name, version: version)
@@ -7,6 +7,8 @@ module Steep
7
7
  HoverJob = Struct.new(:id, :path, :line, :column, keyword_init: true)
8
8
  CompletionJob = Struct.new(:id, :path, :line, :column, :trigger, keyword_init: true)
9
9
 
10
+ LSP = LanguageServer::Protocol
11
+
10
12
  attr_reader :service
11
13
 
12
14
  def initialize(project:, reader:, writer:, queue: Queue.new)
@@ -65,7 +67,7 @@ module Steep
65
67
  uri = URI.parse(params[:textDocument][:uri])
66
68
  path = project.relative_path(Pathname(uri.path))
67
69
  line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
68
- trigger = params[:context][:triggerCharacter]
70
+ trigger = params.dig(:context, :triggerCharacter)
69
71
 
70
72
  queue << CompletionJob.new(id: id, path: path, line: line, column: column, trigger: trigger)
71
73
  end
@@ -77,11 +79,12 @@ module Steep
77
79
  Steep.logger.info { "path=#{job.path}, line=#{job.line}, column=#{job.column}" }
78
80
 
79
81
  hover = Services::HoverContent.new(service: service)
80
- content = hover.content_for(path: job.path, line: job.line, column: job.column+1)
82
+ content = hover.content_for(path: job.path, line: job.line, column: job.column)
81
83
  if content
82
84
  range = content.location.yield_self do |location|
83
- start_position = { line: location.line - 1, character: location.column }
84
- end_position = { line: location.last_line - 1, character: location.last_column }
85
+ lsp_range = location.as_lsp_range
86
+ start_position = { line: lsp_range[:start][:line], character: lsp_range[:start][:character] }
87
+ end_position = { line: lsp_range[:end][:line], character: lsp_range[:end][:character] }
85
88
  { start: start_position, end: end_position }
86
89
  end
87
90
 
@@ -99,6 +102,36 @@ module Steep
99
102
 
100
103
  def format_hover(content)
101
104
  case content
105
+ when Services::HoverContent::TypeAliasContent
106
+ comment = content.decl.comment&.string || ''
107
+
108
+ <<-MD
109
+ #{comment}
110
+
111
+ ```rbs
112
+ #{retrieve_decl_information(content.decl)}
113
+ ```
114
+ MD
115
+ when Services::HoverContent::InterfaceContent
116
+ comment = content.decl.comment&.string || ''
117
+
118
+ <<-MD
119
+ #{comment}
120
+
121
+ ```rbs
122
+ #{retrieve_decl_information(content.decl)}
123
+ ```
124
+ MD
125
+ when Services::HoverContent::ClassContent
126
+ comment = content.decl.comment&.string || ''
127
+
128
+ <<-MD
129
+ #{comment}
130
+
131
+ ```rbs
132
+ #{retrieve_decl_information(content.decl)}
133
+ ```
134
+ MD
102
135
  when Services::HoverContent::VariableContent
103
136
  "`#{content.name}`: `#{content.type.to_s}`"
104
137
  when Services::HoverContent::MethodCallContent
@@ -151,32 +184,207 @@ HOVER
151
184
  Steep.logger.tagged("#response_to_completion") do
152
185
  Steep.measure "Generating response" do
153
186
  Steep.logger.info "path: #{job.path}, line: #{job.line}, column: #{job.column}, trigger: #{job.trigger}"
187
+ case
188
+ when target = project.target_for_source_path(job.path)
189
+ file = service.source_files[job.path] or return
190
+ subtyping = service.signature_services[target.name].current_subtyping or return
191
+
192
+ provider = Services::CompletionProvider.new(source_text: file.content, path: job.path, subtyping: subtyping)
193
+ items = begin
194
+ provider.run(line: job.line, column: job.column)
195
+ rescue Parser::SyntaxError
196
+ []
197
+ end
198
+
199
+ completion_items = items.map do |item|
200
+ format_completion_item(item)
201
+ end
202
+
203
+ Steep.logger.debug "items = #{completion_items.inspect}"
204
+
205
+ LSP::Interface::CompletionList.new(
206
+ is_incomplete: false,
207
+ items: completion_items
208
+ )
209
+ when (_, targets = project.targets_for_path(job.path))
210
+ target = targets[0] or return
211
+ sig_service = service.signature_services[target.name]
212
+ relative_path = job.path
213
+ buffer = RBS::Buffer.new(name: relative_path, content: sig_service.files[relative_path].content)
214
+ pos = buffer.loc_to_pos([job.line, job.column])
215
+ prefix = buffer.content[0...pos].reverse[/\A[\w\d]*/].reverse
216
+
217
+ case sig_service.status
218
+ when Steep::Services::SignatureService::SyntaxErrorStatus, Steep::Services::SignatureService::AncestorErrorStatus
219
+ return
220
+ end
221
+
222
+ decls = sig_service.files[relative_path].decls
223
+ locator = RBS::Locator.new(decls: decls)
224
+
225
+ hd, tail = locator.find2(line: job.line, column: job.column)
226
+
227
+ namespace = []
228
+ tail.each do |t|
229
+ case t
230
+ when RBS::AST::Declarations::Module, RBS::AST::Declarations::Class
231
+ namespace << t.name.to_namespace
232
+ end
233
+ end
234
+ context = []
154
235
 
155
- target = project.target_for_source_path(job.path) or return
156
- file = service.source_files[job.path] or return
157
- subtyping = service.signature_services[target.name].current_subtyping or return
236
+ namespace.each do |ns|
237
+ context.map! { |n| ns + n }
238
+ context << ns
239
+ end
240
+
241
+ context.map!(&:absolute!)
242
+
243
+ class_items = sig_service.latest_env.class_decls.keys.map { |type_name|
244
+ format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
245
+ }.compact
246
+
247
+ alias_items = sig_service.latest_env.alias_decls.keys.map { |type_name|
248
+ format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
249
+ }.compact
250
+
251
+ interface_items = sig_service.latest_env.interface_decls.keys.map {|type_name|
252
+ format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
253
+ }.compact
158
254
 
159
- provider = Services::CompletionProvider.new(source_text: file.content, path: job.path, subtyping: subtyping)
160
- items = begin
161
- provider.run(line: job.line, column: job.column)
162
- rescue Parser::SyntaxError
163
- []
164
- end
255
+ completion_items = class_items + alias_items + interface_items
165
256
 
166
- completion_items = items.map do |item|
167
- format_completion_item(item)
257
+ LSP::Interface::CompletionList.new(
258
+ is_incomplete: false,
259
+ items: completion_items
260
+ )
168
261
  end
262
+ end
263
+ end
264
+ end
265
+
266
+ def format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
267
+ range = LanguageServer::Protocol::Interface::Range.new(
268
+ start: LanguageServer::Protocol::Interface::Position.new(
269
+ line: job.line - 1,
270
+ character: job.column - prefix.size
271
+ ),
272
+ end: LanguageServer::Protocol::Interface::Position.new(
273
+ line: job.line - 1,
274
+ character: job.column - prefix.size
275
+ )
276
+ )
169
277
 
170
- Steep.logger.debug "items = #{completion_items.inspect}"
278
+ name = relative_name_in_context(type_name, context).to_s
171
279
 
172
- LSP::Interface::CompletionList.new(
173
- is_incomplete: false,
174
- items: completion_items
175
- )
280
+ return unless name.start_with?(prefix)
281
+
282
+ case type_name.kind
283
+ when :class
284
+ class_decl = sig_service.latest_env.class_decls[type_name]&.decls[0]&.decl or raise
285
+
286
+ LanguageServer::Protocol::Interface::CompletionItem.new(
287
+ label: "#{name}",
288
+ documentation: format_comment(class_decl.comment),
289
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
290
+ range: range,
291
+ new_text: name
292
+ ),
293
+ kind: LSP::Constant::CompletionItemKind::CLASS,
294
+ insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
295
+
296
+ )
297
+ when :alias
298
+ alias_decl = sig_service.latest_env.alias_decls[type_name]&.decl or raise
299
+ LanguageServer::Protocol::Interface::CompletionItem.new(
300
+ label: "#{name}",
301
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
302
+ range: range,
303
+ new_text: name
304
+ ),
305
+ documentation: format_comment(alias_decl.comment),
306
+ # https://github.com/microsoft/vscode-languageserver-node/blob/6d78fc4d25719b231aba64a721a606f58b9e0a5f/client/src/common/client.ts#L624-L650
307
+ kind: LSP::Constant::CompletionItemKind::FIELD,
308
+ insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
309
+ )
310
+ when :interface
311
+ interface_decl = sig_service.latest_env.interface_decls[type_name]&.decl or raise
312
+
313
+ LanguageServer::Protocol::Interface::CompletionItem.new(
314
+ label: "#{name}",
315
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
316
+ range: range,
317
+ new_text: name
318
+ ),
319
+ documentation: format_comment(interface_decl.comment),
320
+ kind: LanguageServer::Protocol::Constant::CompletionItemKind::INTERFACE,
321
+ insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET
322
+ )
323
+ end
324
+ end
325
+
326
+ def format_comment(comment)
327
+ if comment
328
+ LSP::Interface::MarkupContent.new(
329
+ kind: LSP::Constant::MarkupKind::MARKDOWN,
330
+ value: comment.string
331
+ )
332
+ end
333
+ end
334
+
335
+ def name_and_params(name, params)
336
+ if params.empty?
337
+ "#{name}"
338
+ else
339
+ ps = params.each.map do |param|
340
+ s = ""
341
+ if param.skip_validation
342
+ s << "unchecked "
343
+ end
344
+ case param.variance
345
+ when :invariant
346
+ # nop
347
+ when :covariant
348
+ s << "out "
349
+ when :contravariant
350
+ s << "in "
351
+ end
352
+ s + param.name.to_s
353
+ end
354
+
355
+ "#{name}[#{ps.join(", ")}]"
356
+ end
357
+ end
358
+
359
+ def name_and_args(name, args)
360
+ if name && args
361
+ if args.empty?
362
+ "#{name}"
363
+ else
364
+ "#{name}[#{args.join(", ")}]"
176
365
  end
177
366
  end
178
367
  end
179
368
 
369
+ def retrieve_decl_information(decl)
370
+ case decl
371
+ when RBS::AST::Declarations::Class
372
+ super_class = if super_class = decl.super_class
373
+ " < #{name_and_args(super_class.name, super_class.args)}"
374
+ end
375
+ "class #{name_and_params(decl.name, decl.type_params)}#{super_class}"
376
+ when RBS::AST::Declarations::Module
377
+ self_type = unless decl.self_types.empty?
378
+ " : #{decl.self_types.join(", ")}"
379
+ end
380
+ "module #{name_and_params(decl.name, decl.type_params)}#{self_type}"
381
+ when RBS::AST::Declarations::Alias
382
+ "type #{decl.name} = #{decl.type}"
383
+ when RBS::AST::Declarations::Interface
384
+ "interface #{name_and_params(decl.name, decl.type_params)}"
385
+ end
386
+ end
387
+
180
388
  def format_completion_item(item)
181
389
  range = LanguageServer::Protocol::Interface::Range.new(
182
390
  start: LanguageServer::Protocol::Interface::Position.new(
@@ -297,6 +505,17 @@ HOVER
297
505
 
298
506
  params.join(", ")
299
507
  end
508
+
509
+ def relative_name_in_context(type_name, context)
510
+ context.each do |namespace|
511
+ if (type_name.to_s == namespace.to_type_name.to_s || type_name.namespace.to_s == "::")
512
+ return RBS::TypeName.new(namespace: RBS::Namespace.empty, name: type_name.name)
513
+ elsif type_name.to_s.start_with?(namespace.to_s)
514
+ return TypeName(type_name.to_s.sub(namespace.to_type_name.to_s, '')).relative!
515
+ end
516
+ end
517
+ type_name
518
+ end
300
519
  end
301
520
  end
302
521
  end