solargraph-rspec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walker_base'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ # A corrector of RSpec parsed pins by Solargraph
9
+ class ContextBlockNamespaceCorrector < WalkerBase
10
+ # @param source_map [Solargraph::SourceMap]
11
+ def correct(source_map)
12
+ rspec_walker.on_each_context_block do |namespace_name, ast|
13
+ original_block_pin = source_map.locate_block_pin(ast.location.begin.line, ast.location.begin.column)
14
+ original_block_pin_index = source_map.pins.index(original_block_pin)
15
+ location = Util.build_location(ast, source_map.filename)
16
+
17
+ # Define a dynamic module for the example group block
18
+ # Example:
19
+ # RSpec.describe Foo::Bar do # => module RSpec::ExampleGroups::FooBar
20
+ # context 'some context' do # => module RSpec::ExampleGroups::FooBar::SomeContext
21
+ # end
22
+ # end
23
+ namespace_pin = Solargraph::Pin::Namespace.new(
24
+ name: namespace_name,
25
+ location: location
26
+ )
27
+
28
+ fixed_namespace_block_pin = Solargraph::Pin::Block.new(
29
+ closure: namespace_pin,
30
+ location: original_block_pin.location,
31
+ receiver: original_block_pin.receiver,
32
+ scope: original_block_pin.scope
33
+ )
34
+
35
+ source_map.pins[original_block_pin_index] = fixed_namespace_block_pin
36
+
37
+ # Include DSL methods in the example group block
38
+ # TOOD: This does not work on solagraph! Class methods are not included from parent class.
39
+ namespace_extend_pin = Util.build_module_extend(
40
+ namespace_pin,
41
+ root_example_group_namespace_pin.name,
42
+ location
43
+ )
44
+
45
+ # Include parent example groups to share let definitions
46
+ parent_namespace_name = namespace_name.split('::')[0..-2].join('::')
47
+ namespace_include_pin = Util.build_module_include(
48
+ namespace_pin,
49
+ parent_namespace_name,
50
+ location
51
+ )
52
+
53
+ namespace_pins << namespace_pin
54
+ if block_given?
55
+ yield [
56
+ namespace_include_pin,
57
+ namespace_extend_pin
58
+ ]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walker_base'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ class DescribedClassCorrector < WalkerBase
9
+ # @param source_map [Solargraph::SourceMap]
10
+ # @return [void]
11
+ def correct(_source_map)
12
+ rspec_walker.on_described_class do |ast, described_class_name|
13
+ namespace_pin = closest_namespace_pin(namespace_pins, ast.loc.line)
14
+ next unless namespace_pin
15
+
16
+ described_class_pin = rspec_described_class_method(namespace_pin, ast, described_class_name)
17
+ yield [described_class_pin].compact if block_given?
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # @param namespace [Pin::Namespace]
24
+ # @param ast [Parser::AST::Node]
25
+ # @param described_class_name [String]
26
+ # @return [Pin::Method, nil]
27
+ def rspec_described_class_method(namespace, ast, described_class_name)
28
+ Util.build_public_method(
29
+ namespace,
30
+ 'described_class',
31
+ types: ["Class<#{described_class_name}>"],
32
+ location: Util.build_location(ast, namespace.filename),
33
+ scope: :instance
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walker_base'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ class DslMethodsCorrector < WalkerBase
9
+ # @param namespace_pins [Array<Solargraph::Pin::Base>]
10
+ # @param rspec_walker [Solargraph::Rspec::SpecWalker]
11
+ # @param config [Solargraph::Rspec::Config]
12
+ def initialize(namespace_pins:, rspec_walker:, config:)
13
+ super(namespace_pins: namespace_pins, rspec_walker: rspec_walker)
14
+ @config = config
15
+ end
16
+
17
+ # @param source_map [Solargraph::SourceMap]
18
+ # @return [void]
19
+ def correct(_source_map)
20
+ rspec_walker.after_walk do
21
+ if block_given?
22
+ yield namespace_pins.flat_map { |namespace_pin| add_context_dsl_methods(namespace_pin) }
23
+ yield namespace_pins.flat_map { |namespace_pin| add_methods_with_example_binding(namespace_pin) }
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # @return [Solargraph::Rspec::Config]
31
+ attr_reader :config
32
+
33
+ # RSpec executes example and hook blocks (eg. it, before, after)in the context of the example group.
34
+ # @yieldsef changes the binding of the block to correct class.
35
+ # @return [Array<Solargraph::Pin::Method>]
36
+ def add_methods_with_example_binding(namespace_pin)
37
+ rspec_context_block_methods.map do |method|
38
+ Util.build_public_method(
39
+ namespace_pin,
40
+ method.to_s,
41
+ comments: ["@yieldself [#{namespace_pin.path}]"], # Fixes the binding of the block to the correct class
42
+ scope: :class
43
+ )
44
+ end
45
+ end
46
+
47
+ # TODO: DSL methods should be defined once in the root example group and extended to all example groups.
48
+ # Fix this once Solargraph supports extending class methods.
49
+ # @param namespace_pin [Solargraph::Pin::Base]
50
+ # @return [Array<Solargraph::Pin::Base>]
51
+ def add_context_dsl_methods(namespace_pin)
52
+ Rspec::CONTEXT_METHODS.map do |method|
53
+ Util.build_public_method(
54
+ namespace_pin,
55
+ method.to_s,
56
+ scope: :class
57
+ )
58
+ end
59
+ end
60
+
61
+ # @return [Array<String>]
62
+ def rspec_context_block_methods
63
+ config.let_methods + Rspec::HOOK_METHODS + Rspec::EXAMPLE_METHODS
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walker_base'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ # Sets the correct namespace binding for example group blocks (it, example, etc.) and
9
+ # hook blocks (before, after, around)
10
+ class ExampleAndHookBlocksBindingCorrector < WalkerBase
11
+ # @param source_map [Solargraph::SourceMap]
12
+ # @return [void]
13
+ def correct(source_map)
14
+ rspec_walker.on_example_block do |block_ast|
15
+ bind_closest_namespace(block_ast, source_map)
16
+
17
+ yield [] if block_given?
18
+ end
19
+
20
+ rspec_walker.on_hook_block do |block_ast|
21
+ bind_closest_namespace(block_ast, source_map)
22
+
23
+ yield [] if block_given?
24
+ end
25
+
26
+ rspec_walker.on_let_method do |let_method_ast|
27
+ bind_closest_namespace(let_method_ast, source_map)
28
+
29
+ yield [] if block_given?
30
+ end
31
+
32
+ rspec_walker.on_blocks_in_examples do |block_ast|
33
+ bind_closest_namespace(block_ast, source_map)
34
+
35
+ yield [] if block_given?
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @param block_ast [Parser::AST::Node]
42
+ # @param source_map [Solargraph::SourceMap]
43
+ # @return [void]
44
+ def bind_closest_namespace(block_ast, source_map)
45
+ namespace_pin = closest_namespace_pin(namespace_pins, block_ast.loc.line)
46
+ return unless namespace_pin
47
+
48
+ original_block_pin = source_map.locate_block_pin(block_ast.location.begin.line,
49
+ block_ast.location.begin.column)
50
+ original_block_pin_index = source_map.pins.index(original_block_pin)
51
+ fixed_namespace_block_pin = Solargraph::Pin::Block.new(
52
+ closure: example_run_method(namespace_pin),
53
+ location: original_block_pin.location,
54
+ receiver: original_block_pin.receiver,
55
+ scope: original_block_pin.scope
56
+ )
57
+
58
+ source_map.pins[original_block_pin_index] = fixed_namespace_block_pin
59
+ end
60
+
61
+ # @param namespace_pin [Solargraph::Pin::Namespace]
62
+ # @return [Solargraph::Pin::Method]
63
+ def example_run_method(namespace_pin)
64
+ Util.build_public_method(
65
+ namespace_pin,
66
+ 'run',
67
+ # https://github.com/rspec/rspec-core/blob/main/lib/rspec/core/example.rb#L246
68
+ location: Solargraph::Location.new('lib/rspec/core/example.rb', Solargraph::Range.from_to(246, 1, 297, 1))
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'let_methods_corrector'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ # Defines let-like methods in the example group block
9
+ class ImplicitSubjectMethodCorrector < Base
10
+ # @return [Pin::Method]
11
+ attr_reader :described_class_pin
12
+
13
+ # @param namespace_pins [Array<Pin::Namespace>]
14
+ # @param described_class_pin [Pin::Method]
15
+ def initialize(namespace_pins:, described_class_pin:)
16
+ super(namespace_pins: namespace_pins)
17
+
18
+ @described_class_pin = described_class_pin
19
+ end
20
+
21
+ # @param source_map [Solargraph::SourceMap]
22
+ # @return [void]
23
+ def correct(_source_map)
24
+ namespace_pin = closest_namespace_pin(namespace_pins, described_class_pin.location.range.start.line)
25
+
26
+ yield [implicit_subject_pin(described_class_pin, namespace_pin)] if block_given? && namespace_pin
27
+ end
28
+
29
+ private
30
+
31
+ # @param described_class_pin [Pin::Method]
32
+ # @param namespace_pin [Pin::Namespace]
33
+ # @return [Pin::Method]
34
+ def implicit_subject_pin(described_class_pin, namespace_pin)
35
+ described_class = described_class_pin.return_type.first.subtypes.first.name
36
+
37
+ Util.build_public_method(
38
+ namespace_pin,
39
+ 'subject',
40
+ types: [described_class],
41
+ location: described_class_pin.location,
42
+ scope: :instance
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walker_base'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ # Defines let-like methods in the example group block
9
+ class LetMethodsCorrector < WalkerBase
10
+ # @param source_map [Solargraph::SourceMap]
11
+ # @return [void]
12
+ def correct(_source_map)
13
+ rspec_walker.on_let_method do |ast|
14
+ namespace_pin = closest_namespace_pin(namespace_pins, ast.loc.line)
15
+ next unless namespace_pin
16
+
17
+ pin = rspec_let_method(namespace_pin, ast)
18
+ yield [pin] if block_given?
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # @param namespace [Pin::Namespace]
25
+ # @param ast [Parser::AST::Node]
26
+ # @param types [Array<String>, nil]
27
+ # @return [Pin::Method, nil]
28
+ def rspec_let_method(namespace, ast, types: nil)
29
+ return unless ast.children
30
+ return unless ast.children[2]&.children
31
+
32
+ method_name = ast.children[2].children[0]&.to_s or return
33
+ Util.build_public_method(
34
+ namespace,
35
+ method_name,
36
+ types: types,
37
+ location: Util.build_location(ast, namespace.filename),
38
+ scope: :instance
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'let_methods_corrector'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ module Correctors
8
+ # Defines let-like methods in the example group block
9
+ class SubjectMethodCorrector < LetMethodsCorrector
10
+ # @param source_map [Solargraph::SourceMap]
11
+ # @return [void]
12
+ def correct(_source_map)
13
+ rspec_walker.on_subject do |ast|
14
+ namespace_pin = closest_namespace_pin(namespace_pins, ast.loc.line)
15
+ next unless namespace_pin
16
+
17
+ subject_pin = rspec_let_method(namespace_pin, ast)
18
+ yield [subject_pin].compact if block_given?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # A corrector that walks through RSpec AST nodes and corrects them
6
+ module Solargraph
7
+ module Rspec
8
+ module Correctors
9
+ # A corrector of RSpec parsed pins by Solargraph
10
+ # @abstract
11
+ class WalkerBase < Base
12
+ # @return [Array<Solargraph::Pin::Namespace>]
13
+ attr_reader :namespace_pins
14
+
15
+ # @return [Solargraph::Rspec::SpecWalker]
16
+ attr_reader :rspec_walker
17
+
18
+ # @param namespace_pins [Array<Solargraph::Pin::Base>]
19
+ # @param rspec_walker [Solargraph::Rspec::SpecWalker]
20
+ def initialize(namespace_pins:, rspec_walker:)
21
+ super(namespace_pins: namespace_pins)
22
+ @rspec_walker = rspec_walker
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'walker'
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ class SpecWalker
8
+ # @param source_map [SourceMap]
9
+ # @param config [Config]
10
+ def initialize(source_map:, config:)
11
+ @source_map = source_map
12
+ @config = config
13
+ @walker = Rspec::Walker.from_source(source_map.source)
14
+ @handlers = {
15
+ on_described_class: [],
16
+ on_let_method: [],
17
+ on_subject: [],
18
+ on_each_context_block: [],
19
+ on_example_block: [],
20
+ on_hook_block: [],
21
+ on_blocks_in_examples: [],
22
+ after_walk: []
23
+ }
24
+ end
25
+
26
+ # @return [Walker]
27
+ attr_reader :walker
28
+
29
+ # @return [Config]
30
+ attr_reader :config
31
+
32
+ # @param block [Proc]
33
+ # @return [void]
34
+ def on_described_class(&block)
35
+ @handlers[:on_described_class] << block
36
+ end
37
+
38
+ # @param block [Proc]
39
+ # @return [void]
40
+ def on_let_method(&block)
41
+ @handlers[:on_let_method] << block
42
+ end
43
+
44
+ # @param block [Proc]
45
+ # @return [void]
46
+ def on_subject(&block)
47
+ @handlers[:on_subject] << block
48
+ end
49
+
50
+ # @param block [Proc]
51
+ # @return [void]
52
+ def on_each_context_block(&block)
53
+ @handlers[:on_each_context_block] << block
54
+ end
55
+
56
+ #
57
+ # @param block [Proc]
58
+ # @return [void]
59
+ def on_example_block(&block)
60
+ @handlers[:on_example_block] << block
61
+ end
62
+
63
+ # @param block [Proc]
64
+ # @return [void]
65
+ def on_hook_block(&block)
66
+ @handlers[:on_hook_block] << block
67
+ end
68
+
69
+ # @param block [Proc]
70
+ # @return [void]
71
+ def on_blocks_in_examples(&block)
72
+ @handlers[:on_blocks_in_examples] << block
73
+ end
74
+
75
+ # @param block [Proc]
76
+ # @return [void]
77
+ def after_walk(&block)
78
+ @handlers[:after_walk] << block
79
+ end
80
+
81
+ # @return [void]
82
+ def walk!
83
+ each_context_block(@walker.ast, Rspec::ROOT_NAMESPACE) do |namespace_name, ast|
84
+ @handlers[:on_each_context_block].each do |handler|
85
+ handler.call(namespace_name, ast)
86
+ end
87
+ end
88
+
89
+ rspec_const = ::Parser::AST::Node.new(:const, [nil, :RSpec])
90
+ walker.on :send, [rspec_const, :describe, :any] do |ast|
91
+ @handlers[:on_described_class].each do |handler|
92
+ class_ast = ast.children[2]
93
+ next unless class_ast
94
+
95
+ class_name = full_constant_name(class_ast)
96
+ handler.call(class_ast, class_name)
97
+ end
98
+ end
99
+
100
+ config.let_methods.each do |let_method|
101
+ walker.on :send, [nil, let_method] do |ast|
102
+ @handlers[:on_let_method].each do |handler|
103
+ handler.call(ast)
104
+ end
105
+ end
106
+ end
107
+
108
+ walker.on :send, [nil, :subject] do |ast|
109
+ @handlers[:on_subject].each do |handler|
110
+ handler.call(ast)
111
+ end
112
+ end
113
+
114
+ walker.on :block do |block_ast|
115
+ next if block_ast.children.first.type != :send
116
+
117
+ method_ast = block_ast.children.first
118
+ method_name = method_ast.children[1]
119
+ next unless Rspec::EXAMPLE_METHODS.include?(method_name.to_s)
120
+
121
+ @handlers[:on_example_block].each do |handler|
122
+ handler.call(block_ast)
123
+ end
124
+
125
+ # @param blocks_in_examples [Parser::AST::Node]
126
+ each_block(block_ast.children[2]) do |blocks_in_examples|
127
+ @handlers[:on_blocks_in_examples].each do |handler|
128
+ handler.call(blocks_in_examples)
129
+ end
130
+ end
131
+ end
132
+
133
+ walker.on :block do |block_ast|
134
+ next if block_ast.children.first.type != :send
135
+
136
+ method_ast = block_ast.children.first
137
+ method_name = method_ast.children[1]
138
+ next unless Rspec::HOOK_METHODS.include?(method_name.to_s)
139
+
140
+ @handlers[:on_hook_block].each do |handler|
141
+ handler.call(block_ast)
142
+ end
143
+
144
+ # @param blocks_in_examples [Parser::AST::Node]
145
+ each_block(block_ast.children[2]) do |blocks_in_examples|
146
+ @handlers[:on_blocks_in_examples].each do |handler|
147
+ handler.call(blocks_in_examples)
148
+ end
149
+ end
150
+ end
151
+
152
+ walker.walk
153
+
154
+ @handlers[:after_walk].each(&:call)
155
+ end
156
+
157
+ private
158
+
159
+ # @param ast [Parser::AST::Node]
160
+ # @param parent_result [Object]
161
+ def each_block(ast, parent_result = nil, &block)
162
+ return unless ast.is_a?(::Parser::AST::Node)
163
+
164
+ is_a_block = ast.type == :block && ast.children[0].type == :send
165
+
166
+ if is_a_block
167
+ result = block&.call(ast, parent_result)
168
+ parent_result = result if result
169
+ end
170
+
171
+ ast.children.each { |child| each_block(child, parent_result, &block) }
172
+ end
173
+
174
+ # Find all describe/context blocks in the AST.
175
+ # @param ast [Parser::AST::Node]
176
+ # @yield [String, Parser::AST::Node]
177
+ def each_context_block(ast, root_namespace = Rspec::ROOT_NAMESPACE, &block)
178
+ each_block(ast, root_namespace) do |block_ast, parent_namespace|
179
+ is_a_context = %i[describe context].include?(block_ast.children[0].children[1])
180
+
181
+ next unless is_a_context
182
+
183
+ description_node = block_ast.children[0].children[2]
184
+ block_name = rspec_describe_class_name(description_node)
185
+ next unless block_name
186
+
187
+ parent_namespace = namespace_name = "#{parent_namespace}::#{block_name}"
188
+ block&.call(namespace_name, block_ast)
189
+ next parent_namespace
190
+ end
191
+ end
192
+
193
+ # @param ast [Parser::AST::Node]
194
+ # @return [String, nil]
195
+ def rspec_describe_class_name(ast)
196
+ if ast.type == :str
197
+ string_to_const_name(ast)
198
+ elsif ast.type == :const
199
+ full_constant_name(ast).gsub('::', '')
200
+ else
201
+ Solargraph.logger.warn "[RSpec] Unexpected AST type #{ast.type}"
202
+ nil
203
+ end
204
+ end
205
+
206
+ # @param ast [Parser::AST::Node]
207
+ # @return [String]
208
+ def full_constant_name(ast)
209
+ raise 'Node is not a constant' unless ast.type == :const
210
+
211
+ name = ast.children[1].to_s
212
+ if ast.children[0].nil?
213
+ name
214
+ else
215
+ "#{full_constant_name(ast.children[0])}::#{name}"
216
+ end
217
+ end
218
+
219
+ # @see https://github.com/rspec/rspec-core/blob/1eeadce5aa7137ead054783c31ff35cbfe9d07cc/lib/rspec/core/example_group.rb#L862
220
+ # @param ast [Parser::AST::Node]
221
+ # @return [String]
222
+ def string_to_const_name(string_ast)
223
+ return unless string_ast.type == :str
224
+
225
+ name = string_ast.children[0]
226
+ return 'Anonymous'.dup if name.empty?
227
+
228
+ # Convert to CamelCase.
229
+ name = +" #{name}"
230
+ name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) do
231
+ match = ::Regexp.last_match[1]
232
+ match.upcase!
233
+ match
234
+ end
235
+
236
+ name.lstrip! # Remove leading whitespace
237
+ name.gsub!(/\W/, '') # JRuby, RBX and others don't like non-ascii in const names
238
+
239
+ # Ruby requires first const letter to be A-Z. Use `Nested`
240
+ # as necessary to enforce that.
241
+ name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1')
242
+
243
+ name
244
+ end
245
+ end
246
+ end
247
+ end