solargraph-rspec 0.1.1 β†’ 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46e92fa0735bc1e0b1129ce3c06208969dd8917a28e70286876d7f6307928e8c
4
- data.tar.gz: 718485f0ea53780cb3ee6d076cb5be112d160d1c39866c91334142d1abff515a
3
+ metadata.gz: 1f73a95cc73aa3ce52cff1e5e4e8e06509f3a59ccf6a3371b9087fad38e135b0
4
+ data.tar.gz: 221342fb0163f67fce1648aa4b14310b31907aacc9637def0b61e690b8ff750e
5
5
  SHA512:
6
- metadata.gz: '06580663bc8c5424219a171e4f61121d98d04dfe9048bfaa3595c7717bdd2a88b6dcd5055218cbf2fe3c50f7ade5a148811a7c23b780c86d9f0bae8ea554a236'
7
- data.tar.gz: 28ea8fe4be4f3402ca3079a747764013d6154adac0b114c2f9f23afe81599b403f0bb024856c3744ce7e8b64f80898eba210962cec68253e0edf25c90f3ce1d6
6
+ metadata.gz: 47f84a714dc459f67e354b68ff1c037e0ee9170a364db3fb4aa8e7a04cef1ce81952fade0a109a41de91f57b0b18201d3d8ab95b901f94de679357688bc9e41f
7
+ data.tar.gz: a5bff062303a4fd717315083944c19dbe1c21a94ee795e9f29d5e2971d574038bb75677c596645dacd5861f5f624a7a36a93347c48e443a549ab7decc387dae3
data/CHANGELOG.md CHANGED
@@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- -
10
+ ## [0.2.0] - 2024-05-20
11
+
12
+ ### Added
13
+
14
+ - `let` and `subject` type inference πŸš€ (Resolves: [Issue #1](https://github.com/lekemula/solargraph-rspec/issues/1))
15
+
16
+ ### Changed
17
+
18
+ - Migrate from `parser` gem to using ruby's built-in RubyVM::AbstractSyntaxTree ([see why](https://github.com/castwide/solargraph/issues/522#issuecomment-993016664))
19
+
20
+ ### Fixed
21
+
22
+ - Fix subject without name block completion: `subject { ... }`
23
+ - Fix subject return class overlap with `Rspec::ExampleGroups::` when class has no namespace
11
24
 
12
25
  ## [0.1.1] - 2024-05-13
13
26
 
data/README.md CHANGED
@@ -7,15 +7,18 @@
7
7
 
8
8
  RSpec is a testing framework of choice for many Ruby developers. But at the same time is highly dynamic and heavily relying on metaprogramming making it hard to provide accurate code completion and type inference.
9
9
 
10
- This gem aims to provide a better support for RSpec in Solargraph and it supports the following features:
10
+ This gem aims to provide better support for RSpec in Solargraph and it supports the following features:
11
11
  - `describe` and `it` methods completion
12
12
  - memoized `let` and `let!` methods completion
13
13
  - implicit and explicit `subject` methods
14
14
  - `described_class` with appropriate type inference
15
15
  - `RSpec::Matchers` methods completion
16
+ - type inference πŸš€
16
17
  - and more to come...
17
18
 
18
- ![solargraph-rspec_smaller](https://github.com/lekemula/solargraph-rspec/assets/9197495/66f86e5b-258e-4d8c-8a64-0685d15ece00)
19
+ ![solargraph-rspec-with-types](https://github.com/lekemula/solargraph-rspec/assets/9197495/077e74f8-a800-4e90-8922-fa5351adcda3)
20
+ ![solargraph-rspec-with-types-vs-code](https://github.com/lekemula/solargraph-rspec/assets/9197495/6a942460-256d-46ca-82de-9869a65309ec)
21
+
19
22
 
20
23
  ## Installation
21
24
 
@@ -10,7 +10,7 @@ require_relative 'correctors/subject_method_corrector'
10
10
  require_relative 'correctors/context_block_methods_corrector'
11
11
  require_relative 'correctors/implicit_subject_method_corrector'
12
12
  require_relative 'correctors/dsl_methods_corrector'
13
- require_relative 'util'
13
+ require_relative 'pin_factory'
14
14
 
15
15
  module Solargraph
16
16
  module Rspec
@@ -121,14 +121,20 @@ module Solargraph
121
121
 
122
122
  # @type [Pin::Method, nil]
123
123
  described_class_pin = nil
124
- Correctors::DescribedClassCorrector.new(namespace_pins: namespace_pins, rspec_walker: rspec_walker).correct(
124
+ Correctors::DescribedClassCorrector.new(
125
+ namespace_pins: namespace_pins,
126
+ rspec_walker: rspec_walker
127
+ ).correct(
125
128
  source_map
126
129
  ) do |pins_to_add|
127
130
  described_class_pin = pins_to_add.first
128
131
  pins += pins_to_add
129
132
  end
130
133
 
131
- Correctors::LetMethodsCorrector.new(namespace_pins: namespace_pins, rspec_walker: rspec_walker).correct(
134
+ Correctors::LetMethodsCorrector.new(
135
+ namespace_pins: namespace_pins,
136
+ rspec_walker: rspec_walker
137
+ ).correct(
132
138
  source_map
133
139
  ) do |pins_to_add|
134
140
  pins += pins_to_add
@@ -136,7 +142,10 @@ module Solargraph
136
142
 
137
143
  # @type [Pin::Method, nil]
138
144
  subject_pin = nil
139
- Correctors::SubjectMethodCorrector.new(namespace_pins: namespace_pins, rspec_walker: rspec_walker).correct(
145
+ Correctors::SubjectMethodCorrector.new(
146
+ namespace_pins: namespace_pins,
147
+ rspec_walker: rspec_walker
148
+ ).correct(
140
149
  source_map
141
150
  ) do |pins_to_add|
142
151
  subject_pin = pins_to_add.first
@@ -199,7 +208,7 @@ module Solargraph
199
208
  # @return [Array<Pin::Base>]
200
209
  def include_helper_pins(helper_modules: HELPER_MODULES)
201
210
  helper_modules.map do |helper_module|
202
- Util.build_module_include(
211
+ PinFactory.build_module_include(
203
212
  root_example_group_namespace_pin,
204
213
  helper_module,
205
214
  root_example_group_namespace_pin.location
@@ -216,7 +225,7 @@ module Solargraph
216
225
  def root_example_group_namespace_pin
217
226
  Solargraph::Pin::Namespace.new(
218
227
  name: ROOT_NAMESPACE,
219
- location: Util.dummy_location('lib/rspec/core/example_group.rb')
228
+ location: PinFactory.dummy_location('lib/rspec/core/example_group.rb')
220
229
  )
221
230
  end
222
231
  end
@@ -27,7 +27,7 @@ module Solargraph
27
27
  def root_example_group_namespace_pin
28
28
  Solargraph::Pin::Namespace.new(
29
29
  name: ROOT_NAMESPACE,
30
- location: Util.dummy_location('lib/rspec/core/example_group.rb')
30
+ location: PinFactory.dummy_location('lib/rspec/core/example_group.rb')
31
31
  )
32
32
  end
33
33
 
@@ -9,10 +9,11 @@ module Solargraph
9
9
  class ContextBlockNamespaceCorrector < WalkerBase
10
10
  # @param source_map [Solargraph::SourceMap]
11
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)
12
+ # @param location_range [Solargraph::Range]
13
+ rspec_walker.on_each_context_block do |namespace_name, location_range|
14
+ original_block_pin = source_map.locate_block_pin(location_range.start.line, location_range.start.column)
14
15
  original_block_pin_index = source_map.pins.index(original_block_pin)
15
- location = Util.build_location(ast, source_map.filename)
16
+ location = PinFactory.build_location(location_range, source_map.filename)
16
17
 
17
18
  # Define a dynamic module for the example group block
18
19
  # Example:
@@ -36,7 +37,7 @@ module Solargraph
36
37
 
37
38
  # Include DSL methods in the example group block
38
39
  # 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_extend_pin = PinFactory.build_module_extend(
40
41
  namespace_pin,
41
42
  root_example_group_namespace_pin.name,
42
43
  location
@@ -44,7 +45,7 @@ module Solargraph
44
45
 
45
46
  # Include parent example groups to share let definitions
46
47
  parent_namespace_name = namespace_name.split('::')[0..-2].join('::')
47
- namespace_include_pin = Util.build_module_include(
48
+ namespace_include_pin = PinFactory.build_module_include(
48
49
  namespace_pin,
49
50
  parent_namespace_name,
50
51
  location
@@ -9,11 +9,11 @@ module Solargraph
9
9
  # @param source_map [Solargraph::SourceMap]
10
10
  # @return [void]
11
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)
12
+ rspec_walker.on_described_class do |described_class_name, location_range|
13
+ namespace_pin = closest_namespace_pin(namespace_pins, location_range.start.line)
14
14
  next unless namespace_pin
15
15
 
16
- described_class_pin = rspec_described_class_method(namespace_pin, ast, described_class_name)
16
+ described_class_pin = rspec_described_class_method(namespace_pin, location_range, described_class_name)
17
17
  yield [described_class_pin].compact if block_given?
18
18
  end
19
19
  end
@@ -21,15 +21,15 @@ module Solargraph
21
21
  private
22
22
 
23
23
  # @param namespace [Pin::Namespace]
24
- # @param ast [Parser::AST::Node]
24
+ # @param location_range [Solargraph::Range]
25
25
  # @param described_class_name [String]
26
26
  # @return [Pin::Method, nil]
27
- def rspec_described_class_method(namespace, ast, described_class_name)
28
- Util.build_public_method(
27
+ def rspec_described_class_method(namespace, location_range, described_class_name)
28
+ PinFactory.build_public_method(
29
29
  namespace,
30
30
  'described_class',
31
- types: ["Class<#{described_class_name}>"],
32
- location: Util.build_location(ast, namespace.filename),
31
+ types: ["Class<::#{described_class_name}>"],
32
+ location: PinFactory.build_location(location_range, namespace.filename),
33
33
  scope: :instance
34
34
  )
35
35
  end
@@ -35,7 +35,7 @@ module Solargraph
35
35
  # @return [Array<Solargraph::Pin::Method>]
36
36
  def add_methods_with_example_binding(namespace_pin)
37
37
  rspec_context_block_methods.map do |method|
38
- Util.build_public_method(
38
+ PinFactory.build_public_method(
39
39
  namespace_pin,
40
40
  method.to_s,
41
41
  comments: ["@yieldself [#{namespace_pin.path}]"], # Fixes the binding of the block to the correct class
@@ -50,7 +50,7 @@ module Solargraph
50
50
  # @return [Array<Solargraph::Pin::Base>]
51
51
  def add_context_dsl_methods(namespace_pin)
52
52
  Rspec::CONTEXT_METHODS.map do |method|
53
- Util.build_public_method(
53
+ PinFactory.build_public_method(
54
54
  namespace_pin,
55
55
  method.to_s,
56
56
  scope: :class
@@ -11,32 +11,32 @@ module Solargraph
11
11
  # @param source_map [Solargraph::SourceMap]
12
12
  # @return [void]
13
13
  def correct(source_map)
14
- rspec_walker.on_example_block do |block_ast|
15
- bind_closest_namespace(block_ast, source_map)
14
+ rspec_walker.on_example_block do |location_range|
15
+ bind_closest_namespace(location_range, source_map)
16
16
 
17
17
  yield [] if block_given?
18
18
  end
19
19
 
20
- rspec_walker.on_hook_block do |block_ast|
21
- bind_closest_namespace(block_ast, source_map)
20
+ rspec_walker.on_hook_block do |location_range|
21
+ bind_closest_namespace(location_range, source_map)
22
22
 
23
23
  yield [] if block_given?
24
24
  end
25
25
 
26
- rspec_walker.on_let_method do |let_method_ast|
27
- bind_closest_namespace(let_method_ast, source_map)
26
+ rspec_walker.on_let_method do |_method_name, location_range|
27
+ bind_closest_namespace(location_range, source_map)
28
28
 
29
29
  yield [] if block_given?
30
30
  end
31
31
 
32
- rspec_walker.on_blocks_in_examples do |block_ast|
33
- bind_closest_namespace(block_ast, source_map)
32
+ rspec_walker.on_blocks_in_examples do |location_range|
33
+ bind_closest_namespace(location_range, source_map)
34
34
 
35
35
  yield [] if block_given?
36
36
  end
37
37
 
38
- rspec_walker.on_subject do |subject_ast|
39
- bind_closest_namespace(subject_ast, source_map)
38
+ rspec_walker.on_subject do |_method_name, location_range|
39
+ bind_closest_namespace(location_range, source_map)
40
40
 
41
41
  yield [] if block_given?
42
42
  end
@@ -44,15 +44,15 @@ module Solargraph
44
44
 
45
45
  private
46
46
 
47
- # @param block_ast [Parser::AST::Node]
47
+ # @param location_range [Solargraph::Range]
48
48
  # @param source_map [Solargraph::SourceMap]
49
49
  # @return [void]
50
- def bind_closest_namespace(block_ast, source_map)
51
- namespace_pin = closest_namespace_pin(namespace_pins, block_ast.loc.line)
50
+ def bind_closest_namespace(location_range, source_map)
51
+ namespace_pin = closest_namespace_pin(namespace_pins, location_range.start.line)
52
52
  return unless namespace_pin
53
53
 
54
- original_block_pin = source_map.locate_block_pin(block_ast.location.begin.line,
55
- block_ast.location.begin.column)
54
+ original_block_pin = source_map.locate_block_pin(location_range.start.line,
55
+ location_range.start.column)
56
56
  original_block_pin_index = source_map.pins.index(original_block_pin)
57
57
  fixed_namespace_block_pin = Solargraph::Pin::Block.new(
58
58
  closure: example_run_method(namespace_pin),
@@ -67,7 +67,7 @@ module Solargraph
67
67
  # @param namespace_pin [Solargraph::Pin::Namespace]
68
68
  # @return [Solargraph::Pin::Method]
69
69
  def example_run_method(namespace_pin)
70
- Util.build_public_method(
70
+ PinFactory.build_public_method(
71
71
  namespace_pin,
72
72
  'run',
73
73
  # https://github.com/rspec/rspec-core/blob/main/lib/rspec/core/example.rb#L246
@@ -34,10 +34,10 @@ module Solargraph
34
34
  def implicit_subject_pin(described_class_pin, namespace_pin)
35
35
  described_class = described_class_pin.return_type.first.subtypes.first.name
36
36
 
37
- Util.build_public_method(
37
+ PinFactory.build_public_method(
38
38
  namespace_pin,
39
39
  'subject',
40
- types: [described_class],
40
+ types: ["::#{described_class}"],
41
41
  location: described_class_pin.location,
42
42
  scope: :instance
43
43
  )
@@ -10,11 +10,11 @@ module Solargraph
10
10
  # @param source_map [Solargraph::SourceMap]
11
11
  # @return [void]
12
12
  def correct(_source_map)
13
- rspec_walker.on_let_method do |ast|
14
- namespace_pin = closest_namespace_pin(namespace_pins, ast.loc.line)
13
+ rspec_walker.on_let_method do |let_name, location_range, fake_method_ast|
14
+ namespace_pin = closest_namespace_pin(namespace_pins, location_range.start.line)
15
15
  next unless namespace_pin
16
16
 
17
- pin = rspec_let_method(namespace_pin, ast)
17
+ pin = rspec_let_method(namespace_pin, let_name, location_range, fake_method_ast)
18
18
  yield [pin] if block_given?
19
19
  end
20
20
  end
@@ -22,19 +22,17 @@ module Solargraph
22
22
  private
23
23
 
24
24
  # @param namespace [Pin::Namespace]
25
- # @param ast [Parser::AST::Node]
25
+ # @param method_name [String]
26
+ # @param node [RubyVM::AbstractSyntaxTree::Node]
26
27
  # @param types [Array<String>, nil]
27
28
  # @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(
29
+ def rspec_let_method(namespace, method_name, location_range, node = nil, types: nil)
30
+ PinFactory.build_public_method(
34
31
  namespace,
35
32
  method_name,
33
+ node: node,
36
34
  types: types,
37
- location: Util.build_location(ast, namespace.filename),
35
+ location: PinFactory.build_location(location_range, namespace.filename),
38
36
  scope: :instance
39
37
  )
40
38
  end
@@ -10,11 +10,13 @@ module Solargraph
10
10
  # @param source_map [Solargraph::SourceMap]
11
11
  # @return [void]
12
12
  def correct(_source_map)
13
- rspec_walker.on_subject do |ast|
14
- namespace_pin = closest_namespace_pin(namespace_pins, ast.loc.line)
13
+ rspec_walker.on_subject do |subject_name, location_range, fake_method_ast|
14
+ next unless subject_name
15
+
16
+ namespace_pin = closest_namespace_pin(namespace_pins, location_range.start.line)
15
17
  next unless namespace_pin
16
18
 
17
- subject_pin = rspec_let_method(namespace_pin, ast)
19
+ subject_pin = rspec_let_method(namespace_pin, subject_name, location_range, fake_method_ast)
18
20
  yield [subject_pin].compact if block_given?
19
21
  end
20
22
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Credits: This file is a copy of the file from the solargraph-rspec gem
4
+
5
+ module Solargraph
6
+ module Rspec
7
+ # Factory class for building pins and references.
8
+ module PinFactory
9
+ # @param namespace [Solargraph::Pin::Namespace]
10
+ # @param name [String]
11
+ # @param types [Array<String>]
12
+ # @param location [Solargraph::Location]
13
+ # @param comments [Array<String>]
14
+ # @param attribute [Boolean]
15
+ # @param scope [:instance, :class]
16
+ # @return [Solargraph::Pin::Method]
17
+ def self.build_public_method(
18
+ namespace,
19
+ name,
20
+ types: nil,
21
+ location: nil,
22
+ comments: [],
23
+ attribute: false,
24
+ scope: :instance,
25
+ node: nil
26
+ )
27
+ opts = {
28
+ name: name,
29
+ location: location,
30
+ closure: namespace,
31
+ scope: scope,
32
+ attribute: attribute,
33
+ comments: [],
34
+ node: node
35
+ }
36
+
37
+ comments << "@return [#{types.join(",")}]" if types
38
+
39
+ opts[:comments] = comments.join("\n")
40
+
41
+ Solargraph::Pin::Method.new(**opts)
42
+ end
43
+
44
+ # @param namespace [Solargraph::Pin::Namespace]
45
+ # @param name [String]
46
+ # @param location [Solargraph::Location]
47
+ # @return [Solargraph::Pin::Reference::Include]
48
+ def self.build_module_include(namespace, module_name, location)
49
+ Solargraph::Pin::Reference::Include.new(
50
+ closure: namespace,
51
+ name: module_name,
52
+ location: location
53
+ )
54
+ end
55
+
56
+ # @param namespace [Solargraph::Pin::Namespace]
57
+ # @param module_name [String]
58
+ # @param location [Solargraph::Location]
59
+ # @return [Solargraph::Pin::Reference::Extend]
60
+ def self.build_module_extend(namespace, module_name, location)
61
+ Solargraph::Pin::Reference::Extend.new(
62
+ closure: namespace,
63
+ name: module_name,
64
+ location: location
65
+ )
66
+ end
67
+
68
+ # @param path [String]
69
+ # @return [Solargraph::Location]
70
+ def self.dummy_location(path)
71
+ Solargraph::Location.new(
72
+ File.expand_path(path),
73
+ Solargraph::Range.from_to(0, 0, 0, 0)
74
+ )
75
+ end
76
+
77
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
78
+ # @see [RubyVM::AbstractSyntaxTree::NodeWrapper] - for why we need -1 for lineno
79
+ # @return [Solargraph::Range]
80
+ def self.build_location_range(ast)
81
+ Solargraph::Range.from_to(
82
+ ast.first_lineno - 1,
83
+ ast.first_column,
84
+ ast.last_lineno - 1,
85
+ ast.last_column
86
+ )
87
+ end
88
+
89
+ # @param location_range [Solargraph::Range]
90
+ # @param path [String]
91
+ # @return [Solargraph::Location]
92
+ def self.build_location(location_range, path)
93
+ Solargraph::Location.new(
94
+ File.expand_path(path),
95
+ location_range
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module Rspec
5
+ class SpecWalker
6
+ class FakeLetMethod
7
+ MATCH_BODY = Regexp.union(
8
+ /do(.*)end/m,
9
+ /{(.*)}/m
10
+ )
11
+
12
+ # @param block_ast [RubyVM::AbstractSyntaxTree::Node]
13
+ # @return [RubyVM::AbstractSyntaxTree::Node]
14
+ def self.transform_block(block_ast, code, method_name = nil)
15
+ method_name ||= NodeTypes.let_method_name(block_ast)
16
+ block_body = block_ast.children[1]
17
+ matches = code.lines[block_body.first_lineno - 1..block_body.last_lineno - 1].join.match(MATCH_BODY)
18
+ method_body = (matches[1] || matches[2]).strip
19
+
20
+ ast = RubyVM::AbstractSyntaxTree.parse <<~RUBY
21
+ def #{method_name}
22
+ #{method_body}
23
+ end
24
+ RUBY
25
+
26
+ ast.children[2]
27
+ rescue SyntaxError
28
+ raise "Failed to build fake let method: #{block_ast.inspect}, message: #{e.message}"
29
+ ensure
30
+ nil
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module Rspec
5
+ class SpecWalker
6
+ class FullConstantName
7
+ class << self
8
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
9
+ # @return [String]
10
+ def from_ast(ast)
11
+ raise 'Node is not a constant' unless NodeTypes.a_constant?(ast)
12
+
13
+ if ast.type == :CONST
14
+ ast.children[0].to_s
15
+ elsif ast.type == :COLON2
16
+ name = ast.children[1].to_s
17
+ "#{from_ast(ast.children[0])}::#{name}"
18
+ end
19
+ end
20
+
21
+ def from_context_block_ast(block_ast)
22
+ ast = NodeTypes.context_description_node(block_ast)
23
+ from_ast(ast)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module Rspec
5
+ class SpecWalker
6
+ class NodeTypes
7
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
8
+ # @return [Boolean]
9
+ def self.a_block?(ast)
10
+ return false unless ast.is_a?(RubyVM::AbstractSyntaxTree::Node)
11
+
12
+ %i[ITER LAMBDA].include?(ast.type)
13
+ end
14
+
15
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
16
+ # @return [Boolean]
17
+ def self.a_context_block?(block_ast)
18
+ Solargraph::Rspec::CONTEXT_METHODS.include?(method_with_block_name(block_ast))
19
+ end
20
+
21
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
22
+ # @return [Boolean]
23
+ def self.a_subject_block?(block_ast)
24
+ Solargraph::Rspec::SUBJECT_METHODS.include?(method_with_block_name(block_ast))
25
+ end
26
+
27
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
28
+ # @return [Boolean]
29
+ def self.a_example_block?(block_ast)
30
+ Solargraph::Rspec::EXAMPLE_METHODS.include?(method_with_block_name(block_ast))
31
+ end
32
+
33
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
34
+ # @param config [Config]
35
+ # @return [Boolean]
36
+ def self.a_let_block?(block_ast, config)
37
+ config.let_methods.map(&:to_s).include?(method_with_block_name(block_ast))
38
+ end
39
+
40
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
41
+ # @return [Boolean]
42
+ def self.a_hook_block?(block_ast)
43
+ Solargraph::Rspec::HOOK_METHODS.include?(method_with_block_name(block_ast))
44
+ end
45
+
46
+ def self.a_constant?(ast)
47
+ %i[CONST COLON2].include?(ast.type)
48
+ end
49
+
50
+ # @param block_ast [RubyVM::AbstractSyntaxTree::Node]
51
+ # @return [String, nil]
52
+ def self.method_with_block_name(block_ast)
53
+ return nil unless a_block?(block_ast)
54
+
55
+ method_call = %i[CALL FCALL].include?(block_ast.children[0].type)
56
+ return nil unless method_call
57
+
58
+ block_ast.children[0].children.select { |child| child.is_a?(Symbol) }.first&.to_s
59
+ end
60
+
61
+ # @param block_ast [RubyVM::AbstractSyntaxTree::Node]
62
+ # @return [RubyVM::AbstractSyntaxTree::Node]
63
+ def self.context_description_node(block_ast)
64
+ return nil unless a_context_block?(block_ast)
65
+
66
+ case block_ast.children[0].type
67
+ when :CALL # RSpec.describe "something" do end
68
+ block_ast.children[0].children[2].children[0]
69
+ when :FCALL # describe "something" do end
70
+ block_ast.children[0].children[1].children[0]
71
+ end
72
+ end
73
+
74
+ # @param block_ast [RubyVM::AbstractSyntaxTree::Node]
75
+ # @return [String]
76
+ def self.let_method_name(block_ast)
77
+ block_ast.children[0].children[1]&.children&.[](0)&.children&.[](0)&.to_s
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solargraph
4
+ module Rspec
5
+ class SpecWalker
6
+ class RspecContextNamespace
7
+ class << self
8
+ # @param block_ast [RubyVM::AbstractSyntaxTree::Node]
9
+ # @return [String, nil]
10
+ def from_block_ast(block_ast)
11
+ return unless block_ast.is_a?(RubyVM::AbstractSyntaxTree::Node)
12
+
13
+ ast = NodeTypes.context_description_node(block_ast)
14
+ if ast.type == :STR
15
+ string_to_const_name(ast)
16
+ elsif NodeTypes.a_constant?(ast)
17
+ FullConstantName.from_ast(ast).gsub('::', '')
18
+ else
19
+ Solargraph.logger.warn "[RSpec] Unexpected AST type #{ast.type}"
20
+ nil
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @see https://github.com/rspec/rspec-core/blob/1eeadce5aa7137ead054783c31ff35cbfe9d07cc/lib/rspec/core/example_group.rb#L862
27
+ # @param ast [Parser::AST::Node]
28
+ # @return [String]
29
+ def string_to_const_name(string_ast)
30
+ return unless string_ast.type == :STR
31
+
32
+ name = string_ast.children[0]
33
+ return 'Anonymous'.dup if name.empty?
34
+
35
+ # Convert to CamelCase.
36
+ name = +" #{name}"
37
+ name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) do
38
+ match = ::Regexp.last_match[1]
39
+ match.upcase!
40
+ match
41
+ end
42
+
43
+ name.lstrip! # Remove leading whitespace
44
+ name.gsub!(/\W/, '') # JRuby, RBX and others don't like non-ascii in const names
45
+
46
+ # Ruby requires first const letter to be A-Z. Use `Nested`
47
+ # as necessary to enforce that.
48
+ name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1')
49
+
50
+ name
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'walker'
4
+ require_relative 'spec_walker/node_types'
5
+ require_relative 'spec_walker/full_constant_name'
6
+ require_relative 'spec_walker/rspec_context_namespace'
7
+ require_relative 'spec_walker/fake_let_method'
4
8
 
5
9
  module Solargraph
6
10
  module Rspec
@@ -10,7 +14,7 @@ module Solargraph
10
14
  def initialize(source_map:, config:)
11
15
  @source_map = source_map
12
16
  @config = config
13
- @walker = Rspec::Walker.from_source(source_map.source)
17
+ @walker = Rspec::Walker.new(source_map.source.node)
14
18
  @handlers = {
15
19
  on_described_class: [],
16
20
  on_let_method: [],
@@ -30,24 +34,34 @@ module Solargraph
30
34
  attr_reader :config
31
35
 
32
36
  # @param block [Proc]
37
+ # @yieldparam class_name [String]
38
+ # @yieldparam location_range [Solargraph::Range]
33
39
  # @return [void]
34
40
  def on_described_class(&block)
35
41
  @handlers[:on_described_class] << block
36
42
  end
37
43
 
38
44
  # @param block [Proc]
45
+ # @yieldparam method_name [String]
46
+ # @yieldparam location_range [Solargraph::Range]
47
+ # @yieldparam fake_method_ast [RubyVM::AbstractSyntaxTree::Node]
39
48
  # @return [void]
40
49
  def on_let_method(&block)
41
50
  @handlers[:on_let_method] << block
42
51
  end
43
52
 
44
53
  # @param block [Proc]
54
+ # @yieldparam method_name [String]
55
+ # @yieldparam location_range [Solargraph::Range]
56
+ # @yieldparam fake_method_ast [RubyVM::AbstractSyntaxTree::Node]
45
57
  # @return [void]
46
58
  def on_subject(&block)
47
59
  @handlers[:on_subject] << block
48
60
  end
49
61
 
50
62
  # @param block [Proc]
63
+ # @yieldparam namespace_name [String]
64
+ # @yieldparam location_range [Solargraph::Range]
51
65
  # @return [void]
52
66
  def on_each_context_block(&block)
53
67
  @handlers[:on_each_context_block] << block
@@ -55,18 +69,21 @@ module Solargraph
55
69
 
56
70
  #
57
71
  # @param block [Proc]
72
+ # @yieldparam location_range [Solargraph::Range]
58
73
  # @return [void]
59
74
  def on_example_block(&block)
60
75
  @handlers[:on_example_block] << block
61
76
  end
62
77
 
63
78
  # @param block [Proc]
79
+ # @yieldparam location_range [Solargraph::Range]
64
80
  # @return [void]
65
81
  def on_hook_block(&block)
66
82
  @handlers[:on_hook_block] << block
67
83
  end
68
84
 
69
85
  # @param block [Proc]
86
+ # @yieldparam location_range [Solargraph::Range]
70
87
  # @return [void]
71
88
  def on_blocks_in_examples(&block)
72
89
  @handlers[:on_blocks_in_examples] << block
@@ -80,77 +97,72 @@ module Solargraph
80
97
 
81
98
  # @return [void]
82
99
  def walk!
83
- each_context_block(@walker.ast, Rspec::ROOT_NAMESPACE) do |namespace_name, ast|
100
+ each_context_block(@walker.ast, Rspec::ROOT_NAMESPACE) do |namespace_name, block_ast|
101
+ desc_node = NodeTypes.context_description_node(block_ast)
102
+
84
103
  @handlers[:on_each_context_block].each do |handler|
85
- handler.call(namespace_name, ast)
104
+ handler.call(namespace_name, PinFactory.build_location_range(block_ast))
86
105
  end
87
- end
88
106
 
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)
107
+ if NodeTypes.a_constant?(desc_node) # rubocop:disable Style/Next
108
+ @handlers[:on_described_class].each do |handler|
109
+ class_name_ast = NodeTypes.context_description_node(block_ast)
110
+ class_name = FullConstantName.from_ast(class_name_ast)
111
+ handler.call(class_name, PinFactory.build_location_range(class_name_ast))
112
+ end
97
113
  end
98
114
  end
99
115
 
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
116
+ walker.on :ITER do |block_ast|
117
+ next unless NodeTypes.a_let_block?(block_ast, config)
118
+
119
+ method_name = NodeTypes.let_method_name(block_ast)
120
+ next unless method_name
121
+
122
+ fake_method_ast = FakeLetMethod.transform_block(block_ast, @source_map.source.code)
123
+
124
+ @handlers[:on_let_method].each do |handler|
125
+ handler.call(method_name, PinFactory.build_location_range(block_ast.children[0]), fake_method_ast)
105
126
  end
106
127
  end
107
128
 
108
- walker.on :block do |block_ast|
109
- next if block_ast.children.first.type != :send
129
+ walker.on :ITER do |block_ast|
130
+ next unless NodeTypes.a_subject_block?(block_ast)
110
131
 
111
- method_ast = block_ast.children.first
112
- method_name = method_ast.children[1]
113
- next unless Rspec::SUBJECT_METHODS.include?(method_name.to_s)
132
+ method_name = NodeTypes.let_method_name(block_ast)
133
+ fake_method_ast = FakeLetMethod.transform_block(block_ast, @source_map.source.code, method_name || 'subject')
114
134
 
115
135
  @handlers[:on_subject].each do |handler|
116
- handler.call(method_ast)
136
+ handler.call(method_name, PinFactory.build_location_range(block_ast.children[0]), fake_method_ast)
117
137
  end
118
138
  end
119
139
 
120
- walker.on :block do |block_ast|
121
- next if block_ast.children.first.type != :send
122
-
123
- method_ast = block_ast.children.first
124
- method_name = method_ast.children[1]
125
- next unless Rspec::EXAMPLE_METHODS.include?(method_name.to_s)
140
+ walker.on :ITER do |block_ast|
141
+ next unless NodeTypes.a_example_block?(block_ast)
126
142
 
127
143
  @handlers[:on_example_block].each do |handler|
128
- handler.call(block_ast)
144
+ handler.call(PinFactory.build_location_range(block_ast))
129
145
  end
130
146
 
131
- # @param blocks_in_examples [Parser::AST::Node]
132
- each_block(block_ast.children[2]) do |blocks_in_examples|
147
+ # @param blocks_in_examples [RubyVM::AbstractSyntaxTree::Node]
148
+ each_block(block_ast.children[1]) do |blocks_in_examples|
133
149
  @handlers[:on_blocks_in_examples].each do |handler|
134
- handler.call(blocks_in_examples)
150
+ handler.call(PinFactory.build_location_range(blocks_in_examples))
135
151
  end
136
152
  end
137
153
  end
138
154
 
139
- walker.on :block do |block_ast|
140
- next if block_ast.children.first.type != :send
141
-
142
- method_ast = block_ast.children.first
143
- method_name = method_ast.children[1]
144
- next unless Rspec::HOOK_METHODS.include?(method_name.to_s)
155
+ walker.on :ITER do |block_ast|
156
+ next unless NodeTypes.a_hook_block?(block_ast)
145
157
 
146
158
  @handlers[:on_hook_block].each do |handler|
147
- handler.call(block_ast)
159
+ handler.call(PinFactory.build_location_range(block_ast))
148
160
  end
149
161
 
150
- # @param blocks_in_examples [Parser::AST::Node]
151
- each_block(block_ast.children[2]) do |blocks_in_examples|
162
+ # @param blocks_in_examples [RubyVM::AbstractSyntaxTree::Node]
163
+ each_block(block_ast.children[1]) do |blocks_in_examples|
152
164
  @handlers[:on_blocks_in_examples].each do |handler|
153
- handler.call(blocks_in_examples)
165
+ handler.call(PinFactory.build_location_range(blocks_in_examples))
154
166
  end
155
167
  end
156
168
  end
@@ -165,9 +177,9 @@ module Solargraph
165
177
  # @param ast [Parser::AST::Node]
166
178
  # @param parent_result [Object]
167
179
  def each_block(ast, parent_result = nil, &block)
168
- return unless ast.is_a?(::Parser::AST::Node)
180
+ return unless ast.is_a?(RubyVM::AbstractSyntaxTree::Node)
169
181
 
170
- is_a_block = ast.type == :block && ast.children[0].type == :send
182
+ is_a_block = NodeTypes.a_block?(ast)
171
183
 
172
184
  if is_a_block
173
185
  result = block&.call(ast, parent_result)
@@ -182,12 +194,11 @@ module Solargraph
182
194
  # @yield [String, Parser::AST::Node]
183
195
  def each_context_block(ast, root_namespace = Rspec::ROOT_NAMESPACE, &block)
184
196
  each_block(ast, root_namespace) do |block_ast, parent_namespace|
185
- is_a_context = %i[describe context].include?(block_ast.children[0].children[1])
197
+ is_a_context = NodeTypes.a_context_block?(block_ast)
186
198
 
187
199
  next unless is_a_context
188
200
 
189
- description_node = block_ast.children[0].children[2]
190
- block_name = rspec_describe_class_name(description_node)
201
+ block_name = RspecContextNamespace.from_block_ast(block_ast)
191
202
  next unless block_name
192
203
 
193
204
  parent_namespace = namespace_name = "#{parent_namespace}::#{block_name}"
@@ -195,59 +206,6 @@ module Solargraph
195
206
  next parent_namespace
196
207
  end
197
208
  end
198
-
199
- # @param ast [Parser::AST::Node]
200
- # @return [String, nil]
201
- def rspec_describe_class_name(ast)
202
- if ast.type == :str
203
- string_to_const_name(ast)
204
- elsif ast.type == :const
205
- full_constant_name(ast).gsub('::', '')
206
- else
207
- Solargraph.logger.warn "[RSpec] Unexpected AST type #{ast.type}"
208
- nil
209
- end
210
- end
211
-
212
- # @param ast [Parser::AST::Node]
213
- # @return [String]
214
- def full_constant_name(ast)
215
- raise 'Node is not a constant' unless ast.type == :const
216
-
217
- name = ast.children[1].to_s
218
- if ast.children[0].nil?
219
- name
220
- else
221
- "#{full_constant_name(ast.children[0])}::#{name}"
222
- end
223
- end
224
-
225
- # @see https://github.com/rspec/rspec-core/blob/1eeadce5aa7137ead054783c31ff35cbfe9d07cc/lib/rspec/core/example_group.rb#L862
226
- # @param ast [Parser::AST::Node]
227
- # @return [String]
228
- def string_to_const_name(string_ast)
229
- return unless string_ast.type == :str
230
-
231
- name = string_ast.children[0]
232
- return 'Anonymous'.dup if name.empty?
233
-
234
- # Convert to CamelCase.
235
- name = +" #{name}"
236
- name.gsub!(/[^0-9a-zA-Z]+([0-9a-zA-Z])/) do
237
- match = ::Regexp.last_match[1]
238
- match.upcase!
239
- match
240
- end
241
-
242
- name.lstrip! # Remove leading whitespace
243
- name.gsub!(/\W/, '') # JRuby, RBX and others don't like non-ascii in const names
244
-
245
- # Ruby requires first const letter to be A-Z. Use `Nested`
246
- # as necessary to enforce that.
247
- name.gsub!(/\A([^A-Z]|\z)/, 'Nested\1')
248
-
249
- name
250
- end
251
209
  end
252
210
  end
253
211
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Solargraph
4
4
  module Rspec
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Credits: This file is a copy of the original file from the solargraph-rails gem.
4
-
5
3
  module Solargraph
6
4
  module Rspec
7
5
  class Walker
6
+ class ParsingError < StandardError; end
7
+
8
8
  class Hook
9
9
  attr_reader :node_type
10
10
 
@@ -17,7 +17,7 @@ module Solargraph
17
17
  @proc = Proc.new(&block)
18
18
  end
19
19
 
20
- # @param node [Parser::AST::Node]
20
+ # @param node [RubyVM::AbstractSyntaxTree::Node]
21
21
  # @return [void]
22
22
  def visit(node)
23
23
  return unless matches?(node)
@@ -33,15 +33,15 @@ module Solargraph
33
33
 
34
34
  private
35
35
 
36
- # @param node [Parser::AST::Node]
36
+ # @param node [RubyVM::AbstractSyntaxTree::Node]
37
37
  # @return [Boolean]
38
38
  def matches?(node)
39
39
  return false unless node.type == node_type
40
40
  return false unless node.children
41
41
  return true if @args.empty?
42
42
 
43
- a_child_matches = node.children.first.is_a?(::Parser::AST::Node) && node.children.any? do |child|
44
- child.is_a?(::Parser::AST::Node) &&
43
+ a_child_matches = node.children.first.is_a?(RubyVM::AbstractSyntaxTree::Node) && node.children.any? do |child|
44
+ child.is_a?(RubyVM::AbstractSyntaxTree::Node) &&
45
45
  match_children(child.children, @args[1..])
46
46
  end
47
47
 
@@ -50,12 +50,12 @@ module Solargraph
50
50
  match_children(node.children)
51
51
  end
52
52
 
53
- # @param children [Array<Parser::AST::Node>]
53
+ # @param children [Array<RubyVM::AbstractSyntaxTree::Node>]
54
54
  def match_children(children, args = @args)
55
55
  args.each_with_index.all? do |arg, i|
56
56
  if arg == :any
57
57
  true
58
- elsif children[i].is_a?(::Parser::AST::Node) && arg.is_a?(Symbol)
58
+ elsif children[i].is_a?(RubyVM::AbstractSyntaxTree::Node) && arg.is_a?(Symbol)
59
59
  children[i].type == arg
60
60
  else
61
61
  children[i] == arg
@@ -64,28 +64,9 @@ module Solargraph
64
64
  end
65
65
  end
66
66
 
67
- # https://github.com/castwide/solargraph/issues/522
68
- def self.normalize_ast(source)
69
- ast = source.node
70
-
71
- if ast.is_a?(::Parser::AST::Node)
72
- ast
73
- else
74
- NodeParser.parse_with_comments(source.code, source.filename)
75
- end
76
- end
77
-
78
- # @param source [Solargraph::Source]
79
- def self.from_source(source)
80
- new(*normalize_ast(source))
81
- end
82
-
83
- # @return ast [Parser::AST::Node]
84
- attr_reader :ast
85
- # @return comments [Hash]
86
- attr_reader :comments
67
+ attr_reader :ast, :comments
87
68
 
88
- # @param ast [Parser::AST::Node]
69
+ # @param ast [RubyVM::AbstractSyntaxTree::Node]
89
70
  # @param comments [Hash]
90
71
  def initialize(ast, comments = {})
91
72
  @comments = comments
@@ -93,23 +74,18 @@ module Solargraph
93
74
  @hooks = Hash.new([])
94
75
  end
95
76
 
96
- # @param node_type [Symbol]
97
- # @param args [Array]
98
- # @param block [Proc]
99
77
  def on(node_type, args = [], &block)
100
78
  @hooks[node_type] << Hook.new(node_type, args, &block)
101
79
  end
102
80
 
103
- # @return [void]
104
81
  def walk
105
82
  @ast.is_a?(Array) ? @ast.each { |node| traverse(node) } : traverse(@ast)
106
83
  end
107
84
 
108
85
  private
109
86
 
110
- # @param node [Parser::AST::Node]
111
87
  def traverse(node)
112
- return unless node.is_a?(::Parser::AST::Node)
88
+ return unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
113
89
 
114
90
  @hooks[node.type].each { |hook| hook.visit(node) }
115
91
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solargraph-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LekΓ« Mula
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-13 00:00:00.000000000 Z
11
+ date: 2024-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: solargraph
@@ -58,8 +58,12 @@ files:
58
58
  - lib/solargraph/rspec/correctors/let_methods_corrector.rb
59
59
  - lib/solargraph/rspec/correctors/subject_method_corrector.rb
60
60
  - lib/solargraph/rspec/correctors/walker_base.rb
61
+ - lib/solargraph/rspec/pin_factory.rb
61
62
  - lib/solargraph/rspec/spec_walker.rb
62
- - lib/solargraph/rspec/util.rb
63
+ - lib/solargraph/rspec/spec_walker/fake_let_method.rb
64
+ - lib/solargraph/rspec/spec_walker/full_constant_name.rb
65
+ - lib/solargraph/rspec/spec_walker/node_types.rb
66
+ - lib/solargraph/rspec/spec_walker/rspec_context_namespace.rb
63
67
  - lib/solargraph/rspec/version.rb
64
68
  - lib/solargraph/rspec/walker.rb
65
69
  - lib/solargraph_rspec.rb
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Credits: This file is a copy of the file from the solargraph-rspec gem
4
-
5
- # rubocop:disable Naming/MethodParameterName
6
- module Solargraph
7
- module Rspec
8
- # Utility methods for building pins and references.
9
- module Util
10
- def self.build_public_method(
11
- ns,
12
- name,
13
- types: nil,
14
- location: nil,
15
- comments: [],
16
- attribute: false,
17
- scope: :instance
18
- )
19
- opts = {
20
- name: name,
21
- location: location,
22
- closure: ns,
23
- scope: scope,
24
- attribute: attribute,
25
- comments: []
26
- }
27
-
28
- comments << "@return [#{types.join(",")}]" if types
29
-
30
- opts[:comments] = comments.join("\n")
31
-
32
- Solargraph::Pin::Method.new(**opts)
33
- end
34
-
35
- def self.build_module_include(ns, module_name, location)
36
- Solargraph::Pin::Reference::Include.new(
37
- closure: ns,
38
- name: module_name,
39
- location: location
40
- )
41
- end
42
-
43
- def self.build_module_extend(ns, module_name, location)
44
- Solargraph::Pin::Reference::Extend.new(
45
- closure: ns,
46
- name: module_name,
47
- location: location
48
- )
49
- end
50
-
51
- def self.dummy_location(path)
52
- Solargraph::Location.new(
53
- File.expand_path(path),
54
- Solargraph::Range.from_to(0, 0, 0, 0)
55
- )
56
- end
57
-
58
- # @param ast [Parser::AST::Node]
59
- def self.build_location(ast, path)
60
- Solargraph::Location.new(
61
- File.expand_path(path),
62
- Solargraph::Range.from_to(
63
- ast.location.first_line,
64
- ast.location.column,
65
- ast.location.last_line,
66
- ast.location.last_column
67
- )
68
- )
69
- end
70
-
71
- def self.method_return(path, type)
72
- Solargraph::Pin::Reference::Override.method_return(path, type)
73
- end
74
- end
75
- end
76
- end
77
- # rubocop:enable Naming/MethodParameterName