solargraph-rspec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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