diff --git a/CHANGELOG.md b/CHANGELOG.md index 32524fb..b3b2eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 3rd party matchers completion +- Implement RSpec [one-liner syntax](https://rspec.info/features/3-12/rspec-core/subject/one-liner-syntax/) helpers: `is_expected`, `should` and `should_not` + -
📹 + One-liner syntax completion +
## v0.3.0 - 2024-07-10 diff --git a/doc/images/one_liners_demo.gif b/doc/images/one_liners_demo.gif new file mode 100644 index 0000000..478688e Binary files /dev/null and b/doc/images/one_liners_demo.gif differ diff --git a/lib/solargraph/rspec/correctors/subject_method_corrector.rb b/lib/solargraph/rspec/correctors/subject_method_corrector.rb index 62d0a38..409a304 100644 --- a/lib/solargraph/rspec/correctors/subject_method_corrector.rb +++ b/lib/solargraph/rspec/correctors/subject_method_corrector.rb @@ -16,6 +16,7 @@ def correct(_source_map) subject_pin = rspec_subject_method(namespace_pin, subject_name, location_range, fake_method_ast) add_pin(subject_pin) + add_pins(one_liner_expecation_pins(subject_pin)) end rspec_walker.after_walk do @@ -23,7 +24,11 @@ def correct(_source_map) namespace_pin = closest_namespace_pin(namespace_pins, described_class_pin.location.range.start.line) - add_pin(implicit_subject_pin(described_class_pin, namespace_pin)) if namespace_pin + if namespace_pin + implicit_subject_pin = implicit_subject_method(described_class_pin, namespace_pin) + add_pin(implicit_subject_pin) + add_pins(one_liner_expecation_pins(implicit_subject_pin)) + end end end @@ -47,7 +52,7 @@ def rspec_subject_method(namespace_pin, subject_name, location_range, fake_metho # @param described_class_pin [Pin::Method] # @param namespace_pin [Pin::Namespace] # @return [Pin::Method] - def implicit_subject_pin(described_class_pin, namespace_pin) + def implicit_subject_method(described_class_pin, namespace_pin) described_class = described_class_pin.return_type.first.subtypes.first.name PinFactory.build_public_method( @@ -58,6 +63,41 @@ def implicit_subject_pin(described_class_pin, namespace_pin) scope: :instance ) end + + # @param subject_pin [Pin::Method] + # @return [Array] + def one_liner_expecation_pins(subject_pin) + [ + one_liner_expecation_pin(subject_pin.closure, :is_expected, subject_pin.location), + one_liner_expecation_pin(subject_pin.closure, :should, subject_pin.location), + one_liner_expecation_pin(subject_pin.closure, :should_not, subject_pin.location) + ] + end + + # @param namespace_pin [Pin::Namespace] + # @param method_name [:is_expected, :should, :should_not] + # @param location [Solargraph::Location] + # @return [Pin::Method] + def one_liner_expecation_pin(namespace_pin, method_name, location) + return_type = case method_name + when :is_expected + ['::RSpec::Expectations::ExpectationTarget'] + when :should + ['::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher'] + when :should_not + ['::RSpec::Matchers::BuiltIn::NegativeOperatorMatcher'] + else + raise ArgumentError, "Unknown inline expectation method: #{method_name}" + end + + PinFactory.build_public_method( + namespace_pin, + method_name.to_s, + types: [return_type], + location: location, + scope: :instance + ) + end end end end diff --git a/spec/solargraph/rspec/convention_spec.rb b/spec/solargraph/rspec/convention_spec.rb index b65155e..c94c945 100644 --- a/spec/solargraph/rspec/convention_spec.rb +++ b/spec/solargraph/rspec/convention_spec.rb @@ -95,6 +95,39 @@ expect(completion_at(filename, [22, 8])).to include('nested_something') end + # @see https://rspec.info/features/3-12/rspec-core/subject/one-liner-syntax/ + it 'generates method for one-liner expectations' do + load_string filename, <<~RUBY + RSpec.describe SomeNamespace::Transaction, type: :model do + subject(:transaction) { described_class.new } + + it { is_expected.to be_a(SomeNamespace::Transaction) } + it { should be_a(SomeNamespace::Transaction) } + it { should_not be_a(String) } + end + RUBY + + subject_location = { start: { line: 1, character: 2 }, end: { line: 1, character: 23 } } + + assert_public_instance_method( + api_map, + 'RSpec::ExampleGroups::TestSomeNamespaceTransaction#is_expected', + ['RSpec::Expectations::ExpectationTarget'] + ) { |pin| expect(pin.location.range.to_hash).to eq(subject_location) } + + assert_public_instance_method( + api_map, + 'RSpec::ExampleGroups::TestSomeNamespaceTransaction#should', + ['RSpec::Matchers::BuiltIn::PositiveOperatorMatcher'] + ) { |pin| expect(pin.location.range.to_hash).to eq(subject_location) } + + assert_public_instance_method( + api_map, + 'RSpec::ExampleGroups::TestSomeNamespaceTransaction#should_not', + ['RSpec::Matchers::BuiltIn::NegativeOperatorMatcher'] + ) { |pin| expect(pin.location.range.to_hash).to eq(subject_location) } + end + it 'generates let methods with do/end block' do load_string filename, <<~RUBY RSpec.describe SomeNamespace::Transaction, type: :model do