solargraph 0.38.6 → 0.39.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/SPONSORS.md +9 -0
  3. data/lib/solargraph.rb +1 -0
  4. data/lib/solargraph/api_map.rb +24 -13
  5. data/lib/solargraph/api_map/bundler_methods.rb +3 -0
  6. data/lib/solargraph/api_map/source_to_yard.rb +1 -6
  7. data/lib/solargraph/api_map/store.rb +19 -3
  8. data/lib/solargraph/complex_type/type_methods.rb +4 -4
  9. data/lib/solargraph/convention/base.rb +0 -5
  10. data/lib/solargraph/core_fills.rb +28 -2
  11. data/lib/solargraph/diagnostics/base.rb +1 -0
  12. data/lib/solargraph/diagnostics/type_check.rb +13 -13
  13. data/lib/solargraph/language_server/host.rb +11 -2
  14. data/lib/solargraph/language_server/host/sources.rb +20 -11
  15. data/lib/solargraph/language_server/message/text_document/formatting.rb +16 -11
  16. data/lib/solargraph/language_server/transport/data_reader.rb +2 -0
  17. data/lib/solargraph/library.rb +3 -3
  18. data/lib/solargraph/parser.rb +26 -0
  19. data/lib/solargraph/parser/comment_ripper.rb +52 -0
  20. data/lib/solargraph/parser/legacy.rb +12 -0
  21. data/lib/solargraph/parser/legacy/class_methods.rb +105 -0
  22. data/lib/solargraph/parser/legacy/flawed_builder.rb +16 -0
  23. data/lib/solargraph/parser/legacy/node_chainer.rb +115 -0
  24. data/lib/solargraph/parser/legacy/node_methods.rb +289 -0
  25. data/lib/solargraph/parser/legacy/node_processors.rb +54 -0
  26. data/lib/solargraph/parser/legacy/node_processors/alias_node.rb +23 -0
  27. data/lib/solargraph/parser/legacy/node_processors/args_node.rb +35 -0
  28. data/lib/solargraph/parser/legacy/node_processors/begin_node.rb +15 -0
  29. data/lib/solargraph/parser/legacy/node_processors/block_node.rb +22 -0
  30. data/lib/solargraph/parser/legacy/node_processors/casgn_node.rb +25 -0
  31. data/lib/solargraph/parser/legacy/node_processors/cvasgn_node.rb +23 -0
  32. data/lib/solargraph/parser/legacy/node_processors/def_node.rb +63 -0
  33. data/lib/solargraph/parser/legacy/node_processors/defs_node.rb +36 -0
  34. data/lib/solargraph/parser/legacy/node_processors/gvasgn_node.rb +23 -0
  35. data/lib/solargraph/parser/legacy/node_processors/ivasgn_node.rb +38 -0
  36. data/lib/solargraph/parser/legacy/node_processors/lvasgn_node.rb +28 -0
  37. data/lib/solargraph/parser/legacy/node_processors/namespace_node.rb +39 -0
  38. data/lib/solargraph/parser/legacy/node_processors/orasgn_node.rb +16 -0
  39. data/lib/solargraph/parser/legacy/node_processors/resbody_node.rb +36 -0
  40. data/lib/solargraph/parser/legacy/node_processors/sclass_node.rb +21 -0
  41. data/lib/solargraph/parser/legacy/node_processors/send_node.rb +232 -0
  42. data/lib/solargraph/parser/legacy/node_processors/sym_node.rb +18 -0
  43. data/lib/solargraph/parser/node_methods.rb +43 -0
  44. data/lib/solargraph/parser/node_processor.rb +43 -0
  45. data/lib/solargraph/parser/node_processor/base.rb +77 -0
  46. data/lib/solargraph/{source_map → parser}/region.rb +11 -6
  47. data/lib/solargraph/parser/rubyvm.rb +40 -0
  48. data/lib/solargraph/parser/rubyvm/class_methods.rb +146 -0
  49. data/lib/solargraph/parser/rubyvm/node_chainer.rb +133 -0
  50. data/lib/solargraph/parser/rubyvm/node_methods.rb +279 -0
  51. data/lib/solargraph/parser/rubyvm/node_processors.rb +60 -0
  52. data/lib/solargraph/parser/rubyvm/node_processors/alias_node.rb +23 -0
  53. data/lib/solargraph/parser/rubyvm/node_processors/args_node.rb +62 -0
  54. data/lib/solargraph/parser/rubyvm/node_processors/begin_node.rb +15 -0
  55. data/lib/solargraph/parser/rubyvm/node_processors/block_node.rb +22 -0
  56. data/lib/solargraph/parser/rubyvm/node_processors/casgn_node.rb +22 -0
  57. data/lib/solargraph/parser/rubyvm/node_processors/cvasgn_node.rb +23 -0
  58. data/lib/solargraph/parser/rubyvm/node_processors/def_node.rb +64 -0
  59. data/lib/solargraph/parser/rubyvm/node_processors/defs_node.rb +57 -0
  60. data/lib/solargraph/parser/rubyvm/node_processors/gvasgn_node.rb +23 -0
  61. data/lib/solargraph/parser/rubyvm/node_processors/ivasgn_node.rb +38 -0
  62. data/lib/solargraph/parser/rubyvm/node_processors/kw_arg_node.rb +34 -0
  63. data/lib/solargraph/parser/rubyvm/node_processors/lit_node.rb +20 -0
  64. data/lib/solargraph/parser/rubyvm/node_processors/lvasgn_node.rb +27 -0
  65. data/lib/solargraph/parser/rubyvm/node_processors/namespace_node.rb +39 -0
  66. data/lib/solargraph/parser/rubyvm/node_processors/opt_arg_node.rb +26 -0
  67. data/lib/solargraph/parser/rubyvm/node_processors/orasgn_node.rb +15 -0
  68. data/lib/solargraph/parser/rubyvm/node_processors/resbody_node.rb +45 -0
  69. data/lib/solargraph/parser/rubyvm/node_processors/sclass_node.rb +21 -0
  70. data/lib/solargraph/parser/rubyvm/node_processors/scope_node.rb +15 -0
  71. data/lib/solargraph/parser/rubyvm/node_processors/send_node.rb +292 -0
  72. data/lib/solargraph/parser/rubyvm/node_processors/sym_node.rb +18 -0
  73. data/lib/solargraph/parser/snippet.rb +13 -0
  74. data/lib/solargraph/pin/attribute.rb +8 -0
  75. data/lib/solargraph/pin/base.rb +1 -1
  76. data/lib/solargraph/pin/base_method.rb +26 -4
  77. data/lib/solargraph/pin/base_variable.rb +7 -8
  78. data/lib/solargraph/pin/block.rb +1 -1
  79. data/lib/solargraph/pin/constant.rb +16 -0
  80. data/lib/solargraph/pin/conversions.rb +2 -2
  81. data/lib/solargraph/pin/method.rb +26 -14
  82. data/lib/solargraph/pin/parameter.rb +56 -2
  83. data/lib/solargraph/pin/reference.rb +1 -0
  84. data/lib/solargraph/pin/reference/override.rb +2 -0
  85. data/lib/solargraph/pin/reference/prepend.rb +10 -0
  86. data/lib/solargraph/pin/yard_pin/constant.rb +2 -4
  87. data/lib/solargraph/pin/yard_pin/method.rb +34 -18
  88. data/lib/solargraph/pin/yard_pin/namespace.rb +3 -11
  89. data/lib/solargraph/pin/yard_pin/yard_mixin.rb +3 -12
  90. data/lib/solargraph/range.rb +8 -2
  91. data/lib/solargraph/shell.rb +17 -11
  92. data/lib/solargraph/source.rb +114 -135
  93. data/lib/solargraph/source/chain.rb +11 -3
  94. data/lib/solargraph/source/chain/call.rb +1 -2
  95. data/lib/solargraph/source/chain/constant.rb +44 -8
  96. data/lib/solargraph/source/chain/link.rb +1 -0
  97. data/lib/solargraph/source/cursor.rb +2 -28
  98. data/lib/solargraph/source/source_chainer.rb +12 -6
  99. data/lib/solargraph/source/updater.rb +2 -0
  100. data/lib/solargraph/source_map.rb +0 -2
  101. data/lib/solargraph/source_map/clip.rb +24 -17
  102. data/lib/solargraph/source_map/mapper.rb +34 -29
  103. data/lib/solargraph/type_checker.rb +367 -275
  104. data/lib/solargraph/type_checker/checks.rb +95 -0
  105. data/lib/solargraph/type_checker/param_def.rb +2 -17
  106. data/lib/solargraph/type_checker/rules.rb +53 -0
  107. data/lib/solargraph/version.rb +1 -1
  108. data/lib/solargraph/views/environment.erb +5 -3
  109. data/lib/solargraph/workspace.rb +17 -2
  110. data/lib/solargraph/workspace/config.rb +15 -7
  111. data/lib/solargraph/yard_map.rb +89 -49
  112. data/lib/solargraph/yard_map/core_docs.rb +1 -1
  113. data/solargraph.gemspec +2 -1
  114. metadata +88 -32
  115. data/OVERVIEW.md +0 -37
  116. data/lib/solargraph/source/flawed_builder.rb +0 -15
  117. data/lib/solargraph/source/node_chainer.rb +0 -111
  118. data/lib/solargraph/source/node_methods.rb +0 -240
  119. data/lib/solargraph/source_map/node_processor.rb +0 -85
  120. data/lib/solargraph/source_map/node_processor/alias_node.rb +0 -21
  121. data/lib/solargraph/source_map/node_processor/args_node.rb +0 -24
  122. data/lib/solargraph/source_map/node_processor/base.rb +0 -103
  123. data/lib/solargraph/source_map/node_processor/begin_node.rb +0 -13
  124. data/lib/solargraph/source_map/node_processor/block_node.rb +0 -21
  125. data/lib/solargraph/source_map/node_processor/casgn_node.rb +0 -21
  126. data/lib/solargraph/source_map/node_processor/cvasgn_node.rb +0 -21
  127. data/lib/solargraph/source_map/node_processor/def_node.rb +0 -62
  128. data/lib/solargraph/source_map/node_processor/defs_node.rb +0 -33
  129. data/lib/solargraph/source_map/node_processor/gvasgn_node.rb +0 -21
  130. data/lib/solargraph/source_map/node_processor/ivasgn_node.rb +0 -34
  131. data/lib/solargraph/source_map/node_processor/lvasgn_node.rb +0 -24
  132. data/lib/solargraph/source_map/node_processor/namespace_node.rb +0 -35
  133. data/lib/solargraph/source_map/node_processor/orasgn_node.rb +0 -14
  134. data/lib/solargraph/source_map/node_processor/resbody_node.rb +0 -32
  135. data/lib/solargraph/source_map/node_processor/sclass_node.rb +0 -19
  136. data/lib/solargraph/source_map/node_processor/send_node.rb +0 -217
  137. data/lib/solargraph/source_map/node_processor/sym_node.rb +0 -16
@@ -4,379 +4,471 @@ module Solargraph
4
4
  # A static analysis tool for validating data types.
5
5
  #
6
6
  class TypeChecker
7
- autoload :Problem, 'solargraph/type_checker/problem'
7
+ autoload :Problem, 'solargraph/type_checker/problem'
8
8
  autoload :ParamDef, 'solargraph/type_checker/param_def'
9
+ autoload :Rules, 'solargraph/type_checker/rules'
10
+ autoload :Checks, 'solargraph/type_checker/checks'
11
+
12
+ include Checks
13
+ include Parser::NodeMethods
9
14
 
10
15
  # @return [String]
11
16
  attr_reader :filename
12
17
 
18
+ # @return [Rules]
19
+ attr_reader :rules
20
+
21
+ # @return [ApiMap]
22
+ attr_reader :api_map
23
+
13
24
  # @param filename [String]
14
25
  # @param api_map [ApiMap]
15
- def initialize filename, api_map: nil
26
+ # @param level [Symbol]
27
+ def initialize filename, api_map: nil, level: :normal
16
28
  @filename = filename
17
29
  # @todo Smarter directory resolution
18
30
  @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename))
31
+ @rules = Rules.new(level)
32
+ @marked_ranges = []
19
33
  end
20
34
 
21
- # Return type problems indicate that a method does not specify a type in a
22
- # `@return` tag or the specified type could not be resolved to a known
23
- # type.
24
- #
25
- # @return [Array<Problem>]
26
- def return_type_problems
27
- result = []
28
- smap = api_map.source_map(filename)
29
- pins = smap.pins.select { |pin| pin.is_a?(Solargraph::Pin::BaseMethod) }
30
- pins.each { |pin| result.concat check_return_type(pin) }
31
- result
35
+ # @return [SourceMap]
36
+ def source_map
37
+ @source_map ||= api_map.source_map(filename)
32
38
  end
33
39
 
34
- # Param type problems indicate that a method does not specify a type in a
35
- # `@param` tag for one or more of its parameters, a `@param` tag is defined
36
- # that does not correlate with the method signature, or the specified type
37
- # could not be resolved to a known type.
38
- #
39
40
  # @return [Array<Problem>]
40
- def param_type_problems
41
- result = []
42
- smap = api_map.source_map(filename)
43
- smap.pins.select { |pin| pin.is_a?(Pin::Method) }.each do |pin|
44
- if pin.parameters.empty?
45
- pin.docstring.tags(:param).each do |tag|
46
- result.push Problem.new(pin.location, "#{pin.name} has unknown @param #{tag.name}", pin: pin)
47
- end
48
- end
41
+ def problems
42
+ @problems ||= begin
43
+ method_tag_problems
44
+ .concat variable_type_tag_problems
45
+ .concat const_problems
46
+ .concat call_problems
49
47
  end
50
- smap.locals.select { |pin| pin.is_a?(Solargraph::Pin::Parameter) }.each do |par|
51
- next unless par.closure.is_a?(Solargraph::Pin::Method)
52
- result.concat check_param_tags(par.closure)
53
- type = par.typify(api_map)
54
- pdefs = ParamDef.from(par.closure)
55
- if type.undefined?
56
- if par.return_type.undefined? && !pdefs.any? { |pd| pd.name == par.name && [:restarg, :kwrestarg, :blockarg].include?(pd.type) }
57
- result.push Problem.new(par.location, "#{par.closure.name} has undefined @param type for #{par.name}")
58
- elsif !pdefs.any? { |pd| [:restarg, :kwrestarg, :blockarg].include?(pd.type) }
59
- result.push Problem.new(par.location, "#{par.closure.name} has unresolved @param type for #{par.name}")
60
- end
61
- end
48
+ end
49
+
50
+ class << self
51
+ # @param filename [String]
52
+ # @return [self]
53
+ def load filename, level = :normal
54
+ source = Solargraph::Source.load(filename)
55
+ api_map = Solargraph::ApiMap.new
56
+ api_map.map(source)
57
+ new(filename, api_map: api_map, level: level)
58
+ end
59
+
60
+ # @param code [String]
61
+ # @param filename [String, nil]
62
+ # @return [self]
63
+ def load_string code, filename = nil, level = :normal
64
+ source = Solargraph::Source.load_string(code, filename)
65
+ api_map = Solargraph::ApiMap.new
66
+ api_map.map(source)
67
+ new(filename, api_map: api_map, level: level)
62
68
  end
63
- result
64
69
  end
65
70
 
66
- # Strict type problems indicate that a `@return` type or a `@param` type
67
- # does not match the type inferred from code analysis; or that an argument
68
- # sent to a method does not match the type specified in the corresponding
69
- # `@param` tag.
70
- #
71
+ private
72
+
71
73
  # @return [Array<Problem>]
72
- def strict_type_problems
74
+ def method_tag_problems
73
75
  result = []
74
- smap = api_map.source_map(filename)
75
- smap.pins.select { |pin| pin.is_a?(Pin::BaseMethod) }.each do |pin|
76
- result.concat confirm_return_type(pin)
76
+ # @param pin [Pin::BaseMethod]
77
+ source_map.pins.select { |pin| pin.is_a?(Pin::BaseMethod) }.each do |pin|
78
+ result.concat method_return_type_problems_for(pin)
79
+ result.concat method_param_type_problems_for(pin)
77
80
  end
78
- return result if smap.source.node.nil?
79
- result.concat check_send_args smap.source.node
80
81
  result
81
82
  end
82
83
 
83
- private
84
-
85
- # @return [ApiMap]
86
- attr_reader :api_map
87
-
88
84
  # @param pin [Pin::BaseMethod]
89
85
  # @return [Array<Problem>]
90
- def check_param_tags pin
91
- return [] if ParamDef.from(pin).map(&:type).include?(:kwrestarg)
86
+ def method_return_type_problems_for pin
92
87
  result = []
93
- pin.docstring.tags(:param).each do |par|
94
- next if pin.parameter_names.include?(par.name)
95
- result.push Problem.new(pin.location, "#{pin.name} has unknown @param #{par.name}")
88
+ declared = pin.typify(api_map).self_to(pin.full_context.namespace)
89
+ if declared.undefined?
90
+ if pin.return_type.undefined? && rules.require_type_tags?
91
+ result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", pin: pin)
92
+ elsif pin.return_type.defined?
93
+ result.push Problem.new(pin.location, "Unresolved return type #{pin.return_type} for #{pin.path}", pin: pin)
94
+ elsif rules.must_tag_or_infer? && pin.probe(api_map).undefined?
95
+ result.push Problem.new(pin.location, "Untyped method #{pin.path} could not be inferred")
96
+ end
97
+ elsif rules.validate_tags?
98
+ unless pin.node.nil? || declared.void? || macro_pin?(pin) || abstract?(pin)
99
+ inferred = pin.probe(api_map).self_to(pin.full_context.namespace)
100
+ if inferred.undefined?
101
+ unless rules.ignore_all_undefined? || external?(pin)
102
+ result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin)
103
+ end
104
+ else
105
+ unless (rules.rank > 1 ? types_match?(api_map, declared, inferred) : any_types_match?(api_map, declared, inferred))
106
+ result.push Problem.new(pin.location, "Declared return type #{declared} does not match inferred type #{inferred} for #{pin.path}", pin: pin)
107
+ end
108
+ end
109
+ end
96
110
  end
97
111
  result
98
112
  end
99
113
 
114
+ def macro_pin? pin
115
+ pin.location && source_map.source.comment_at?(pin.location.range.ending)
116
+ end
117
+
100
118
  # @param pin [Pin::BaseMethod]
101
119
  # @return [Array<Problem>]
102
- def check_return_type pin
103
- tagged = pin.typify(api_map)
104
- if tagged.undefined?
105
- if pin.return_type.undefined?
106
- probed = pin.probe(api_map)
107
- return [Problem.new(pin.location, "#{pin.name} has undefined @return type", pin: pin, suggestion: probed.to_s)]
108
- else
109
- return [Problem.new(pin.location, "#{pin.name} has unresolved @return type #{pin.return_type}")]
120
+ def method_param_type_problems_for pin
121
+ stack = api_map.get_method_stack(pin.namespace, pin.name, scope: pin.scope)
122
+ params = first_param_hash(stack)
123
+ result = []
124
+ if rules.require_type_tags?
125
+ pin.parameters.each do |par|
126
+ break if par.decl == :restarg || par.decl == :kwrestarg || par.decl == :blockarg
127
+ unless params[par.name]
128
+ result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin)
129
+ end
110
130
  end
111
131
  end
112
- []
132
+ params.each_pair do |name, tag|
133
+ type = tag.qualify(api_map, pin.full_context.namespace)
134
+ if type.undefined?
135
+ result.push Problem.new(pin.location, "Unresolved type #{tag} for #{name} param on #{pin.path}", pin: pin)
136
+ end
137
+ end
138
+ result
139
+ end
140
+
141
+ def ignored_pins
142
+ @ignored_pins ||= []
113
143
  end
114
144
 
115
- # @param pin [Solargraph::Pin::Base]
116
145
  # @return [Array<Problem>]
117
- def confirm_return_type pin
118
- tagged = pin.typify(api_map).self_to(pin.namespace)
119
- return [] if tagged.void? || tagged.undefined? || pin.is_a?(Pin::Attribute)
120
- probed = pin.probe(api_map)
121
- return [] if probed.undefined?
122
- if tagged.to_s != probed.to_s
123
- if probed.name == 'Array' && probed.subtypes.empty?
124
- return [] if tagged.name == 'Array'
125
- end
126
- if probed.name == 'Hash' && probed.value_types.empty?
127
- return [] if tagged.name == 'Hash'
128
- end
129
- all = true
130
- probed.each do |pt|
131
- tagged.each do |tt|
132
- if pt.name == tt.name && !api_map.super_and_sub?(tt.namespace, pt.namespace) && !tagged.map(&:namespace).include?(pt.namespace)
133
- all = false
134
- break
135
- elsif pt.name == tt.name && ['Array', 'Class', 'Module'].include?(pt.name)
136
- if !(tt.subtypes.any? { |ttx| pt.subtypes.any? { |ptx| api_map.super_and_sub?(ttx.to_s, ptx.to_s) } })
137
- all = false
138
- break
139
- end
140
- elsif pt.name == tt.name && pt.name == 'Hash'
141
- if !(tt.key_types.empty? && !pt.key_types.empty?) && !(tt.key_types.any? { |ttx| pt.key_types.any? { |ptx| api_map.super_and_sub?(ttx.to_s, ptx.to_s) } })
142
- if !(tt.value_types.empty? && !pt.value_types.empty?) && !(tt.value_types.any? { |ttx| pt.value_types.any? { |ptx| api_map.super_and_sub?(ttx.to_s, ptx.to_s) } })
143
- all = false
144
- break
146
+ def variable_type_tag_problems
147
+ result = []
148
+ all_variables.each do |pin|
149
+ if pin.return_type.defined?
150
+ # @todo Somwhere in here we still need to determine if the variable is defined by an external call
151
+ declared = pin.typify(api_map)
152
+ if declared.defined?
153
+ if rules.validate_tags?
154
+ inferred = pin.probe(api_map)
155
+ if inferred.undefined?
156
+ next if rules.ignore_all_undefined?
157
+ # next unless internal?(pin) # @todo This might be redundant for variables
158
+ if declared_externally?(pin)
159
+ ignored_pins.push pin
160
+ else
161
+ result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin)
162
+ end
163
+ else
164
+ unless (rules.rank > 1 ? types_match?(api_map, declared, inferred) : any_types_match?(api_map, declared, inferred))
165
+ result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin)
145
166
  end
146
167
  end
147
- elsif pt.name != tt.name && !api_map.super_and_sub?(tt.to_s, pt.to_s) && !tagged.map(&:to_s).include?(pt.to_s)
148
- all = false
149
- break
168
+ elsif declared_externally?(pin)
169
+ ignored_pins.push pin
150
170
  end
171
+ elsif !pin.is_a?(Pin::Parameter)
172
+ result.push Problem.new(pin.location, "Unresolved type #{pin.return_type} for variable #{pin.name}", pin: pin)
173
+ end
174
+ else
175
+ # @todo Check if the variable is defined by an external call
176
+ inferred = pin.probe(api_map)
177
+ if inferred.undefined? && declared_externally?(pin)
178
+ ignored_pins.push pin
151
179
  end
152
180
  end
153
- return [] if all
154
- return [Problem.new(pin.location, "@return type `#{tagged.to_s}` does not match inferred type `#{probed.to_s}`", pin: pin, suggestion: probed.to_s)]
155
181
  end
156
- []
182
+ result
157
183
  end
158
184
 
159
- # @param node [Parser::AST::Node]
160
- # @param skip_send [Boolean]
161
- # @return [Array<Problem>]
162
- def check_send_args node, skip_send = false
185
+ # @return [Array<Pin::BaseVariable>]
186
+ def all_variables
187
+ source_map.pins.select { |pin| pin.is_a?(Pin::BaseVariable) } +
188
+ source_map.locals.select { |pin| pin.is_a?(Pin::LocalVariable) }
189
+ end
190
+
191
+ def const_problems
192
+ return [] unless rules.validate_consts?
163
193
  result = []
164
- if node.type == :send
165
- smap = api_map.source_map(filename)
166
- locals = smap.locals_at(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)))
167
- block = smap.locate_block_pin(node.loc.line, node.loc.column)
168
- chain = Solargraph::Source::NodeChainer.chain(node, filename)
169
- pins = chain.define(api_map, block, locals).select { |pin| pin.is_a?(Pin::BaseMethod ) }
194
+ Solargraph::Parser::NodeMethods.const_nodes_from(source_map.source.node).each do |const|
195
+ rng = Solargraph::Range.from_node(const)
196
+ chain = Solargraph::Parser.chain(const, filename)
197
+ block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column)
198
+ location = Location.new(filename, rng)
199
+ locals = source_map.locals_at(location)
200
+ pins = chain.define(api_map, block_pin, locals)
170
201
  if pins.empty?
171
- if !more_signature?(node)
172
- base = chain.base.define(api_map, block, locals).first
173
- if base.nil? || report_location?(base.location)
174
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Unresolved method signature #{chain.links.map(&:word).join('.')}")
202
+ result.push Problem.new(location, "Unresolved constant #{Solargraph::Parser::NodeMethods.unpack_name(const)}")
203
+ @marked_ranges.push location.range
204
+ end
205
+ end
206
+ result
207
+ end
208
+
209
+ def call_problems
210
+ result = []
211
+ Solargraph::Parser::NodeMethods.call_nodes_from(source_map.source.node).each do |call|
212
+ rng = Solargraph::Range.from_node(call)
213
+ next if @marked_ranges.any? { |d| d.contain?(rng.start) }
214
+ chain = Solargraph::Parser.chain(call, filename)
215
+ block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column)
216
+ location = Location.new(filename, rng)
217
+ locals = source_map.locals_at(location)
218
+ type = chain.infer(api_map, block_pin, locals)
219
+ if type.undefined? && !rules.ignore_all_undefined?
220
+ base = chain
221
+ missing = chain
222
+ found = nil
223
+ closest = ComplexType::UNDEFINED
224
+ until base.links.first.undefined?
225
+ found = base.define(api_map, block_pin, locals).first
226
+ break if found
227
+ missing = base
228
+ base = base.base
229
+ end
230
+ closest = found.typify(api_map) if found
231
+ if !found || closest.defined? || internal?(found)
232
+ unless ignored_pins.include?(found)
233
+ result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
234
+ @marked_ranges.push rng
175
235
  end
176
236
  end
177
- else
237
+ end
238
+ result.concat argument_problems_for(chain, api_map, block_pin, locals, location)
239
+ end
240
+ result
241
+ end
242
+
243
+ def argument_problems_for chain, api_map, block_pin, locals, location
244
+ result = []
245
+ base = chain
246
+ until base.links.length == 1 && base.undefined?
247
+ pins = base.define(api_map, block_pin, locals)
248
+ if pins.first.is_a?(Pin::BaseMethod)
249
+ # @type [Pin::BaseMethod]
178
250
  pin = pins.first
179
- ptypes = ParamDef.from(pin)
180
- params = first_param_tags_from(pins)
181
- cursor = 0
182
- curtype = nil
183
- # The @param_tuple tag marks exceptional cases for handling, e.g., the Hash#[]= method.
184
- if pin.docstring.tag(:param_tuple)
185
- if node.children[2..-1].length > 2
186
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong number of arguments to #{pin.path}")
187
- else
188
- base = chain.base.infer(api_map, block, locals)
189
- # @todo Don't just use the first key/value type
190
- k = base.key_types.first || ComplexType.parse('Object')
191
- v = base.value_types.first || ComplexType.parse('Object')
192
- tuple = [
193
- ParamDef.new('key', k),
194
- ParamDef.new('value', v)
195
- ]
196
- node.children[2..-1].each_with_index do |arg, index|
197
- chain = Solargraph::Source::NodeChainer.chain(arg, filename)
198
- argtype = chain.infer(api_map, block, locals)
199
- partype = tuple[index].type
200
- if argtype.tag != partype.tag && !api_map.super_and_sub?(partype.tag.to_s, argtype.tag.to_s)
201
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: #{tuple[index].name} expected #{partype.tag}, received #{argtype.tag}")
202
- end
203
- end
204
- end
205
- return result
251
+ ap = arity_problems_for(pin, base.links.last.arguments, location)
252
+ unless ap.empty?
253
+ result.concat ap
254
+ break
206
255
  end
207
- node.children[2..-1].each_with_index do |arg, index|
208
- if pin.is_a?(Pin::Attribute)
209
- curtype = ParamDef.new('value', :arg)
210
- else
211
- curtype = ptypes[cursor] if curtype.nil? || curtype == :arg
256
+ break unless rules.validate_calls?
257
+ params = first_param_hash(pins)
258
+ pin.parameters.each_with_index do |par, idx|
259
+ argchain = base.links.last.arguments[idx]
260
+ if argchain.nil? && par.decl == :arg
261
+ result.push Problem.new(location, "Not enough arguments to #{pin.path}")
262
+ break
212
263
  end
213
- if curtype.nil?
214
- if pin.parameters[index].nil?
215
- if params.values[index]
216
- # Allow for methods that have named parameters but no
217
- # arguments in their definitions. This is common in the Ruby
218
- # core, e.g., the Hash#[]= method.
219
- chain = Solargraph::Source::NodeChainer.chain(arg, filename)
220
- argtype = chain.infer(api_map, block, locals)
221
- partype = params.values[index]
222
- if argtype.tag != partype.tag && !api_map.super_and_sub?(partype.tag.to_s, argtype.tag.to_s)
223
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: #{params.keys[index]} expected #{partype.tag}, received #{argtype.tag}")
224
- end
225
- else
226
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Not enough arguments sent to #{pin.path}")
227
- break
228
- end
229
- end
230
- else
231
- # @todo This should also detect when the last parameter is a hash
232
- if curtype.type == :kwrestarg
233
- if arg.type != :hash
234
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: expected hash or keyword")
235
- else
236
- result.concat check_hash_params arg, params
237
- end
238
- # @todo Break here? Not sure about that
264
+ if argchain
265
+ if par.decl != :arg
266
+ result.concat kwarg_problems_for argchain, api_map, block_pin, locals, location, pin, params, idx
239
267
  break
240
- end
241
- break if curtype.type == :restarg
242
- if arg.is_a?(Parser::AST::Node) && arg.type == :hash
243
- arg.children.each do |pair|
244
- sym = pair.children[0].children[0].to_s
245
- partype = params[sym]
246
- if partype
247
- chain = Solargraph::Source::NodeChainer.chain(pair.children[1], filename)
248
- argtype = chain.infer(api_map, block, locals)
249
- if argtype.tag != partype.tag && !api_map.super_and_sub?(partype.tag.to_s, argtype.tag.to_s)
250
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: #{pin.parameter_names[index]} expected #{partype.tag}, received #{argtype.tag}")
251
- end
252
- end
253
- end
254
- elsif arg.is_a?(Parser::AST::Node) && arg.type == :splat
255
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Can't handle splat in #{pin.parameter_names[index]} #{pin.path}")
256
- break if curtype != :arg && ptypes.map(&:type).include?(:restarg)
257
268
  else
258
- if pin.is_a?(Pin::Attribute)
259
- partype = pin.return_type
269
+ ptype = params[par.name]
270
+ if ptype.nil?
271
+ # @todo Some level (strong, I guess) should require the param here
260
272
  else
261
- partype = params[pin.parameter_names[index]]
262
- end
263
- if partype
264
- arg = chain.links.last.arguments[index]
265
- if arg.nil?
266
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong number of arguments to #{pin.path}")
267
- else
268
- argtype = arg.infer(api_map, block, locals)
269
- if !arg_to_duck(argtype, partype)
270
- match = false
271
- partype.each do |pt|
272
- if argtype.tag == pt.tag || api_map.super_and_sub?(pt.tag.to_s, argtype.tag.to_s)
273
- match = true
274
- break
275
- end
276
- end
277
- unless match
278
- result.push Problem.new(Solargraph::Location.new(filename, Solargraph::Range.from_node(node)), "Wrong parameter type for #{pin.path}: #{pin.parameter_names[index]} expected [#{partype}], received [#{argtype.tag}]")
279
- end
280
- end
273
+ argtype = argchain.infer(api_map, block_pin, locals)
274
+ if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
275
+ result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
281
276
  end
282
277
  end
283
278
  end
279
+ elsif par.rest?
280
+ next
281
+ elsif par.decl == :kwarg
282
+ result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
283
+ break
284
284
  end
285
- cursor += 1 if curtype == :arg
286
285
  end
287
286
  end
288
- end
289
- node.children.each do |child|
290
- next unless child.is_a?(Parser::AST::Node)
291
- next if child.type == :send && skip_send
292
- result.concat check_send_args(child)
287
+ base = base.base
293
288
  end
294
289
  result
295
290
  end
296
291
 
297
- def check_hash_params arg, params
292
+ def kwarg_problems_for argchain, api_map, block_pin, locals, location, pin, params, first
298
293
  result = []
299
- keys = arg.children.map do |child|
300
- child.children[0].children[0].to_s
301
- end
302
- keys.each do |key|
303
- param = params[key]
304
- if param
305
- # @todo typecheck
294
+ kwargs = convert_hash(argchain.node)
295
+ pin.parameters[first..-1].each_with_index do |par, cur|
296
+ idx = first + cur
297
+ argchain = kwargs[par.name.to_sym]
298
+ if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}')
299
+ result.concat kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kwargs)
306
300
  else
307
- # @todo This error might not be valid. If there's a splat in the
308
- # method parameters, should the type checker let it pass?
309
- result.push Problem.new(nil, "Keyword argument #{key} does not have a @param tag")
301
+ if argchain
302
+ ptype = params[par.name]
303
+ if ptype.nil?
304
+ # @todo Some level (strong, I guess) should require the param here
305
+ else
306
+ argtype = argchain.infer(api_map, block_pin, locals)
307
+ if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
308
+ result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
309
+ end
310
+ end
311
+ else
312
+ if par.decl == :kwarg
313
+ # @todo Problem: missing required keyword argument
314
+ result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
315
+ end
316
+ end
310
317
  end
311
318
  end
312
319
  result
313
320
  end
314
321
 
315
- def arg_to_duck arg, par
316
- return false unless par.duck_type?
317
- meths = api_map.get_complex_type_methods(arg).map(&:name)
318
- par.each do |quack|
319
- return true if quack.duck_type? && meths.include?(quack.to_s[1..-1])
322
+ def kwrestarg_problems_for(api_map, block_pin, locals, location, pin, params, kwargs)
323
+ result = []
324
+ kwargs.each_pair do |pname, argchain|
325
+ ptype = params[pname.to_s]
326
+ if ptype.nil?
327
+ # Probably nothing to do here. All of these args should be optional.
328
+ else
329
+ argtype = argchain.infer(api_map, block_pin, locals)
330
+ if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
331
+ result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}")
332
+ end
333
+ end
320
334
  end
321
- false
335
+ result
322
336
  end
323
337
 
324
- # @param pin [Pin::Base]
325
- # @return [Hash]
326
- def param_tags_from pin
327
- # @todo Look for see references
328
- # and dig through all the pins
329
- return {} if pin.nil?
338
+ def param_hash(pin)
330
339
  tags = pin.docstring.tags(:param)
340
+ return {} if tags.empty?
331
341
  result = {}
332
342
  tags.each do |tag|
333
- result[tag.name] = ComplexType::UNDEFINED
334
- result[tag.name] = ComplexType.try_parse(*tag.types).qualify(api_map, pin.context.namespace)
343
+ next if tag.types.nil? || tag.types.empty?
344
+ result[tag.name.to_s] = Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, pin.full_context.namespace)
335
345
  end
336
346
  result
337
347
  end
338
348
 
339
- def first_param_tags_from pins
349
+ # @param [Array<Pin::Method>]
350
+ def first_param_hash(pins)
340
351
  pins.each do |pin|
341
- result = param_tags_from(pin)
352
+ result = param_hash(pin)
342
353
  return result unless result.empty?
343
354
  end
344
355
  {}
345
356
  end
346
357
 
347
- # @param location [Location, nil]
348
- def report_location? location
349
- return false if location.nil?
350
- filename == location.filename || api_map.bundled?(location.filename)
358
+ # @param pin [Pin::Base]
359
+ def internal? pin
360
+ pin.location && api_map.bundled?(pin.location.filename)
351
361
  end
352
362
 
353
- def more_signature? node
354
- node.children.any? do |child|
355
- child.is_a?(Parser::AST::Node) && (
356
- child.type == :send || (child.type == :block && more_signature?(child))
357
- )
363
+ # @param pin [Pin::Base]
364
+ def external? pin
365
+ !internal? pin
366
+ end
367
+
368
+ def declared_externally? pin
369
+ return true if pin.assignment.nil?
370
+ chain = Solargraph::Parser.chain(pin.assignment, filename)
371
+ rng = Solargraph::Range.from_node(pin.assignment)
372
+ block_pin = source_map.locate_block_pin(rng.start.line, rng.start.column)
373
+ location = Location.new(filename, Range.from_node(pin.assignment))
374
+ locals = source_map.locals_at(location)
375
+ type = chain.infer(api_map, block_pin, locals)
376
+ if type.undefined? && !rules.ignore_all_undefined?
377
+ base = chain
378
+ missing = chain
379
+ found = nil
380
+ closest = ComplexType::UNDEFINED
381
+ until base.links.first.undefined?
382
+ found = base.define(api_map, block_pin, locals).first
383
+ break if found
384
+ missing = base
385
+ base = base.base
386
+ end
387
+ closest = found.typify(api_map) if found
388
+ if !found || closest.defined? || internal?(found)
389
+ return false
390
+ end
358
391
  end
392
+ true
359
393
  end
360
394
 
361
- class << self
362
- # @param filename [String]
363
- # @return [self]
364
- def load filename
365
- source = Solargraph::Source.load(filename)
366
- api_map = Solargraph::ApiMap.new
367
- api_map.map(source)
368
- new(filename, api_map: api_map)
395
+ # @param pin [Pin::BaseMethod]
396
+ def arity_problems_for(pin, arguments, location)
397
+ return [] unless pin.explicit?
398
+ return [] if pin.parameters.empty? && arguments.empty?
399
+ if pin.parameters.empty?
400
+ # Functions tagged param_tuple accepts two arguments (e.g., Hash#[]=)
401
+ return [] if pin.docstring.tag(:param_tuple) && arguments.length == 2
402
+ return [] if arguments.length == 1 && arguments.last.links.last.is_a?(Source::Chain::BlockVariable)
403
+ return [Problem.new(location, "Too many arguments to #{pin.path}")]
404
+ end
405
+ unchecked = arguments.clone
406
+ add_params = 0
407
+ if unchecked.empty? && pin.parameters.any? { |param| param.decl == :kwarg }
408
+ return [Problem.new(location, "Missing keyword arguments to #{pin.path}")]
409
+ end
410
+ unless unchecked.empty?
411
+ kwargs = convert_hash(unchecked.last.node)
412
+ # unless kwargs.empty?
413
+ if pin.parameters.any? { |param| [:kwarg, :kwoptarg].include?(param.decl) || param.kwrestarg? }
414
+ if kwargs.empty?
415
+ add_params += 1
416
+ else
417
+ unchecked.pop
418
+ pin.parameters.each do |param|
419
+ next unless param.keyword?
420
+ if kwargs.key?(param.name.to_sym)
421
+ kwargs.delete param.name.to_sym
422
+ elsif param.decl == :kwarg
423
+ return [Problem.new(location, "Missing keyword argument #{param.name} to #{pin.path}")]
424
+ end
425
+ end
426
+ kwargs.clear if pin.parameters.any?(&:kwrestarg?)
427
+ unless kwargs.empty?
428
+ return [Problem.new(location, "Unrecognized keyword argument #{kwargs.keys.first} to #{pin.path}")]
429
+ end
430
+ end
431
+ end
432
+ # end
369
433
  end
434
+ req = required_param_count(pin)
435
+ if req + add_params < unchecked.length
436
+ return [] if pin.parameters.any?(&:rest?)
437
+ opt = optional_param_count(pin)
438
+ return [] if unchecked.length <= req + opt
439
+ if unchecked.length == req + opt + 1 && unchecked.last.links.last.is_a?(Source::Chain::BlockVariable)
440
+ return []
441
+ end
442
+ return [Problem.new(location, "Too many arguments to #{pin.path}")]
443
+ elsif unchecked.length < req && (arguments.empty? || !arguments.last.splat?)
444
+ return [Problem.new(location, "Not enough arguments to #{pin.path}")]
445
+ end
446
+ []
447
+ end
370
448
 
371
- # @param code [String]
372
- # @param filename [String, nil]
373
- # @return [self]
374
- def load_string code, filename = nil
375
- source = Solargraph::Source.load_string(code, filename)
376
- api_map = Solargraph::ApiMap.new
377
- api_map.map(source)
378
- new(filename, api_map: api_map)
449
+ # @param pin [Pin::BaseMethod]
450
+ def required_param_count(pin)
451
+ count = 0
452
+ pin.parameters.each do |param|
453
+ break unless param.decl == :arg
454
+ count += 1
379
455
  end
456
+ count
457
+ end
458
+
459
+ # @param pin [Pin::BaseMethod]
460
+ def optional_param_count(pin)
461
+ count = 0
462
+ pin.parameters.each do |param|
463
+ next unless param.decl == :optarg
464
+ count += 1
465
+ end
466
+ count
467
+ end
468
+
469
+ def abstract? pin
470
+ pin.docstring.has_tag?(:abstract) ||
471
+ (pin.closure && pin.closure.docstring.has_tag?(:abstract))
380
472
  end
381
473
  end
382
474
  end