solargraph 0.58.0 → 0.59.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.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +3 -0
  3. data/.gitattributes +2 -0
  4. data/.github/workflows/linting.yml +6 -5
  5. data/.github/workflows/plugins.yml +48 -35
  6. data/.github/workflows/rspec.yml +15 -28
  7. data/.github/workflows/typecheck.yml +3 -2
  8. data/.gitignore +1 -0
  9. data/.rubocop.yml +38 -6
  10. data/.rubocop_todo.yml +53 -966
  11. data/CHANGELOG.md +24 -0
  12. data/Gemfile +3 -1
  13. data/README.md +3 -3
  14. data/Rakefile +26 -23
  15. data/bin/solargraph +9 -8
  16. data/lib/solargraph/api_map/cache.rb +110 -110
  17. data/lib/solargraph/api_map/constants.rb +288 -279
  18. data/lib/solargraph/api_map/index.rb +204 -193
  19. data/lib/solargraph/api_map/source_to_yard.rb +110 -97
  20. data/lib/solargraph/api_map/store.rb +395 -384
  21. data/lib/solargraph/api_map.rb +1029 -945
  22. data/lib/solargraph/bench.rb +44 -45
  23. data/lib/solargraph/complex_type/conformance.rb +176 -0
  24. data/lib/solargraph/complex_type/type_methods.rb +240 -228
  25. data/lib/solargraph/complex_type/unique_type.rb +641 -482
  26. data/lib/solargraph/complex_type.rb +557 -444
  27. data/lib/solargraph/convention/data_definition/data_assignment_node.rb +61 -61
  28. data/lib/solargraph/convention/data_definition/data_definition_node.rb +93 -91
  29. data/lib/solargraph/convention/data_definition.rb +108 -105
  30. data/lib/solargraph/convention/gemfile.rb +15 -15
  31. data/lib/solargraph/convention/gemspec.rb +23 -23
  32. data/lib/solargraph/convention/rakefile.rb +17 -17
  33. data/lib/solargraph/convention/struct_definition/struct_assignment_node.rb +62 -61
  34. data/lib/solargraph/convention/struct_definition/struct_definition_node.rb +103 -102
  35. data/lib/solargraph/convention/struct_definition.rb +168 -164
  36. data/lib/solargraph/convention.rb +78 -78
  37. data/lib/solargraph/converters/dd.rb +19 -17
  38. data/lib/solargraph/converters/dl.rb +17 -15
  39. data/lib/solargraph/converters/dt.rb +17 -15
  40. data/lib/solargraph/converters/misc.rb +3 -1
  41. data/lib/solargraph/diagnostics/require_not_found.rb +54 -53
  42. data/lib/solargraph/diagnostics/rubocop.rb +119 -118
  43. data/lib/solargraph/diagnostics/rubocop_helpers.rb +70 -68
  44. data/lib/solargraph/diagnostics/type_check.rb +56 -55
  45. data/lib/solargraph/diagnostics/update_errors.rb +37 -41
  46. data/lib/solargraph/doc_map.rb +438 -439
  47. data/lib/solargraph/equality.rb +34 -34
  48. data/lib/solargraph/gem_pins.rb +98 -98
  49. data/lib/solargraph/language_server/error_codes.rb +20 -20
  50. data/lib/solargraph/language_server/host/diagnoser.rb +89 -89
  51. data/lib/solargraph/language_server/host/dispatch.rb +130 -130
  52. data/lib/solargraph/language_server/host/message_worker.rb +113 -112
  53. data/lib/solargraph/language_server/host/sources.rb +100 -99
  54. data/lib/solargraph/language_server/host.rb +886 -878
  55. data/lib/solargraph/language_server/message/base.rb +97 -97
  56. data/lib/solargraph/language_server/message/client/register_capability.rb +13 -15
  57. data/lib/solargraph/language_server/message/completion_item/resolve.rb +58 -60
  58. data/lib/solargraph/language_server/message/extended/check_gem_version.rb +108 -114
  59. data/lib/solargraph/language_server/message/extended/document.rb +24 -23
  60. data/lib/solargraph/language_server/message/extended/document_gems.rb +32 -32
  61. data/lib/solargraph/language_server/message/extended/download_core.rb +20 -19
  62. data/lib/solargraph/language_server/message/extended/search.rb +20 -20
  63. data/lib/solargraph/language_server/message/initialize.rb +197 -191
  64. data/lib/solargraph/language_server/message/text_document/completion.rb +58 -56
  65. data/lib/solargraph/language_server/message/text_document/definition.rb +49 -40
  66. data/lib/solargraph/language_server/message/text_document/document_highlight.rb +23 -16
  67. data/lib/solargraph/language_server/message/text_document/document_symbol.rb +36 -26
  68. data/lib/solargraph/language_server/message/text_document/formatting.rb +150 -148
  69. data/lib/solargraph/language_server/message/text_document/hover.rb +58 -58
  70. data/lib/solargraph/language_server/message/text_document/prepare_rename.rb +18 -11
  71. data/lib/solargraph/language_server/message/text_document/references.rb +23 -16
  72. data/lib/solargraph/language_server/message/text_document/rename.rb +26 -19
  73. data/lib/solargraph/language_server/message/text_document/signature_help.rb +25 -24
  74. data/lib/solargraph/language_server/message/text_document/type_definition.rb +33 -25
  75. data/lib/solargraph/language_server/message/workspace/did_change_configuration.rb +41 -35
  76. data/lib/solargraph/language_server/message/workspace/did_change_watched_files.rb +48 -40
  77. data/lib/solargraph/language_server/message/workspace/did_change_workspace_folders.rb +32 -26
  78. data/lib/solargraph/language_server/message/workspace/workspace_symbol.rb +33 -23
  79. data/lib/solargraph/language_server/message.rb +94 -94
  80. data/lib/solargraph/language_server/request.rb +29 -27
  81. data/lib/solargraph/language_server/transport/data_reader.rb +72 -74
  82. data/lib/solargraph/language_server/uri_helpers.rb +49 -49
  83. data/lib/solargraph/library.rb +702 -683
  84. data/lib/solargraph/location.rb +85 -82
  85. data/lib/solargraph/logging.rb +55 -37
  86. data/lib/solargraph/page.rb +92 -92
  87. data/lib/solargraph/parser/comment_ripper.rb +84 -69
  88. data/lib/solargraph/parser/flow_sensitive_typing.rb +471 -255
  89. data/lib/solargraph/parser/node_processor/base.rb +122 -92
  90. data/lib/solargraph/parser/node_processor.rb +63 -62
  91. data/lib/solargraph/parser/parser_gem/class_methods.rb +165 -149
  92. data/lib/solargraph/parser/parser_gem/flawed_builder.rb +19 -19
  93. data/lib/solargraph/parser/parser_gem/node_chainer.rb +191 -166
  94. data/lib/solargraph/parser/parser_gem/node_methods.rb +507 -486
  95. data/lib/solargraph/parser/parser_gem/node_processors/and_node.rb +22 -22
  96. data/lib/solargraph/parser/parser_gem/node_processors/args_node.rb +61 -59
  97. data/lib/solargraph/parser/parser_gem/node_processors/begin_node.rb +24 -15
  98. data/lib/solargraph/parser/parser_gem/node_processors/block_node.rb +45 -46
  99. data/lib/solargraph/parser/parser_gem/node_processors/def_node.rb +60 -53
  100. data/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb +38 -37
  101. data/lib/solargraph/parser/parser_gem/node_processors/if_node.rb +53 -23
  102. data/lib/solargraph/parser/parser_gem/node_processors/ivasgn_node.rb +42 -40
  103. data/lib/solargraph/parser/parser_gem/node_processors/lvasgn_node.rb +30 -29
  104. data/lib/solargraph/parser/parser_gem/node_processors/masgn_node.rb +61 -59
  105. data/lib/solargraph/parser/parser_gem/node_processors/opasgn_node.rb +98 -98
  106. data/lib/solargraph/parser/parser_gem/node_processors/or_node.rb +22 -0
  107. data/lib/solargraph/parser/parser_gem/node_processors/orasgn_node.rb +17 -17
  108. data/lib/solargraph/parser/parser_gem/node_processors/resbody_node.rb +39 -38
  109. data/lib/solargraph/parser/parser_gem/node_processors/sclass_node.rb +51 -52
  110. data/lib/solargraph/parser/parser_gem/node_processors/send_node.rb +302 -291
  111. data/lib/solargraph/parser/parser_gem/node_processors/until_node.rb +29 -29
  112. data/lib/solargraph/parser/parser_gem/node_processors/when_node.rb +23 -0
  113. data/lib/solargraph/parser/parser_gem/node_processors/while_node.rb +33 -29
  114. data/lib/solargraph/parser/parser_gem/node_processors.rb +74 -70
  115. data/lib/solargraph/parser/parser_gem.rb +14 -12
  116. data/lib/solargraph/parser/region.rb +75 -69
  117. data/lib/solargraph/parser/snippet.rb +19 -17
  118. data/lib/solargraph/parser.rb +25 -23
  119. data/lib/solargraph/pin/base.rb +773 -729
  120. data/lib/solargraph/pin/base_variable.rb +375 -126
  121. data/lib/solargraph/pin/block.rb +127 -104
  122. data/lib/solargraph/pin/breakable.rb +15 -9
  123. data/lib/solargraph/pin/callable.rb +275 -231
  124. data/lib/solargraph/pin/closure.rb +69 -72
  125. data/lib/solargraph/pin/common.rb +97 -79
  126. data/lib/solargraph/pin/compound_statement.rb +55 -0
  127. data/lib/solargraph/pin/constant.rb +43 -45
  128. data/lib/solargraph/pin/conversions.rb +129 -123
  129. data/lib/solargraph/pin/delegated_method.rb +131 -120
  130. data/lib/solargraph/pin/documenting.rb +116 -114
  131. data/lib/solargraph/pin/instance_variable.rb +38 -34
  132. data/lib/solargraph/pin/keyword.rb +16 -20
  133. data/lib/solargraph/pin/local_variable.rb +31 -75
  134. data/lib/solargraph/pin/method.rb +724 -672
  135. data/lib/solargraph/pin/method_alias.rb +42 -34
  136. data/lib/solargraph/pin/namespace.rb +122 -115
  137. data/lib/solargraph/pin/parameter.rb +339 -275
  138. data/lib/solargraph/pin/proxy_type.rb +42 -39
  139. data/lib/solargraph/pin/reference/override.rb +47 -47
  140. data/lib/solargraph/pin/reference/superclass.rb +17 -15
  141. data/lib/solargraph/pin/reference/type_alias.rb +16 -0
  142. data/lib/solargraph/pin/reference.rb +59 -39
  143. data/lib/solargraph/pin/search.rb +62 -61
  144. data/lib/solargraph/pin/signature.rb +64 -61
  145. data/lib/solargraph/pin/symbol.rb +54 -53
  146. data/lib/solargraph/pin/until.rb +16 -18
  147. data/lib/solargraph/pin/while.rb +16 -18
  148. data/lib/solargraph/pin.rb +46 -44
  149. data/lib/solargraph/pin_cache.rb +248 -245
  150. data/lib/solargraph/position.rb +139 -119
  151. data/lib/solargraph/range.rb +113 -112
  152. data/lib/solargraph/rbs_map/conversions.rb +952 -823
  153. data/lib/solargraph/rbs_map/core_fills.rb +91 -84
  154. data/lib/solargraph/rbs_map/core_map.rb +65 -58
  155. data/lib/solargraph/rbs_map/stdlib_map.rb +71 -43
  156. data/lib/solargraph/rbs_map.rb +207 -163
  157. data/lib/solargraph/server_methods.rb +16 -16
  158. data/lib/solargraph/shell.rb +555 -352
  159. data/lib/solargraph/source/chain/array.rb +39 -37
  160. data/lib/solargraph/source/chain/call.rb +377 -337
  161. data/lib/solargraph/source/chain/class_variable.rb +13 -13
  162. data/lib/solargraph/source/chain/constant.rb +30 -26
  163. data/lib/solargraph/source/chain/global_variable.rb +13 -13
  164. data/lib/solargraph/source/chain/hash.rb +37 -34
  165. data/lib/solargraph/source/chain/if.rb +30 -28
  166. data/lib/solargraph/source/chain/instance_variable.rb +36 -13
  167. data/lib/solargraph/source/chain/link.rb +99 -109
  168. data/lib/solargraph/source/chain/literal.rb +51 -48
  169. data/lib/solargraph/source/chain/or.rb +29 -23
  170. data/lib/solargraph/source/chain/q_call.rb +13 -11
  171. data/lib/solargraph/source/chain/variable.rb +15 -13
  172. data/lib/solargraph/source/chain/z_super.rb +28 -30
  173. data/lib/solargraph/source/chain.rb +304 -291
  174. data/lib/solargraph/source/change.rb +89 -82
  175. data/lib/solargraph/source/cursor.rb +172 -166
  176. data/lib/solargraph/source/encoding_fixes.rb +22 -23
  177. data/lib/solargraph/source/source_chainer.rb +218 -194
  178. data/lib/solargraph/source/updater.rb +59 -55
  179. data/lib/solargraph/source.rb +522 -498
  180. data/lib/solargraph/source_map/clip.rb +245 -226
  181. data/lib/solargraph/source_map/data.rb +37 -34
  182. data/lib/solargraph/source_map/mapper.rb +286 -259
  183. data/lib/solargraph/source_map.rb +224 -212
  184. data/lib/solargraph/type_checker/problem.rb +34 -32
  185. data/lib/solargraph/type_checker/rules.rb +157 -84
  186. data/lib/solargraph/type_checker.rb +888 -814
  187. data/lib/solargraph/version.rb +5 -5
  188. data/lib/solargraph/workspace/config.rb +258 -255
  189. data/lib/solargraph/workspace/gemspecs.rb +367 -0
  190. data/lib/solargraph/workspace/require_paths.rb +98 -97
  191. data/lib/solargraph/workspace.rb +242 -220
  192. data/lib/solargraph/yard_map/helpers.rb +49 -44
  193. data/lib/solargraph/yard_map/mapper/to_method.rb +136 -130
  194. data/lib/solargraph/yard_map/mapper/to_namespace.rb +32 -31
  195. data/lib/solargraph/yard_map/mapper.rb +84 -79
  196. data/lib/solargraph/yard_map.rb +17 -18
  197. data/lib/solargraph/yard_tags.rb +20 -20
  198. data/lib/solargraph/yardoc.rb +90 -87
  199. data/lib/solargraph.rb +128 -105
  200. data/rbs/fills/rubygems/0/dependency.rbs +193 -0
  201. data/rbs/fills/tuple/tuple.rbs +28 -0
  202. data/rbs/shims/ast/0/node.rbs +5 -0
  203. data/rbs/shims/diff-lcs/1.5/diff-lcs.rbs +11 -0
  204. data/rbs_collection.yaml +1 -1
  205. data/solargraph.gemspec +37 -35
  206. metadata +52 -51
  207. data/lib/solargraph/type_checker/checks.rb +0 -124
  208. data/lib/solargraph/type_checker/param_def.rb +0 -37
  209. data/lib/solargraph/yard_map/to_method.rb +0 -89
  210. data/sig/shims/ast/0/node.rbs +0 -5
  211. /data/{sig → rbs}/shims/ast/2.4/.rbs_meta.yaml +0 -0
  212. /data/{sig → rbs}/shims/ast/2.4/ast.rbs +0 -0
  213. /data/{sig → rbs}/shims/parser/3.2.0.1/builders/default.rbs +0 -0
  214. /data/{sig → rbs}/shims/parser/3.2.0.1/manifest.yaml +0 -0
  215. /data/{sig → rbs}/shims/parser/3.2.0.1/parser.rbs +0 -0
  216. /data/{sig → rbs}/shims/parser/3.2.0.1/polyfill.rbs +0 -0
  217. /data/{sig → rbs}/shims/thor/1.2.0.1/.rbs_meta.yaml +0 -0
  218. /data/{sig → rbs}/shims/thor/1.2.0.1/manifest.yaml +0 -0
  219. /data/{sig → rbs}/shims/thor/1.2.0.1/thor.rbs +0 -0
@@ -1,814 +1,888 @@
1
- # frozen_string_literal: true
2
-
3
- module Solargraph
4
- # A static analysis tool for validating data types.
5
- #
6
- class TypeChecker
7
- autoload :Problem, 'solargraph/type_checker/problem'
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
14
-
15
- # @return [String]
16
- attr_reader :filename
17
-
18
- # @return [Rules]
19
- attr_reader :rules
20
-
21
- # @return [ApiMap]
22
- attr_reader :api_map
23
-
24
- # @param filename [String, nil]
25
- # @param api_map [ApiMap, nil]
26
- # @param level [Symbol] Don't complain about anything above this level
27
- # @param workspace [Workspace, nil] Workspace to use for loading
28
- # type checker rules modified by user config
29
- # @param type_checker_rules [Hash{Symbol => Symbol}] Overrides for
30
- # type checker rules - e.g., :report_undefined => :strong
31
- # @param rules [Rules] Type checker rules object
32
- def initialize filename,
33
- api_map: nil,
34
- level: :normal,
35
- workspace: filename ? Workspace.new(File.dirname(filename)) : nil,
36
- rules: workspace ? workspace.rules(level) : Rules.new(level, {})
37
- @filename = filename
38
- # @todo Smarter directory resolution
39
- @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename))
40
- @rules = rules
41
- # @type [Array<Range>]
42
- @marked_ranges = []
43
- end
44
-
45
- # @return [SourceMap]
46
- def source_map
47
- @source_map ||= api_map.source_map(filename)
48
- end
49
-
50
- # @return [Source]
51
- def source
52
- @source_map.source
53
- end
54
-
55
- # @return [Array<Problem>]
56
- def problems
57
- @problems ||= begin
58
- all = method_tag_problems
59
- .concat(variable_type_tag_problems)
60
- .concat(const_problems)
61
- .concat(call_problems)
62
- unignored = without_ignored(all)
63
- unignored.concat(unneeded_sgignore_problems)
64
- end
65
- end
66
-
67
- class << self
68
- # @param filename [String]
69
- # @param level [Symbol]
70
- # @return [self]
71
- def load filename, level = :normal
72
- source = Solargraph::Source.load(filename)
73
- api_map = Solargraph::ApiMap.new
74
- api_map.map(source)
75
- new(filename, api_map: api_map, level: level)
76
- end
77
-
78
- # @param code [String]
79
- # @param filename [String, nil]
80
- # @param level [Symbol]
81
- # @return [self]
82
- def load_string code, filename = nil, level = :normal
83
- source = Solargraph::Source.load_string(code, filename)
84
- api_map = Solargraph::ApiMap.new
85
- api_map.map(source)
86
- new(filename, api_map: api_map, level: level)
87
- end
88
- end
89
-
90
- private
91
-
92
- # @return [Array<Problem>]
93
- def method_tag_problems
94
- result = []
95
- # @param pin [Pin::Method]
96
- source_map.pins_by_class(Pin::Method).each do |pin|
97
- result.concat method_return_type_problems_for(pin)
98
- result.concat method_param_type_problems_for(pin)
99
- end
100
- result
101
- end
102
-
103
- # @param pin [Pin::Method]
104
- # @return [Array<Problem>]
105
- def method_return_type_problems_for pin
106
- return [] if pin.is_a?(Pin::MethodAlias)
107
- result = []
108
- declared = pin.typify(api_map).self_to_type(pin.full_context).qualify(api_map, *pin.gates)
109
- if declared.undefined?
110
- if pin.return_type.undefined? && rules.require_type_tags?
111
- if pin.attribute?
112
- inferred = pin.probe(api_map).self_to_type(pin.full_context)
113
- result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", pin: pin) unless inferred.defined?
114
- else
115
- result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", pin: pin)
116
- end
117
- elsif pin.return_type.defined? && !resolved_constant?(pin)
118
- result.push Problem.new(pin.location, "Unresolved return type #{pin.return_type} for #{pin.path}", pin: pin)
119
- elsif rules.must_tag_or_infer? && pin.probe(api_map).undefined?
120
- result.push Problem.new(pin.location, "Untyped method #{pin.path} could not be inferred")
121
- end
122
- elsif rules.validate_tags?
123
- unless pin.node.nil? || declared.void? || virtual_pin?(pin) || abstract?(pin)
124
- inferred = pin.probe(api_map).self_to_type(pin.full_context)
125
- if inferred.undefined?
126
- unless rules.ignore_all_undefined? || external?(pin)
127
- result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin)
128
- end
129
- else
130
- unless (rules.require_all_return_types_match_inferred? ? all_types_match?(api_map, inferred, declared) : any_types_match?(api_map, declared, inferred))
131
- result.push Problem.new(pin.location, "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin)
132
- end
133
- end
134
- end
135
- end
136
- result
137
- end
138
-
139
- # @todo This is not optimal. A better solution would probably be to mix
140
- # namespace alias into types at the ApiMap level.
141
- #
142
- # @param pin [Pin::Base]
143
- # @return [Boolean]
144
- def resolved_constant? pin
145
- return true if pin.typify(api_map).defined?
146
- constant_pins = api_map.get_constants('', *pin.closure.gates)
147
- .select { |p| p.name == pin.return_type.namespace }
148
- return true if constant_pins.find { |p| p.typify(api_map).defined? }
149
- # will need to probe when a constant name is assigned to a
150
- # class/module (alias)
151
- return true if constant_pins.find { |p| p.probe(api_map).defined? }
152
- false
153
- end
154
-
155
- # @param pin [Pin::Base]
156
- def virtual_pin? pin
157
- pin.location && source.comment_at?(pin.location.range.ending)
158
- end
159
-
160
- # @param pin [Pin::Method]
161
- # @return [Array<Problem>]
162
- def method_param_type_problems_for pin
163
- stack = api_map.get_method_stack(pin.namespace, pin.name, scope: pin.scope)
164
- result = []
165
- pin.signatures.each do |sig|
166
- params = param_details_from_stack(sig, stack)
167
- if rules.require_type_tags?
168
- sig.parameters.each do |par|
169
- break if par.decl == :restarg || par.decl == :kwrestarg || par.decl == :blockarg
170
- unless params[par.name]
171
- if pin.attribute?
172
- inferred = pin.probe(api_map).self_to_type(pin.full_context)
173
- if inferred.undefined?
174
- result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin)
175
- end
176
- else
177
- result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin)
178
- end
179
- end
180
- end
181
- end
182
- # @param name [String]
183
- # @param data [Hash{Symbol => BasicObject}]
184
- params.each_pair do |name, data|
185
- # @type [ComplexType]
186
- type = data[:qualified]
187
- if type.undefined?
188
- result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}", pin: pin)
189
- end
190
- end
191
- end
192
- result
193
- end
194
-
195
- # @return [Array<Pin::Base>]
196
- def ignored_pins
197
- @ignored_pins ||= []
198
- end
199
-
200
- # @return [Array<Problem>]
201
- def variable_type_tag_problems
202
- result = []
203
- all_variables.each do |pin|
204
- if pin.return_type.defined?
205
- declared = pin.typify(api_map)
206
- next if declared.duck_type?
207
- if declared.defined? && pin.assignment
208
- if rules.validate_tags?
209
- inferred = pin.probe(api_map)
210
- if inferred.undefined?
211
- next if rules.ignore_all_undefined?
212
- if declared_externally?(pin)
213
- ignored_pins.push pin
214
- else
215
- result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin)
216
- end
217
- else
218
- unless any_types_match?(api_map, declared, inferred)
219
- result.push Problem.new(pin.location, "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin)
220
- end
221
- end
222
- elsif declared_externally?(pin)
223
- ignored_pins.push pin
224
- end
225
- elsif !pin.is_a?(Pin::Parameter) && !resolved_constant?(pin)
226
- result.push Problem.new(pin.location, "Unresolved type #{pin.return_type} for variable #{pin.name}", pin: pin)
227
- end
228
- elsif pin.assignment
229
- inferred = pin.probe(api_map)
230
- if inferred.undefined? && declared_externally?(pin)
231
- ignored_pins.push pin
232
- end
233
- end
234
- end
235
- result
236
- end
237
-
238
- # @return [Array<Pin::BaseVariable>]
239
- def all_variables
240
- source_map.pins_by_class(Pin::BaseVariable) + source_map.locals.select { |pin| pin.is_a?(Pin::LocalVariable) }
241
- end
242
-
243
- # @return [Array<Problem>]
244
- def const_problems
245
- return [] unless rules.validate_consts?
246
- result = []
247
- Solargraph::Parser::NodeMethods.const_nodes_from(source.node).each do |const|
248
- rng = Solargraph::Range.from_node(const)
249
- chain = Solargraph::Parser.chain(const, filename)
250
- closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
251
- closure_pin.rebind(api_map)
252
- location = Location.new(filename, rng)
253
- locals = source_map.locals_at(location)
254
- pins = chain.define(api_map, closure_pin, locals)
255
- if pins.empty?
256
- result.push Problem.new(location, "Unresolved constant #{Solargraph::Parser::NodeMethods.unpack_name(const)}")
257
- @marked_ranges.push location.range
258
- end
259
- end
260
- result
261
- end
262
-
263
- # @return [Array<Problem>]
264
- def call_problems
265
- result = []
266
- Solargraph::Parser::NodeMethods.call_nodes_from(source.node).each do |call|
267
- rng = Solargraph::Range.from_node(call)
268
- next if @marked_ranges.any? { |d| d.contain?(rng.start) }
269
- chain = Solargraph::Parser.chain(call, filename)
270
- closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
271
- namespace_pin = closure_pin
272
- if call.type == :block
273
- # blocks in the AST include the method call as well, so the
274
- # node returned by #call_nodes_from needs to be backed out
275
- # one closure
276
- closure_pin = closure_pin.closure
277
- end
278
- closure_pin.rebind(api_map)
279
- location = Location.new(filename, rng)
280
- locals = source_map.locals_at(location)
281
- type = chain.infer(api_map, closure_pin, locals)
282
- if type.undefined? && !rules.ignore_all_undefined?
283
- base = chain
284
- missing = chain
285
- found = nil
286
- closest = ComplexType::UNDEFINED
287
- until base.links.first.undefined?
288
- found = base.define(api_map, closure_pin, locals).first
289
- break if found
290
- missing = base
291
- base = base.base
292
- end
293
- closest = found.typify(api_map) if found
294
- # @todo remove the internal_or_core? check at a higher-than-strict level
295
- if !found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found))
296
- unless closest.generic? || ignored_pins.include?(found)
297
- if closest.defined?
298
- result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}")
299
- else
300
- result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
301
- end
302
- @marked_ranges.push rng
303
- end
304
- end
305
- end
306
- result.concat argument_problems_for(chain, api_map, closure_pin, locals, location)
307
- end
308
- result
309
- end
310
-
311
- # @param chain [Solargraph::Source::Chain]
312
- # @param api_map [Solargraph::ApiMap]
313
- # @param closure_pin [Solargraph::Pin::Closure]
314
- # @param locals [Array<Solargraph::Pin::Base>]
315
- # @param location [Solargraph::Location]
316
- # @return [Array<Problem>]
317
- def argument_problems_for chain, api_map, closure_pin, locals, location
318
- result = []
319
- base = chain
320
- # @type last_base_link [Solargraph::Source::Chain::Call]
321
- last_base_link = base.links.last
322
- return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call)
323
-
324
- arguments = last_base_link.arguments
325
-
326
- pins = base.define(api_map, closure_pin, locals)
327
-
328
- first_pin = pins.first
329
- if first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map)
330
- # Do nothing, as we can't find the actual method implementation
331
- elsif first_pin.is_a?(Pin::Method)
332
- # @type [Pin::Method]
333
- pin = first_pin
334
- ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
335
- arity_problems_for(pin, fake_args_for(closure_pin), location)
336
- elsif pin.path == 'Class#new'
337
- fqns = if base.links.one?
338
- closure_pin.namespace
339
- else
340
- base.base.infer(api_map, closure_pin, locals).namespace
341
- end
342
- init = api_map.get_method_stack(fqns, 'initialize').first
343
- init ? arity_problems_for(init, arguments, location) : []
344
- else
345
- arity_problems_for(pin, arguments, location)
346
- end
347
- return ap unless ap.empty?
348
- return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper)
349
-
350
- all_errors = []
351
- pin.signatures.sort { |sig| sig.parameters.length }.each do |sig|
352
- params = param_details_from_stack(sig, pins)
353
-
354
- signature_errors = signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
355
-
356
- if signature_errors.empty?
357
- # we found a signature that works - meaning errors from
358
- # other signatures don't matter.
359
- return []
360
- end
361
- all_errors.concat signature_errors
362
- end
363
- result.concat all_errors
364
- end
365
- result
366
- end
367
-
368
- # @param location [Location]
369
- # @param locals [Array<Pin::LocalVariable>]
370
- # @param closure_pin [Pin::Closure]
371
- # @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}]
372
- # @param arguments [Array<Source::Chain>]
373
- # @param sig [Pin::Signature]
374
- # @param pin [Pin::Method]
375
- # @param pins [Array<Pin::Method>]
376
- #
377
- # @return [Array<Problem>]
378
- def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
379
- errors = []
380
- # @todo add logic mapping up restarg parameters with
381
- # arguments (including restarg arguments). Use tuples
382
- # when possible, and when not, ensure provably
383
- # incorrect situations are detected.
384
- sig.parameters.each_with_index do |par, idx|
385
- return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing
386
- argchain = arguments[idx]
387
- if argchain.nil?
388
- if par.decl == :arg
389
- final_arg = arguments.last
390
- if final_arg && final_arg.node.type == :splat
391
- argchain = final_arg
392
- return errors
393
- else
394
- errors.push Problem.new(location, "Not enough arguments to #{pin.path}")
395
- end
396
- else
397
- final_arg = arguments.last
398
- argchain = final_arg if final_arg && [:kwsplat, :hash].include?(final_arg.node.type)
399
- end
400
- end
401
- if argchain
402
- if par.decl != :arg
403
- errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx
404
- next
405
- else
406
- if argchain.node.type == :splat && argchain == arguments.last
407
- final_arg = argchain
408
- end
409
- if (final_arg && final_arg.node.type == :splat)
410
- # The final argument given has been seen and was a
411
- # splat, which doesn't give us useful types or
412
- # arities against positional parameters, so let's
413
- # continue on in case there are any required
414
- # kwargs we should warn about
415
- next
416
- end
417
- if argchain.node.type == :splat && par != sig.parameters.last
418
- # we have been given a splat and there are more
419
- # arguments to come.
420
-
421
- # @todo Improve this so that we can skip past the
422
- # rest of the positional parameters here but still
423
- # process the kwargs
424
- return errors
425
- end
426
- ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED
427
- ptype = ptype.self_to_type(par.context)
428
- if ptype.nil?
429
- # @todo Some level (strong, I guess) should require the param here
430
- else
431
- argtype = argchain.infer(api_map, closure_pin, locals)
432
- argtype = argtype.self_to_type(closure_pin.context)
433
- if argtype.defined? && ptype.defined? && !any_types_match?(api_map, ptype, argtype)
434
- errors.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
435
- return errors
436
- end
437
- end
438
- end
439
- elsif par.decl == :kwarg
440
- errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
441
- next
442
- end
443
- end
444
- errors
445
- end
446
-
447
- # @param sig [Pin::Signature]
448
- # @param argchain [Source::Chain]
449
- # @param api_map [ApiMap]
450
- # @param closure_pin [Pin::Closure]
451
- # @param locals [Array<Pin::LocalVariable>]
452
- # @param location [Location]
453
- # @param pin [Pin::Method]
454
- # @param params [Hash{String => Hash{Symbol => String, Solargraph::ComplexType}}]
455
- # @param idx [Integer]
456
- #
457
- # @return [Array<Problem>]
458
- def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx
459
- result = []
460
- kwargs = convert_hash(argchain.node)
461
- par = sig.parameters[idx]
462
- argchain = kwargs[par.name.to_sym]
463
- if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}')
464
- result.concat kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs)
465
- else
466
- if argchain
467
- data = params[par.name]
468
- if data.nil?
469
- # @todo Some level (strong, I guess) should require the param here
470
- else
471
- ptype = data[:qualified]
472
- ptype = ptype.self_to_type(pin.context)
473
- unless ptype.undefined?
474
- # @sg-ignore https://github.com/castwide/solargraph/pull/1127
475
- argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context)
476
- # @sg-ignore Unresolved call to defined?
477
- if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
478
- result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
479
- end
480
- end
481
- end
482
- elsif par.decl == :kwarg
483
- result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
484
- end
485
- end
486
- result
487
- end
488
-
489
- # @param api_map [ApiMap]
490
- # @param closure_pin [Pin::Closure]
491
- # @param locals [Array<Pin::LocalVariable>]
492
- # @param location [Location]
493
- # @param pin [Pin::Method]
494
- # @param params [Hash{String => [nil, Hash]}]
495
- # @param kwargs [Hash{Symbol => Source::Chain}]
496
- # @return [Array<Problem>]
497
- def kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs)
498
- result = []
499
- kwargs.each_pair do |pname, argchain|
500
- next unless params.key?(pname.to_s)
501
- ptype = params[pname.to_s][:qualified]
502
- ptype = ptype.self_to_type(pin.context)
503
- argtype = argchain.infer(api_map, closure_pin, locals)
504
- argtype = argtype.self_to_type(closure_pin.context)
505
- if argtype.defined? && ptype && !any_types_match?(api_map, ptype, argtype)
506
- result.push Problem.new(location, "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}")
507
- end
508
- end
509
- result
510
- end
511
-
512
- # @param param_details [Hash{String => Hash{Symbol => String, ComplexType}}]
513
- # @param pin [Pin::Method, Pin::Signature]
514
- # @param relevant_pin [Pin::Method, Pin::Signature] the pin which is under inspection
515
- # @return [void]
516
- def add_restkwarg_param_tag_details(param_details, pin, relevant_pin)
517
- # see if we have additional tags to pay attention to from YARD -
518
- # e.g., kwargs in a **restkwargs splat
519
- tags = pin.docstring.tags(:param)
520
- tags.each do |tag|
521
- next if param_details.key? tag.name.to_s
522
- next if tag.types.nil?
523
- details = {
524
- tagged: tag.types.join(', '),
525
- qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, pin.full_context.namespace)
526
- }
527
- # don't complain about a param that didn't come from the pin we're looking at anyway
528
- if details[:qualified].defined? ||
529
- relevant_pin.parameter_names.include?(tag.name.to_s)
530
- param_details[tag.name.to_s] = details
531
- end
532
- end
533
- end
534
-
535
- # @param pin [Pin::Signature]
536
- # @return [Hash{String => Hash{Symbol => String, ComplexType}}]
537
- def signature_param_details(pin)
538
- # @type [Hash{String => Hash{Symbol => String, ComplexType}}]
539
- result = {}
540
- pin.parameters.each do |param|
541
- type = param.typify(api_map)
542
- next if type.nil? || type.undefined?
543
- result[param.name.to_s] = {
544
- tagged: type.tags,
545
- qualified: type
546
- }
547
- end
548
- # see if we have additional tags to pay attention to from YARD -
549
- # e.g., kwargs in a **restkwargs splat
550
- tags = pin.docstring.tags(:param)
551
- tags.each do |tag|
552
- next if result.key? tag.name.to_s
553
- next if tag.types.nil?
554
- result[tag.name.to_s] = {
555
- tagged: tag.types.join(', '),
556
- qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, *pin.closure.gates)
557
- }
558
- end
559
- result
560
- end
561
-
562
- # The original signature defines the parameters, but other
563
- # signatures and method pins can help by adding type information
564
- #
565
- # @param param_details [Hash{String => Hash{Symbol => String, ComplexType}}]
566
- # @param param_names [Array<String>]
567
- # @param new_param_details [Hash{String => Hash{Symbol => String, ComplexType}}]
568
- #
569
- # @return [void]
570
- def add_to_param_details(param_details, param_names, new_param_details)
571
- new_param_details.each do |param_name, details|
572
- next unless param_names.include?(param_name)
573
-
574
- param_details[param_name] ||= {}
575
- param_details[param_name][:tagged] ||= details[:tagged]
576
- param_details[param_name][:qualified] ||= details[:qualified]
577
- end
578
- end
579
-
580
- # @param signature [Pin::Signature]
581
- # @param method_pin_stack [Array<Pin::Method>]
582
- # @return [Hash{String => Hash{Symbol => String, ComplexType}}]
583
- def param_details_from_stack(signature, method_pin_stack)
584
- signature_type = signature.typify(api_map)
585
- signature = signature.proxy signature_type
586
- param_details = signature_param_details(signature)
587
- param_names = signature.parameter_names
588
-
589
- method_pin_stack.each do |method_pin|
590
- add_restkwarg_param_tag_details(param_details, method_pin, signature)
591
-
592
- # documentation of types in superclasses should fail back to
593
- # subclasses if the subclass hasn't documented something
594
- method_pin.signatures.each do |sig|
595
- add_restkwarg_param_tag_details(param_details, sig, signature)
596
- add_to_param_details param_details, param_names, signature_param_details(sig)
597
- end
598
- end
599
- param_details
600
- end
601
-
602
- # @param pin [Pin::Base]
603
- def internal? pin
604
- return false if pin.nil?
605
- pin.location && api_map.bundled?(pin.location.filename)
606
- end
607
-
608
- # True if the pin is either internal (part of the workspace) or from the core/stdlib
609
- # @param pin [Pin::Base]
610
- def internal_or_core? pin
611
- # @todo RBS pins are not necessarily core/stdlib pins
612
- internal?(pin) || pin.source == :rbs
613
- end
614
-
615
- # @param pin [Pin::Base]
616
- def external? pin
617
- !internal? pin
618
- end
619
-
620
- # @param pin [Pin::BaseVariable]
621
- def declared_externally? pin
622
- raise "No assignment found" if pin.assignment.nil?
623
-
624
- chain = Solargraph::Parser.chain(pin.assignment, filename)
625
- rng = Solargraph::Range.from_node(pin.assignment)
626
- closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
627
- location = Location.new(filename, Range.from_node(pin.assignment))
628
- locals = source_map.locals_at(location)
629
- type = chain.infer(api_map, closure_pin, locals)
630
- if type.undefined? && !rules.ignore_all_undefined?
631
- base = chain
632
- missing = chain
633
- found = nil
634
- closest = ComplexType::UNDEFINED
635
- until base.links.first.undefined?
636
- found = base.define(api_map, closure_pin, locals).first
637
- break if found
638
- missing = base
639
- base = base.base
640
- end
641
- closest = found.typify(api_map) if found
642
- if !found || closest.defined? || internal?(found)
643
- return false
644
- end
645
- end
646
- true
647
- end
648
-
649
- # @param pin [Pin::Method]
650
- # @param arguments [Array<Source::Chain>]
651
- # @param location [Location]
652
- # @return [Array<Problem>]
653
- def arity_problems_for pin, arguments, location
654
- results = pin.signatures.map do |sig|
655
- r = parameterized_arity_problems_for(pin, sig.parameters, arguments, location)
656
- return [] if r.empty?
657
- r
658
- end
659
- results.first
660
- end
661
-
662
- # @param pin [Pin::Method]
663
- # @param parameters [Array<Pin::Parameter>]
664
- # @param arguments [Array<Source::Chain>]
665
- # @param location [Location]
666
- # @return [Array<Problem>]
667
- def parameterized_arity_problems_for(pin, parameters, arguments, location)
668
- return [] unless pin.explicit?
669
- return [] if parameters.empty? && arguments.empty?
670
- return [] if pin.anon_splat?
671
- unchecked = arguments.dup # creates copy of and unthaws array
672
- add_params = 0
673
- if unchecked.empty? && parameters.any? { |param| param.decl == :kwarg }
674
- return [Problem.new(location, "Missing keyword arguments to #{pin.path}")]
675
- end
676
- settled_kwargs = 0
677
- unless unchecked.empty?
678
- if any_splatted_call?(unchecked.map(&:node))
679
- settled_kwargs = parameters.count(&:keyword?)
680
- else
681
- kwargs = convert_hash(unchecked.last.node)
682
- if parameters.any? { |param| [:kwarg, :kwoptarg].include?(param.decl) || param.kwrestarg? }
683
- if kwargs.empty?
684
- add_params += 1
685
- else
686
- unchecked.pop
687
- parameters.each do |param|
688
- next unless param.keyword?
689
- if kwargs.key?(param.name.to_sym)
690
- kwargs.delete param.name.to_sym
691
- settled_kwargs += 1
692
- elsif param.decl == :kwarg
693
- last_arg_last_link = arguments.last.links.last
694
- return [] if last_arg_last_link.is_a?(Solargraph::Source::Chain::Hash) && last_arg_last_link.splatted?
695
- return [Problem.new(location, "Missing keyword argument #{param.name} to #{pin.path}")]
696
- end
697
- end
698
- kwargs.clear if parameters.any?(&:kwrestarg?)
699
- unless kwargs.empty?
700
- return [Problem.new(location, "Unrecognized keyword argument #{kwargs.keys.first} to #{pin.path}")]
701
- end
702
- end
703
- end
704
- end
705
- end
706
- req = required_param_count(parameters)
707
- if req + add_params < unchecked.length
708
- return [] if parameters.any?(&:rest?)
709
- opt = optional_param_count(parameters)
710
- return [] if unchecked.length <= req + opt
711
- if req + add_params + 1 == unchecked.length && any_splatted_call?(unchecked.map(&:node)) && (parameters.map(&:decl) & [:kwarg, :kwoptarg, :kwrestarg]).any?
712
- return []
713
- end
714
- return [] if arguments.length - req == parameters.select { |p| [:optarg, :kwoptarg].include?(p.decl) }.length
715
- return [Problem.new(location, "Too many arguments to #{pin.path}")]
716
- elsif unchecked.length < req - settled_kwargs && (arguments.empty? || (!arguments.last.splat? && !arguments.last.links.last.is_a?(Solargraph::Source::Chain::Hash)))
717
- # HACK: Kernel#raise signature is incorrect in Ruby 2.7 core docs.
718
- # See https://github.com/castwide/solargraph/issues/418
719
- unless arguments.empty? && pin.path == 'Kernel#raise'
720
- return [Problem.new(location, "Not enough arguments to #{pin.path}")]
721
- end
722
- end
723
- []
724
- end
725
-
726
- # @param parameters [Enumerable<Pin::Parameter>]
727
- # @todo need to use generic types in method to choose correct
728
- # signature and generate Integer as return type
729
- # @return [Integer]
730
- def required_param_count(parameters)
731
- parameters.sum { |param| %i[arg kwarg].include?(param.decl) ? 1 : 0 }
732
- end
733
-
734
- # @param parameters [Enumerable<Pin::Parameter>]
735
- # @param pin [Pin::Method]
736
- # @return [Integer]
737
- def optional_param_count(parameters)
738
- parameters.select { |p| p.decl == :optarg }.length
739
- end
740
-
741
- # @param pin [Pin::Method]
742
- def abstract? pin
743
- pin.docstring.has_tag?('abstract') ||
744
- (pin.closure && pin.closure.docstring.has_tag?('abstract'))
745
- end
746
-
747
- # @param pin [Pin::Method]
748
- # @return [Array<Source::Chain>]
749
- def fake_args_for(pin)
750
- args = []
751
- with_opts = false
752
- with_block = false
753
- pin.parameters.each do |pin|
754
- if [:kwarg, :kwoptarg, :kwrestarg].include?(pin.decl)
755
- with_opts = true
756
- elsif pin.decl == :block
757
- with_block = true
758
- elsif pin.decl == :restarg
759
- args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true)
760
- else
761
- args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)])
762
- end
763
- end
764
- args.push Solargraph::Parser.chain_string('{}') if with_opts
765
- args.push Solargraph::Parser.chain_string('&') if with_block
766
- args
767
- end
768
-
769
- # @return [Set<Integer>]
770
- def sg_ignore_lines_processed
771
- @sg_ignore_lines_processed ||= Set.new
772
- end
773
-
774
- # @return [Set<Integer>]
775
- def all_sg_ignore_lines
776
- source.associated_comments.select do |_line, text|
777
- text.include?('@sg-ignore')
778
- end.keys.to_set
779
- end
780
-
781
- # @return [Array<Integer>]
782
- def unprocessed_sg_ignore_lines
783
- (all_sg_ignore_lines - sg_ignore_lines_processed).to_a.sort
784
- end
785
-
786
- # @return [Array<Problem>]
787
- def unneeded_sgignore_problems
788
- return [] unless rules.validate_sg_ignores?
789
-
790
- unprocessed_sg_ignore_lines.map do |line|
791
- Problem.new(
792
- Location.new(filename, Range.from_to(line, 0, line, 0)),
793
- 'Unneeded @sg-ignore comment'
794
- )
795
- end
796
- end
797
-
798
- # @param problems [Array<Problem>]
799
- # @return [Array<Problem>]
800
- def without_ignored problems
801
- problems.reject do |problem|
802
- node = source.node_at(problem.location.range.start.line, problem.location.range.start.column)
803
- ignored = node && source.comments_for(node)&.include?('@sg-ignore')
804
- unless !ignored || all_sg_ignore_lines.include?(problem.location.range.start.line)
805
- # :nocov:
806
- Solargraph.assert_or_log(:sg_ignore) { "@sg-ignore accounting issue - node is #{node}" }
807
- # :nocov:
808
- end
809
- sg_ignore_lines_processed.add problem.location.range.start.line if ignored
810
- ignored
811
- end
812
- end
813
- end
814
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ # A static analysis tool for validating data types.
5
+ #
6
+ class TypeChecker
7
+ autoload :Problem, 'solargraph/type_checker/problem'
8
+ autoload :Rules, 'solargraph/type_checker/rules'
9
+
10
+ # @!parse
11
+ # include Solargraph::Parser::ParserGem::NodeMethods
12
+ include Parser::NodeMethods
13
+
14
+ # @return [String]
15
+ attr_reader :filename
16
+
17
+ # @return [Rules]
18
+ attr_reader :rules
19
+
20
+ # @return [ApiMap]
21
+ attr_reader :api_map
22
+
23
+ # @param filename [String, nil]
24
+ # @param api_map [ApiMap, nil]
25
+ # @param level [Symbol] Don't complain about anything above this level
26
+ # @param workspace [Workspace, nil] Workspace to use for loading
27
+ # type checker rules modified by user config
28
+ # @param rules [Rules] Type checker rules object
29
+ def initialize filename,
30
+ api_map: nil,
31
+ level: :normal,
32
+ workspace: filename ? Workspace.new(File.dirname(filename)) : nil,
33
+ rules: workspace ? workspace.rules(level) : Rules.new(level, {})
34
+ @filename = filename
35
+ # @todo Smarter directory resolution
36
+ @api_map = api_map || Solargraph::ApiMap.load(File.dirname(filename),
37
+ loose_unions: !rules.require_all_unique_types_support_call?)
38
+ @rules = rules
39
+ # @type [Array<Range>]
40
+ @marked_ranges = []
41
+ end
42
+
43
+ # @return [SourceMap]
44
+ def source_map
45
+ @source_map ||= api_map.source_map(filename)
46
+ end
47
+
48
+ # @return [Source]
49
+ def source
50
+ source_map.source
51
+ end
52
+
53
+ # @param inferred [ComplexType, ComplexType::UniqueType]
54
+ # @param expected [ComplexType, ComplexType::UniqueType]
55
+ def return_type_conforms_to? inferred, expected
56
+ conforms_to?(inferred, expected, :return_type)
57
+ end
58
+
59
+ # @param inferred [ComplexType, ComplexType::UniqueType]
60
+ # @param expected [ComplexType, ComplexType::UniqueType]
61
+ def arg_conforms_to? inferred, expected
62
+ conforms_to?(inferred, expected, :method_call)
63
+ end
64
+
65
+ # @param inferred [ComplexType, ComplexType::UniqueType]
66
+ # @param expected [ComplexType, ComplexType::UniqueType]
67
+ def assignment_conforms_to? inferred, expected
68
+ conforms_to?(inferred, expected, :assignment)
69
+ end
70
+
71
+ # @param inferred [ComplexType, ComplexType::UniqueType]
72
+ # @param expected [ComplexType, ComplexType::UniqueType]
73
+ # @param scenario [Symbol]
74
+ def conforms_to? inferred, expected, scenario
75
+ rules_arr = []
76
+ rules_arr << :allow_empty_params unless rules.require_inferred_type_params?
77
+ rules_arr << :allow_any_match unless rules.require_all_unique_types_match_expected?
78
+ rules_arr << :allow_undefined unless rules.require_no_undefined_args?
79
+ rules_arr << :allow_unresolved_generic unless rules.require_generics_resolved?
80
+ rules_arr << :allow_unmatched_interface unless rules.require_interfaces_resolved?
81
+ rules_arr << :allow_reverse_match unless rules.require_downcasts?
82
+ inferred.conforms_to?(api_map, expected, scenario,
83
+ rules_arr)
84
+ end
85
+
86
+ # @return [Array<Problem>]
87
+ def problems
88
+ @problems ||= begin
89
+ all = method_tag_problems
90
+ .concat(variable_type_tag_problems)
91
+ .concat(const_problems)
92
+ .concat(call_problems)
93
+ unignored = without_ignored(all)
94
+ unignored.concat(unneeded_sgignore_problems)
95
+ end
96
+ end
97
+
98
+ class << self
99
+ # @param filename [String]
100
+ # @param level [Symbol]
101
+ # @return [self]
102
+ def load filename, level = :normal
103
+ source = Solargraph::Source.load(filename)
104
+ rules = Rules.new(level, {})
105
+ api_map = Solargraph::ApiMap.new(loose_unions:
106
+ !rules.require_all_unique_types_support_call?)
107
+ api_map.map(source)
108
+ new(filename, api_map: api_map, level: level, rules: rules)
109
+ end
110
+
111
+ # @param code [String]
112
+ # @param filename [String, nil]
113
+ # @param level [Symbol]
114
+ # @param api_map [Solargraph::ApiMap, nil]
115
+ # @return [self]
116
+ def load_string code, filename = nil, level = :normal, api_map: nil
117
+ source = Solargraph::Source.load_string(code, filename)
118
+ rules = Rules.new(level, {})
119
+ api_map ||= Solargraph::ApiMap.new(loose_unions:
120
+ !rules.require_all_unique_types_support_call?)
121
+ # @sg-ignore flow sensitive typing needs better handling of ||= on lvars
122
+ api_map.map(source)
123
+ new(filename, api_map: api_map, level: level, rules: rules)
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ # @return [Array<Problem>]
130
+ def method_tag_problems
131
+ result = []
132
+ # @param pin [Pin::Method]
133
+ source_map.pins_by_class(Pin::Method).each do |pin|
134
+ result.concat method_return_type_problems_for(pin)
135
+ result.concat method_param_type_problems_for(pin)
136
+ end
137
+ result
138
+ end
139
+
140
+ # @param pin [Pin::Method]
141
+ # @return [Array<Problem>]
142
+ def method_return_type_problems_for pin
143
+ return [] if pin.is_a?(Pin::MethodAlias)
144
+ result = []
145
+ declared = pin.typify(api_map).self_to_type(pin.full_context).qualify(api_map, *pin.gates)
146
+ if declared.undefined?
147
+ # @sg-ignore Need to add nil check here
148
+ if pin.return_type.undefined? && rules.require_type_tags?
149
+ if pin.attribute?
150
+ inferred = pin.probe(api_map).self_to_type(pin.full_context)
151
+ unless inferred.defined?
152
+ result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}",
153
+ pin: pin)
154
+ end
155
+ else
156
+ result.push Problem.new(pin.location, "Missing @return tag for #{pin.path}", pin: pin)
157
+ end
158
+ # @sg-ignore Need to add nil check here
159
+ elsif pin.return_type.defined? && !resolved_constant?(pin)
160
+ result.push Problem.new(pin.location, "Unresolved return type #{pin.return_type} for #{pin.path}", pin: pin)
161
+ elsif rules.must_tag_or_infer? && pin.probe(api_map).undefined?
162
+ result.push Problem.new(pin.location, "Untyped method #{pin.path} could not be inferred")
163
+ end
164
+ elsif rules.validate_tags?
165
+ unless pin.node.nil? || declared.void? || virtual_pin?(pin) || abstract?(pin)
166
+ inferred = pin.probe(api_map).self_to_type(pin.full_context)
167
+ if inferred.undefined?
168
+ unless rules.ignore_all_undefined? || external?(pin)
169
+ result.push Problem.new(pin.location, "#{pin.path} return type could not be inferred", pin: pin)
170
+ end
171
+ else
172
+ unless return_type_conforms_to?(inferred, declared)
173
+ result.push Problem.new(pin.location,
174
+ "Declared return type #{declared.rooted_tags} does not match inferred type #{inferred.rooted_tags} for #{pin.path}", pin: pin)
175
+ end
176
+ end
177
+ end
178
+ end
179
+ result
180
+ end
181
+
182
+ # @todo This is not optimal. A better solution would probably be to mix
183
+ # namespace alias into types at the ApiMap level.
184
+ #
185
+ # @param pin [Pin::Base]
186
+ # @return [Boolean]
187
+ def resolved_constant? pin
188
+ return true if pin.typify(api_map).defined?
189
+ constant_pins = api_map.get_constants('', *pin.closure.gates)
190
+ .select { |p| p.name == pin.return_type.namespace }
191
+ return true if constant_pins.find { |p| p.typify(api_map).defined? }
192
+ # will need to probe when a constant name is assigned to a
193
+ # class/module (alias)
194
+ return true if constant_pins.find { |p| p.probe(api_map).defined? }
195
+ false
196
+ end
197
+
198
+ # @param pin [Pin::Base]
199
+ def virtual_pin? pin
200
+ # @sg-ignore Need to add nil check here
201
+ pin.location && source.comment_at?(pin.location.range.ending)
202
+ end
203
+
204
+ # @param pin [Pin::Method]
205
+ # @return [Array<Problem>]
206
+ def method_param_type_problems_for pin
207
+ stack = api_map.get_method_stack(pin.namespace, pin.name, scope: pin.scope)
208
+ result = []
209
+ pin.signatures.each do |sig|
210
+ params = param_details_from_stack(sig, stack)
211
+ if rules.require_type_tags?
212
+ sig.parameters.each do |par|
213
+ break if %i[restarg kwrestarg blockarg].include?(par.decl)
214
+ unless params[par.name]
215
+ if pin.attribute?
216
+ inferred = pin.probe(api_map).self_to_type(pin.full_context)
217
+ if inferred.undefined?
218
+ result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin)
219
+ end
220
+ else
221
+ result.push Problem.new(pin.location, "Missing @param tag for #{par.name} on #{pin.path}", pin: pin)
222
+ end
223
+ end
224
+ end
225
+ end
226
+ # @param name [String]
227
+ # @param data [Hash{Symbol => BasicObject}]
228
+ params.each_pair do |name, data|
229
+ # @type [ComplexType]
230
+ type = data[:qualified]
231
+ if type.undefined?
232
+ result.push Problem.new(pin.location, "Unresolved type #{data[:tagged]} for #{name} param on #{pin.path}",
233
+ pin: pin)
234
+ end
235
+ end
236
+ end
237
+ result
238
+ end
239
+
240
+ # @return [Array<Pin::Base>]
241
+ def ignored_pins
242
+ @ignored_pins ||= []
243
+ end
244
+
245
+ # @return [Array<Problem>]
246
+ def variable_type_tag_problems
247
+ result = []
248
+ all_variables.each do |pin|
249
+ # @sg-ignore Need to add nil check here
250
+ if pin.return_type.defined?
251
+ declared = pin.typify(api_map)
252
+ next if declared.duck_type?
253
+ if declared.defined? && pin.assignment
254
+ if rules.validate_tags?
255
+ inferred = pin.probe(api_map)
256
+ if inferred.undefined?
257
+ next if rules.ignore_all_undefined?
258
+ if declared_externally?(pin)
259
+ ignored_pins.push pin
260
+ else
261
+ result.push Problem.new(pin.location, "Variable type could not be inferred for #{pin.name}", pin: pin)
262
+ end
263
+ else
264
+ unless assignment_conforms_to?(inferred, declared)
265
+ result.push Problem.new(pin.location,
266
+ "Declared type #{declared} does not match inferred type #{inferred} for variable #{pin.name}", pin: pin)
267
+ end
268
+ end
269
+ elsif declared_externally?(pin)
270
+ ignored_pins.push pin
271
+ end
272
+ elsif !pin.is_a?(Pin::Parameter) && !resolved_constant?(pin)
273
+ result.push Problem.new(pin.location, "Unresolved type #{pin.return_type} for variable #{pin.name}",
274
+ pin: pin)
275
+ end
276
+ elsif pin.assignment
277
+ inferred = pin.probe(api_map)
278
+ ignored_pins.push pin if inferred.undefined? && declared_externally?(pin)
279
+ end
280
+ end
281
+ result
282
+ end
283
+
284
+ # @return [Array<Pin::BaseVariable>]
285
+ def all_variables
286
+ source_map.pins_by_class(Pin::BaseVariable) + source_map.locals.select { |pin| pin.is_a?(Pin::LocalVariable) }
287
+ end
288
+
289
+ # @return [Array<Problem>]
290
+ def const_problems
291
+ return [] unless rules.validate_consts?
292
+ result = []
293
+ Solargraph::Parser::NodeMethods.const_nodes_from(source.node).each do |const|
294
+ rng = Solargraph::Range.from_node(const)
295
+ chain = Solargraph::Parser.chain(const, filename)
296
+ # @sg-ignore Need to add nil check here
297
+ closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
298
+ closure_pin.rebind(api_map)
299
+ # @sg-ignore Need to add nil check here
300
+ location = Location.new(filename, rng)
301
+ locals = source_map.locals_at(location)
302
+ pins = chain.define(api_map, closure_pin, locals)
303
+ if pins.empty?
304
+ result.push Problem.new(location, "Unresolved constant #{Solargraph::Parser::NodeMethods.unpack_name(const)}")
305
+ @marked_ranges.push location.range
306
+ end
307
+ end
308
+ result
309
+ end
310
+
311
+ # @return [Array<Problem>]
312
+ def call_problems
313
+ result = []
314
+ Solargraph::Parser::NodeMethods.call_nodes_from(source.node).each do |call|
315
+ rng = Solargraph::Range.from_node(call)
316
+ # @sg-ignore Need to add nil check here
317
+ next if @marked_ranges.any? { |d| d.contain?(rng.start) }
318
+ chain = Solargraph::Parser.chain(call, filename)
319
+ # @sg-ignore Need to add nil check here
320
+ closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
321
+ if call.type == :block
322
+ # blocks in the AST include the method call as well, so the
323
+ # node returned by #call_nodes_from needs to be backed out
324
+ # one closure
325
+ # @todo Need to add nil check here
326
+ # @todo Should warn on nil deference here
327
+ closure_pin = closure_pin.closure
328
+ end
329
+ # @sg-ignore Need to add nil check here
330
+ closure_pin.rebind(api_map)
331
+ # @sg-ignore Need to add nil check here
332
+ location = Location.new(filename, rng)
333
+ locals = source_map.locals_at(location)
334
+ # @sg-ignore Need to add nil check here
335
+ type = chain.infer(api_map, closure_pin, locals)
336
+ if type.undefined? && !rules.ignore_all_undefined?
337
+ base = chain
338
+ missing = chain
339
+ # @type [Solargraph::Pin::Base, nil]
340
+ found = nil
341
+ # @type [Array<Solargraph::Pin::Base>]
342
+ all_found = []
343
+ until base.links.first.undefined?
344
+ # @sg-ignore Need to add nil check here
345
+ all_found = base.define(api_map, closure_pin, locals)
346
+ found = all_found.first
347
+ break if found
348
+ missing = base
349
+ base = base.base
350
+ end
351
+ all_closest = all_found.map { |pin| pin.typify(api_map) }
352
+ closest = ComplexType.new(all_closest.flat_map(&:items).uniq)
353
+ # @todo remove the internal_or_core? check at a higher-than-strict level
354
+ if (!found || found.is_a?(Pin::BaseVariable) || (closest.defined? && internal_or_core?(found))) && !(closest.generic? || ignored_pins.include?(found))
355
+ if closest.defined?
356
+ result.push Problem.new(location, "Unresolved call to #{missing.links.last.word} on #{closest}")
357
+ else
358
+ result.push Problem.new(location, "Unresolved call to #{missing.links.last.word}")
359
+ end
360
+ @marked_ranges.push rng
361
+ end
362
+ end
363
+ # @sg-ignore Need to add nil check here
364
+ result.concat argument_problems_for(chain, api_map, closure_pin, locals, location)
365
+ end
366
+ result
367
+ end
368
+
369
+ # @param chain [Solargraph::Source::Chain]
370
+ # @param api_map [Solargraph::ApiMap]
371
+ # @param closure_pin [Solargraph::Pin::Closure]
372
+ # @param locals [Array<Solargraph::Pin::LocalVariable>]
373
+ # @param location [Solargraph::Location]
374
+ # @return [Array<Problem>]
375
+ def argument_problems_for chain, api_map, closure_pin, locals, location
376
+ result = []
377
+ base = chain
378
+ last_base_link = base.links.last
379
+ return [] unless last_base_link.is_a?(Solargraph::Source::Chain::Call)
380
+
381
+ arguments = last_base_link.arguments
382
+
383
+ pins = base.define(api_map, closure_pin, locals)
384
+
385
+ first_pin = pins.first
386
+ if first_pin.is_a?(Pin::DelegatedMethod) && !first_pin.resolvable?(api_map)
387
+ # Do nothing, as we can't find the actual method implementation
388
+ elsif first_pin.is_a?(Pin::Method)
389
+ # @type [Pin::Method]
390
+ pin = first_pin
391
+ ap = if base.links.last.is_a?(Solargraph::Source::Chain::ZSuper)
392
+ arity_problems_for(pin, fake_args_for(closure_pin), location)
393
+ elsif pin.path == 'Class#new'
394
+ fqns = if base.links.one?
395
+ closure_pin.namespace
396
+ else
397
+ base.base.infer(api_map, closure_pin, locals).namespace
398
+ end
399
+ init = api_map.get_method_stack(fqns, 'initialize').first
400
+
401
+ # @type [::Array<Solargraph::TypeChecker::Problem>]
402
+ init ? arity_problems_for(init, arguments, location) : []
403
+ else
404
+ arity_problems_for(pin, arguments, location)
405
+ end
406
+ return ap unless ap.empty?
407
+ return [] if !rules.validate_calls? || base.links.first.is_a?(Solargraph::Source::Chain::ZSuper)
408
+
409
+ all_errors = []
410
+ pin.signatures.sort_by { |sig| sig.parameters.length }.each do |sig|
411
+ params = param_details_from_stack(sig, pins)
412
+
413
+ signature_errors = signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
414
+
415
+ if signature_errors.empty?
416
+ # we found a signature that works - meaning errors from
417
+ # other signatures don't matter.
418
+ return []
419
+ end
420
+ all_errors.concat signature_errors
421
+ end
422
+ result.concat all_errors
423
+ end
424
+ result
425
+ end
426
+
427
+ # @param location [Location]
428
+ # @param locals [Array<Pin::LocalVariable>]
429
+ # @param closure_pin [Pin::Closure]
430
+ # @param params [Hash{String => undefined}]
431
+ # @param arguments [Array<Source::Chain>]
432
+ # @param sig [Pin::Signature]
433
+ # @param pin [Pin::Method]
434
+ #
435
+ # @return [Array<Problem>]
436
+ def signature_argument_problems_for location, locals, closure_pin, params, arguments, sig, pin
437
+ errors = []
438
+ # @todo add logic mapping up restarg parameters with
439
+ # arguments (including restarg arguments). Use tuples
440
+ # when possible, and when not, ensure provably
441
+ # incorrect situations are detected.
442
+ sig.parameters.each_with_index do |par, idx|
443
+ return errors if par.decl == :restarg # bail out and assume the rest is valid pending better arg processing
444
+ argchain = arguments[idx]
445
+ if argchain.nil?
446
+ final_arg = arguments.last
447
+ if par.decl == :arg
448
+ if final_arg && final_arg.node.type == :splat
449
+ argchain = final_arg
450
+ return errors
451
+ else
452
+ errors.push Problem.new(location, "Not enough arguments to #{pin.path}")
453
+ end
454
+ elsif final_arg && %i[kwsplat hash].include?(final_arg.node.type)
455
+ argchain = final_arg
456
+ end
457
+ end
458
+ if argchain
459
+ if par.decl == :arg
460
+ final_arg = argchain if argchain.node.type == :splat && argchain == arguments.last
461
+ if final_arg && final_arg.node.type == :splat
462
+ # The final argument given has been seen and was a
463
+ # splat, which doesn't give us useful types or
464
+ # arities against positional parameters, so let's
465
+ # continue on in case there are any required
466
+ # kwargs we should warn about
467
+ next
468
+ end
469
+ if argchain.node.type == :splat && par != sig.parameters.last
470
+ # we have been given a splat and there are more
471
+ # arguments to come.
472
+
473
+ # @todo Improve this so that we can skip past the
474
+ # rest of the positional parameters here but still
475
+ # process the kwargs
476
+ return errors
477
+ end
478
+ ptype = params.key?(par.name) ? params[par.name][:qualified] : ComplexType::UNDEFINED
479
+ ptype = ptype.self_to_type(par.context)
480
+ if ptype.nil?
481
+ # @todo Some level (strong, I guess) should require the param here
482
+ else
483
+ argtype = argchain.infer(api_map, closure_pin, locals)
484
+ argtype = argtype.self_to_type(closure_pin.context)
485
+ if argtype.defined? && ptype.defined? && !arg_conforms_to?(argtype, ptype)
486
+ errors.push Problem.new(location,
487
+ "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
488
+ return errors
489
+ end
490
+ end
491
+ else
492
+ errors.concat kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx
493
+ next
494
+ end
495
+ elsif par.decl == :kwarg
496
+ errors.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
497
+ next
498
+ end
499
+ end
500
+ errors
501
+ end
502
+
503
+ # @param sig [Pin::Signature]
504
+ # @param argchain [Solargraph::Source::Chain]
505
+ # @param api_map [ApiMap]
506
+ # @param closure_pin [Pin::Closure]
507
+ # @param locals [Array<Pin::LocalVariable>]
508
+ # @param location [Location]
509
+ # @param pin [Pin::Method]
510
+ # @param params [Hash{String => Hash{Symbol => undefined}}]
511
+ # @param idx [Integer]
512
+ #
513
+ # @return [Array<Problem>]
514
+ def kwarg_problems_for sig, argchain, api_map, closure_pin, locals, location, pin, params, idx
515
+ result = []
516
+ kwargs = convert_hash(argchain.node)
517
+ par = sig.parameters[idx]
518
+ # @type [Solargraph::Source::Chain]
519
+ argchain = kwargs[par.name.to_sym]
520
+ if par.decl == :kwrestarg || (par.decl == :optarg && idx == pin.parameters.length - 1 && par.asgn_code == '{}')
521
+ result.concat kwrestarg_problems_for(api_map, closure_pin, locals, location, pin, params, kwargs)
522
+ elsif argchain
523
+ data = params[par.name]
524
+ if data.nil?
525
+ # @todo Some level (strong, I guess) should require the param here
526
+ else
527
+ # @type [ComplexType, ComplexType::UniqueType]
528
+ ptype = data[:qualified]
529
+ ptype = ptype.self_to_type(pin.context)
530
+ unless ptype.undefined?
531
+ # @type [ComplexType]
532
+ argtype = argchain.infer(api_map, closure_pin, locals).self_to_type(closure_pin.context)
533
+ # @todo Unresolved call to defined?
534
+ if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype)
535
+ result.push Problem.new(location,
536
+ "Wrong argument type for #{pin.path}: #{par.name} expected #{ptype}, received #{argtype}")
537
+ end
538
+ end
539
+ end
540
+ elsif par.decl == :kwarg
541
+ result.push Problem.new(location, "Call to #{pin.path} is missing keyword argument #{par.name}")
542
+ end
543
+ result
544
+ end
545
+
546
+ # @param api_map [ApiMap]
547
+ # @param closure_pin [Pin::Closure]
548
+ # @param locals [Array<Pin::LocalVariable>]
549
+ # @param location [Location]
550
+ # @param pin [Pin::Method]
551
+ # @param params [Hash{String => nil, Hash}]
552
+ # @param kwargs [Hash{Symbol => Source::Chain}]
553
+ # @return [Array<Problem>]
554
+ def kwrestarg_problems_for api_map, closure_pin, locals, location, pin, params, kwargs
555
+ result = []
556
+ kwargs.each_pair do |pname, argchain|
557
+ next unless params.key?(pname.to_s)
558
+ # @sg-ignore
559
+ # @type [ComplexType]
560
+ raw_ptype = params[pname.to_s][:qualified]
561
+ ptype = raw_ptype.self_to_type(pin.context)
562
+ argtype = argchain.infer(api_map, closure_pin, locals)
563
+ argtype = argtype.self_to_type(closure_pin.context)
564
+ if argtype.defined? && ptype && !arg_conforms_to?(argtype, ptype)
565
+ result.push Problem.new(location,
566
+ "Wrong argument type for #{pin.path}: #{pname} expected #{ptype}, received #{argtype}")
567
+ end
568
+ end
569
+ result
570
+ end
571
+
572
+ # @param param_details [Hash{String => Hash{Symbol => String, ComplexType}}]
573
+ # @param pin [Pin::Method, Pin::Signature]
574
+ # @param relevant_pin [Pin::Method, Pin::Signature] the pin which is under inspection
575
+ # @return [void]
576
+ def add_restkwarg_param_tag_details param_details, pin, relevant_pin
577
+ # see if we have additional tags to pay attention to from YARD -
578
+ # e.g., kwargs in a **restkwargs splat
579
+ tags = pin.docstring.tags(:param)
580
+ tags.each do |tag|
581
+ next if param_details.key? tag.name.to_s
582
+ next if tag.types.nil?
583
+ details = {
584
+ tagged: tag.types.join(', '),
585
+ qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, pin.full_context.namespace)
586
+ }
587
+ # don't complain about a param that didn't come from the pin we're looking at anyway
588
+ if details[:qualified].defined? ||
589
+ relevant_pin.parameter_names.include?(tag.name.to_s)
590
+ param_details[tag.name.to_s] = details
591
+ end
592
+ end
593
+ end
594
+
595
+ # @param pin [Pin::Signature]
596
+ # @return [Hash{String => Hash{Symbol => String, ComplexType}}]
597
+ def signature_param_details pin
598
+ # @type [Hash{String => Hash{Symbol => String, ComplexType}}]
599
+ result = {}
600
+ pin.parameters.each do |param|
601
+ type = param.typify(api_map)
602
+ next if type.nil? || type.undefined?
603
+ result[param.name.to_s] = {
604
+ tagged: type.tags,
605
+ qualified: type
606
+ }
607
+ end
608
+ # see if we have additional tags to pay attention to from YARD -
609
+ # e.g., kwargs in a **restkwargs splat
610
+ tags = pin.docstring.tags(:param)
611
+ tags.each do |tag|
612
+ next if result.key? tag.name.to_s
613
+ next if tag.types.nil?
614
+ result[tag.name.to_s] = {
615
+ tagged: tag.types.join(', '),
616
+ # @sg-ignore need to add a nil check here
617
+ qualified: Solargraph::ComplexType.try_parse(*tag.types).qualify(api_map, *pin.closure.gates)
618
+ }
619
+ end
620
+ result
621
+ end
622
+
623
+ # The original signature defines the parameters, but other
624
+ # signatures and method pins can help by adding type information
625
+ #
626
+ # @param param_details [Hash{String => Hash{Symbol => String, ComplexType}}]
627
+ # @param param_names [Array<String>]
628
+ # @param new_param_details [Hash{String => Hash{Symbol => String, ComplexType}}]
629
+ #
630
+ # @return [void]
631
+ def add_to_param_details param_details, param_names, new_param_details
632
+ new_param_details.each do |param_name, details|
633
+ next unless param_names.include?(param_name)
634
+
635
+ param_details[param_name] ||= {}
636
+ param_details[param_name][:tagged] ||= details[:tagged]
637
+ param_details[param_name][:qualified] ||= details[:qualified]
638
+ end
639
+ end
640
+
641
+ # @param signature [Pin::Signature]
642
+ # @param method_pin_stack [Array<Pin::Method>]
643
+ # @return [Hash{String => Hash{Symbol => String, ComplexType}}]
644
+ def param_details_from_stack signature, method_pin_stack
645
+ signature_type = signature.typify(api_map)
646
+ signature = signature.proxy signature_type
647
+ param_details = signature_param_details(signature)
648
+ param_names = signature.parameter_names
649
+
650
+ method_pin_stack.each do |method_pin|
651
+ add_restkwarg_param_tag_details(param_details, method_pin, signature)
652
+
653
+ # documentation of types in superclasses should fail back to
654
+ # subclasses if the subclass hasn't documented something
655
+ method_pin.signatures.each do |sig|
656
+ add_restkwarg_param_tag_details(param_details, sig, signature)
657
+ add_to_param_details param_details, param_names, signature_param_details(sig)
658
+ end
659
+ end
660
+ param_details
661
+ end
662
+
663
+ # @param pin [Pin::Base]
664
+ def internal? pin
665
+ return false if pin.nil?
666
+ # @sg-ignore flow sensitive typing needs to handle attrs
667
+ pin.location && api_map.bundled?(pin.location.filename)
668
+ end
669
+
670
+ # True if the pin is either internal (part of the workspace) or from the core/stdlib
671
+ # @param pin [Pin::Base]
672
+ def internal_or_core? pin
673
+ # @todo RBS pins are not necessarily core/stdlib pins
674
+ internal?(pin) || pin.source == :rbs
675
+ end
676
+
677
+ # @param pin [Pin::Base]
678
+ def external? pin
679
+ !internal? pin
680
+ end
681
+
682
+ # @param pin [Pin::BaseVariable]
683
+ def declared_externally? pin
684
+ raise 'No assignment found' if pin.assignment.nil?
685
+
686
+ chain = Solargraph::Parser.chain(pin.assignment, filename)
687
+ # @sg-ignore flow sensitive typing needs to handle attrs
688
+ rng = Solargraph::Range.from_node(pin.assignment)
689
+ # @sg-ignore Need to add nil check here
690
+ closure_pin = source_map.locate_closure_pin(rng.start.line, rng.start.column)
691
+ # @sg-ignore flow sensitive typing needs to handle "if foo.nil?"
692
+ location = Location.new(filename, Range.from_node(pin.assignment))
693
+ locals = source_map.locals_at(location)
694
+ type = chain.infer(api_map, closure_pin, locals)
695
+ if type.undefined? && !rules.ignore_all_undefined?
696
+ base = chain
697
+ # @type [Solargraph::Pin::Base, nil]
698
+ found = nil
699
+ # @type [Array<Solargraph::Pin::Base>]
700
+ all_found = []
701
+ until base.links.first.undefined?
702
+ all_found = base.define(api_map, closure_pin, locals)
703
+ found = all_found.first
704
+ break if found
705
+ base = base.base
706
+ end
707
+ all_closest = all_found.map { |pin| pin.typify(api_map) }
708
+ closest = ComplexType.new(all_closest.flat_map(&:items).uniq)
709
+ return false if !found || closest.defined? || internal?(found)
710
+ end
711
+ true
712
+ end
713
+
714
+ # @param pin [Pin::Method]
715
+ # @param arguments [Array<Source::Chain>]
716
+ # @param location [Location]
717
+ # @return [Array<Problem>]
718
+ def arity_problems_for pin, arguments, location
719
+ results = pin.signatures.map do |sig|
720
+ r = parameterized_arity_problems_for(pin, sig.parameters, arguments, location)
721
+ return [] if r.empty?
722
+ r
723
+ end
724
+ results.first
725
+ end
726
+
727
+ # @param pin [Pin::Method]
728
+ # @param parameters [Array<Pin::Parameter>]
729
+ # @param arguments [Array<Source::Chain>]
730
+ # @param location [Location]
731
+ # @return [Array<Problem>]
732
+ def parameterized_arity_problems_for pin, parameters, arguments, location
733
+ return [] unless pin.explicit?
734
+ return [] if parameters.empty? && arguments.empty?
735
+ return [] if pin.anon_splat?
736
+ unchecked = arguments.dup # creates copy of and unthaws array
737
+ add_params = 0
738
+ if unchecked.empty? && parameters.any? { |param| param.decl == :kwarg }
739
+ return [Problem.new(location, "Missing keyword arguments to #{pin.path}")]
740
+ end
741
+ settled_kwargs = 0
742
+ unless unchecked.empty?
743
+ if any_splatted_call?(unchecked.map(&:node))
744
+ settled_kwargs = parameters.count(&:keyword?)
745
+ else
746
+ kwargs = convert_hash(unchecked.last.node)
747
+ if parameters.any? { |param| %i[kwarg kwoptarg].include?(param.decl) || param.kwrestarg? }
748
+ if kwargs.empty?
749
+ add_params += 1
750
+ else
751
+ unchecked.pop
752
+ parameters.each do |param|
753
+ next unless param.keyword?
754
+ if kwargs.key?(param.name.to_sym)
755
+ kwargs.delete param.name.to_sym
756
+ settled_kwargs += 1
757
+ elsif param.decl == :kwarg
758
+ last_arg_last_link = arguments.last.links.last
759
+ return [] if last_arg_last_link.is_a?(Solargraph::Source::Chain::Hash) && last_arg_last_link.splatted?
760
+ return [Problem.new(location, "Missing keyword argument #{param.name} to #{pin.path}")]
761
+ end
762
+ end
763
+ kwargs.clear if parameters.any?(&:kwrestarg?)
764
+ unless kwargs.empty?
765
+ return [Problem.new(location, "Unrecognized keyword argument #{kwargs.keys.first} to #{pin.path}")]
766
+ end
767
+ end
768
+ end
769
+ end
770
+ end
771
+ req = required_param_count(parameters)
772
+ if req + add_params < unchecked.length
773
+ return [] if parameters.any?(&:rest?)
774
+ opt = optional_param_count(parameters)
775
+ return [] if unchecked.length <= req + opt
776
+ if req + add_params + 1 == unchecked.length && any_splatted_call?(unchecked.map(&:node)) && (parameters.map(&:decl) & %i[
777
+ kwarg kwoptarg kwrestarg
778
+ ]).any?
779
+ return []
780
+ end
781
+ return [] if arguments.length - req == parameters.select { |p| %i[optarg kwoptarg].include?(p.decl) }.length
782
+ return [Problem.new(location, "Too many arguments to #{pin.path}")]
783
+ elsif unchecked.length < req - settled_kwargs && (arguments.empty? || (!arguments.last.splat? && !arguments.last.links.last.is_a?(Solargraph::Source::Chain::Hash)))
784
+ # HACK: Kernel#raise signature is incorrect in Ruby 2.7 core docs.
785
+ # See https://github.com/castwide/solargraph/issues/418
786
+ unless arguments.empty? && pin.path == 'Kernel#raise'
787
+ return [Problem.new(location, "Not enough arguments to #{pin.path}")]
788
+ end
789
+ end
790
+ []
791
+ end
792
+
793
+ # @param parameters [Enumerable<Pin::Parameter>]
794
+ # @todo need to use generic types in method to choose correct
795
+ # signature and generate Integer as return type
796
+ # @return [Integer]
797
+ def required_param_count parameters
798
+ parameters.sum { |param| %i[arg kwarg].include?(param.decl) ? 1 : 0 }
799
+ end
800
+
801
+ # @param parameters [Enumerable<Pin::Parameter>]
802
+ #
803
+ # @return [Integer]
804
+ def optional_param_count parameters
805
+ parameters.select { |p| p.decl == :optarg }.length
806
+ end
807
+
808
+ # @param pin [Pin::Method]
809
+ def abstract? pin
810
+ pin.docstring.has_tag?('abstract') ||
811
+ pin.closure&.docstring&.has_tag?('abstract')
812
+ end
813
+
814
+ # @param pin [Pin::Method]
815
+ # @return [Array<Source::Chain>]
816
+ def fake_args_for pin
817
+ args = []
818
+ with_opts = false
819
+ with_block = false
820
+ # @param pin [Pin::Parameter]
821
+ pin.parameters.each do |pin|
822
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
823
+ if %i[kwarg kwoptarg kwrestarg].include?(pin.decl)
824
+ with_opts = true
825
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
826
+ elsif pin.decl == :block
827
+ with_block = true
828
+ # @sg-ignore flow sensitive typing should be able to handle redefinition
829
+ elsif pin.decl == :restarg
830
+ args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)], nil, true)
831
+ else
832
+ args.push Solargraph::Source::Chain.new([Solargraph::Source::Chain::Variable.new(pin.name)])
833
+ end
834
+ end
835
+ pin_location = pin.location
836
+ starting_line = pin_location ? pin_location.range.start.line : 0
837
+ args.push Solargraph::Parser.chain_string('{}', filename, starting_line) if with_opts
838
+ args.push Solargraph::Parser.chain_string('&', filename, starting_line) if with_block
839
+ args
840
+ end
841
+
842
+ # @return [Set<Integer>]
843
+ def sg_ignore_lines_processed
844
+ @sg_ignore_lines_processed ||= Set.new
845
+ end
846
+
847
+ # @return [Set<Integer>]
848
+ def all_sg_ignore_lines
849
+ source.associated_comments.select do |_line, text|
850
+ # @sg-ignore Need to add nil check here
851
+ text.include?('@sg-ignore')
852
+ end.keys.to_set
853
+ end
854
+
855
+ # @return [Array<Integer>]
856
+ def unprocessed_sg_ignore_lines
857
+ (all_sg_ignore_lines - sg_ignore_lines_processed).to_a.sort
858
+ end
859
+
860
+ # @return [Array<Problem>]
861
+ def unneeded_sgignore_problems
862
+ return [] unless rules.validate_sg_ignores?
863
+
864
+ unprocessed_sg_ignore_lines.map do |line|
865
+ Problem.new(
866
+ Location.new(filename, Range.from_to(line, 0, line, 0)),
867
+ 'Unneeded @sg-ignore comment'
868
+ )
869
+ end
870
+ end
871
+
872
+ # @param problems [Array<Problem>]
873
+ # @return [Array<Problem>]
874
+ def without_ignored problems
875
+ problems.reject do |problem|
876
+ node = source.node_at(problem.location.range.start.line, problem.location.range.start.column)
877
+ ignored = node && source.comments_for(node)&.include?('@sg-ignore')
878
+ unless !ignored || all_sg_ignore_lines.include?(problem.location.range.start.line)
879
+ # :nocov:
880
+ Solargraph.assert_or_log(:sg_ignore) { "@sg-ignore accounting issue - node is #{node}" }
881
+ # :nocov:
882
+ end
883
+ sg_ignore_lines_processed.add problem.location.range.start.line if ignored
884
+ ignored
885
+ end
886
+ end
887
+ end
888
+ end