transpec 0.2.6 → 1.0.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +10 -0
  5. data/README.md +111 -56
  6. data/README.md.erb +117 -62
  7. data/lib/transpec/ast/node.rb +41 -0
  8. data/lib/transpec/base_rewriter.rb +55 -0
  9. data/lib/transpec/cli.rb +43 -153
  10. data/lib/transpec/configuration.rb +13 -9
  11. data/lib/transpec/{rewriter.rb → converter.rb} +44 -71
  12. data/lib/transpec/dynamic_analyzer/rewriter.rb +94 -0
  13. data/lib/transpec/dynamic_analyzer/runtime_data.rb +27 -0
  14. data/lib/transpec/dynamic_analyzer.rb +166 -0
  15. data/lib/transpec/file_finder.rb +53 -0
  16. data/lib/transpec/option_parser.rb +166 -0
  17. data/lib/transpec/{context.rb → static_context_inspector.rb} +2 -2
  18. data/lib/transpec/syntax/be_close.rb +7 -9
  19. data/lib/transpec/syntax/double.rb +6 -10
  20. data/lib/transpec/syntax/expect.rb +35 -0
  21. data/lib/transpec/syntax/have.rb +195 -0
  22. data/lib/transpec/syntax/method_stub.rb +22 -27
  23. data/lib/transpec/syntax/mixin/allow_no_message.rb +73 -0
  24. data/lib/transpec/syntax/mixin/any_instance.rb +22 -0
  25. data/lib/transpec/syntax/mixin/expectizable.rb +26 -0
  26. data/lib/transpec/syntax/mixin/have_matcher.rb +23 -0
  27. data/lib/transpec/syntax/mixin/monkey_patch.rb +37 -0
  28. data/lib/transpec/syntax/mixin/send.rb +109 -0
  29. data/lib/transpec/syntax/{matcher.rb → operator_matcher.rb} +27 -14
  30. data/lib/transpec/syntax/raise_error.rb +6 -10
  31. data/lib/transpec/syntax/rspec_configure.rb +29 -28
  32. data/lib/transpec/syntax/should.rb +45 -15
  33. data/lib/transpec/syntax/should_receive.rb +44 -16
  34. data/lib/transpec/syntax.rb +29 -21
  35. data/lib/transpec/util.rb +12 -2
  36. data/lib/transpec/version.rb +3 -3
  37. data/spec/spec_helper.rb +8 -6
  38. data/spec/support/cache_helper.rb +50 -0
  39. data/spec/support/shared_context.rb +49 -1
  40. data/spec/transpec/ast/node_spec.rb +65 -0
  41. data/spec/transpec/cli_spec.rb +33 -242
  42. data/spec/transpec/commit_message_spec.rb +2 -2
  43. data/spec/transpec/configuration_spec.rb +12 -8
  44. data/spec/transpec/{rewriter_spec.rb → converter_spec.rb} +198 -148
  45. data/spec/transpec/dynamic_analyzer/rewriter_spec.rb +183 -0
  46. data/spec/transpec/dynamic_analyzer_spec.rb +164 -0
  47. data/spec/transpec/file_finder_spec.rb +118 -0
  48. data/spec/transpec/option_parser_spec.rb +185 -0
  49. data/spec/transpec/{context_spec.rb → static_context_inspector_spec.rb} +27 -12
  50. data/spec/transpec/syntax/be_close_spec.rb +8 -4
  51. data/spec/transpec/syntax/double_spec.rb +105 -12
  52. data/spec/transpec/syntax/expect_spec.rb +83 -0
  53. data/spec/transpec/syntax/have_spec.rb +599 -0
  54. data/spec/transpec/syntax/method_stub_spec.rb +276 -115
  55. data/spec/transpec/syntax/{matcher_spec.rb → operator_matcher_spec.rb} +277 -98
  56. data/spec/transpec/syntax/raise_error_spec.rb +92 -46
  57. data/spec/transpec/syntax/should_receive_spec.rb +298 -92
  58. data/spec/transpec/syntax/should_spec.rb +230 -44
  59. data/spec/transpec/util_spec.rb +2 -9
  60. data/tasks/lib/transpec_demo.rb +1 -1
  61. data/tasks/lib/transpec_test.rb +5 -7
  62. data/tasks/test.rake +5 -1
  63. data/transpec.gemspec +1 -1
  64. metadata +46 -22
  65. data/lib/transpec/syntax/able_to_allow_no_message.rb +0 -73
  66. data/lib/transpec/syntax/able_to_target_any_instance.rb +0 -24
  67. data/lib/transpec/syntax/expectizable.rb +0 -27
  68. data/lib/transpec/syntax/send_node_syntax.rb +0 -57
@@ -0,0 +1,166 @@
1
+ # coding: utf-8
2
+
3
+ require 'transpec/file_finder'
4
+ require 'transpec/dynamic_analyzer/rewriter'
5
+ require 'transpec/dynamic_analyzer/runtime_data'
6
+ require 'tmpdir'
7
+ require 'fileutils'
8
+ require 'ostruct'
9
+ require 'shellwords'
10
+ require 'English'
11
+
12
+ module Transpec
13
+ class DynamicAnalyzer
14
+ EVAL_TARGET_TYPES = [:object, :context]
15
+ ANALYSIS_METHOD = 'transpec_analysis'
16
+ HELPER_FILE = 'transpec_analysis_helper.rb'
17
+ RESULT_FILE = 'transpec_analysis_result.dump'
18
+ HELPER_SOURCE = <<-END
19
+ require 'ostruct'
20
+ require 'pathname'
21
+
22
+ module TranspecAnalysis
23
+ @base_pathname = Pathname.pwd
24
+
25
+ def self.data
26
+ @data ||= {}
27
+ end
28
+
29
+ def self.node_id(filename, begin_pos, end_pos)
30
+ absolute_path = File.expand_path(filename)
31
+ relative_path = Pathname.new(absolute_path).relative_path_from(@base_pathname).to_s
32
+ [relative_path, begin_pos, end_pos].join('_')
33
+ end
34
+
35
+ at_exit do
36
+ File.open('#{RESULT_FILE}', 'w') do |file|
37
+ Marshal.dump(data, file)
38
+ end
39
+ end
40
+ end
41
+
42
+ def #{ANALYSIS_METHOD}(object, context, analysis_codes, filename, begin_pos, end_pos)
43
+ pair_array = analysis_codes.map do |key, (target_type, code)|
44
+ target = case target_type
45
+ when :object then object
46
+ when :context then context
47
+ end
48
+
49
+ eval_data = OpenStruct.new
50
+
51
+ begin
52
+ eval_data.result = target.instance_eval(code)
53
+ rescue Exception => error
54
+ eval_data.error = error
55
+ end
56
+
57
+ [key, eval_data]
58
+ end
59
+
60
+ object_data = Hash[pair_array]
61
+
62
+ id = TranspecAnalysis.node_id(filename, begin_pos, end_pos)
63
+ TranspecAnalysis.data[id] = object_data
64
+
65
+ object
66
+ end
67
+ END
68
+
69
+ attr_reader :project_path, :rspec_command, :silent
70
+ alias_method :silent?, :silent
71
+
72
+ def initialize(options = {})
73
+ @project_path = options[:project_path] || Dir.pwd
74
+ @rspec_command = options[:rspec_command] || default_rspec_command
75
+ @silent = options[:silent] || false
76
+
77
+ if block_given?
78
+ in_copied_project do
79
+ yield self
80
+ end
81
+ end
82
+ end
83
+
84
+ def default_rspec_command
85
+ if File.exist?('Gemfile')
86
+ 'bundle exec rspec'
87
+ else
88
+ 'rspec'
89
+ end
90
+ end
91
+
92
+ def analyze(paths = [])
93
+ in_copied_project do
94
+ rewriter = Rewriter.new
95
+
96
+ FileFinder.find(paths).each do |file_path|
97
+ begin
98
+ rewriter.rewrite_file!(file_path)
99
+ rescue Parser::SyntaxError # rubocop:disable HandleExceptions
100
+ # Syntax errors will be reported in CLI with Converter.
101
+ end
102
+ end
103
+
104
+ File.write(HELPER_FILE, HELPER_SOURCE)
105
+
106
+ run_rspec(paths)
107
+
108
+ File.open(RESULT_FILE) do |file|
109
+ hash = Marshal.load(file)
110
+ RuntimeData.new(hash)
111
+ end
112
+ end
113
+ end
114
+
115
+ def in_copied_project
116
+ return yield if @in_copied_project
117
+
118
+ @in_copied_project = true
119
+
120
+ Dir.mktmpdir do |tmpdir|
121
+ FileUtils.cp_r(@project_path, tmpdir)
122
+ @copied_project_path = File.join(tmpdir, File.basename(@project_path))
123
+ Dir.chdir(@copied_project_path) do
124
+ yield
125
+ end
126
+ end
127
+ ensure
128
+ @in_copied_project = false
129
+ end
130
+
131
+ def run_rspec(paths)
132
+ with_bundler_clean_env do
133
+ ENV['SPEC_OPTS'] = ['-r', "./#{HELPER_FILE}"].shelljoin
134
+
135
+ command = "#{rspec_command} #{paths.shelljoin}"
136
+
137
+ if silent?
138
+ rspec_output = `#{command} 2> /dev/null`
139
+ else
140
+ system(command)
141
+ end
142
+
143
+ unless $CHILD_STATUS.exitstatus == 0
144
+ message = 'Dynamic analysis failed!'
145
+ if silent?
146
+ message << "\n"
147
+ message << rspec_output
148
+ end
149
+ fail message
150
+ end
151
+ end
152
+ end
153
+
154
+ def with_bundler_clean_env
155
+ if defined?(Bundler)
156
+ Bundler.with_clean_env do
157
+ # Bundler.with_clean_env cleans environment variables
158
+ # which are set after bundler is loaded.
159
+ yield
160
+ end
161
+ else
162
+ yield
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,53 @@
1
+ # coding: utf-8
2
+
3
+ require 'find'
4
+
5
+ module Transpec
6
+ module FileFinder
7
+ module_function
8
+
9
+ def find(paths)
10
+ base_paths(paths).reduce([]) do |file_paths, path|
11
+ if File.directory?(path)
12
+ file_paths.concat(ruby_files_in_directory(path))
13
+ elsif File.file?(path)
14
+ file_paths << path
15
+ elsif !File.exists?(path)
16
+ fail ArgumentError, "No such file or directory #{path.inspect}"
17
+ end
18
+ end
19
+ end
20
+
21
+ def base_paths(paths)
22
+ if paths.empty?
23
+ if Dir.exists?('spec')
24
+ ['spec']
25
+ else
26
+ fail ArgumentError, 'Specify target files or directories.'
27
+ end
28
+ else
29
+ if paths.all? { |path| inside_of_current_working_directory?(path) }
30
+ paths
31
+ else
32
+ fail ArgumentError, 'Target path must be inside of the current working directory.'
33
+ end
34
+ end
35
+ end
36
+
37
+ def inside_of_current_working_directory?(path)
38
+ File.expand_path(path).start_with?(Dir.pwd)
39
+ end
40
+
41
+ def ruby_files_in_directory(directory_path)
42
+ ruby_file_paths = []
43
+
44
+ Find.find(directory_path) do |path|
45
+ next unless File.file?(path)
46
+ next unless File.extname(path) == '.rb'
47
+ ruby_file_paths << path
48
+ end
49
+
50
+ ruby_file_paths
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,166 @@
1
+ # coding: utf-8
2
+
3
+ require 'transpec/configuration'
4
+ require 'transpec/git'
5
+ require 'transpec/version'
6
+ require 'optparse'
7
+ require 'rainbow'
8
+
9
+ module Transpec
10
+ class OptionParser
11
+ CONFIG_ATTRS_FOR_CLI_TYPES = {
12
+ should: :convert_should=,
13
+ should_receive: :convert_should_receive=,
14
+ stub: :convert_stub=,
15
+ have_items: :convert_have_items=,
16
+ deprecated: :convert_deprecated_method=
17
+ }
18
+
19
+ attr_reader :configuration
20
+
21
+ def self.available_conversion_types
22
+ CONFIG_ATTRS_FOR_CLI_TYPES.keys
23
+ end
24
+
25
+ def initialize(configuration = Configuration.new)
26
+ @configuration = configuration
27
+ setup_parser
28
+ end
29
+
30
+ def parse(args)
31
+ args = args.dup
32
+ @parser.parse!(args)
33
+ args
34
+ end
35
+
36
+ def help
37
+ @parser.help
38
+ end
39
+
40
+ private
41
+
42
+ # rubocop:disable MethodLength
43
+ def setup_parser
44
+ @parser = create_parser
45
+
46
+ define_option('-f', '--force') do
47
+ @configuration.forced = true
48
+ end
49
+
50
+ define_option('-s', '--skip-dynamic-analysis') do
51
+ @configuration.skip_dynamic_analysis = true
52
+ end
53
+
54
+ define_option('-c', '--rspec-command COMMAND') do |command|
55
+ @configuration.rspec_command = command
56
+ end
57
+
58
+ define_option('-m', '--generate-commit-message') do
59
+ unless Git.inside_of_repository?
60
+ fail '-m/--generate-commit-message option is specified but not in a Git repository'
61
+ end
62
+
63
+ @configuration.generate_commit_message = true
64
+ end
65
+
66
+ define_option('-k', '--keep TYPE[,TYPE...]') do |types|
67
+ types.split(',').each do |type|
68
+ config_attr = CONFIG_ATTRS_FOR_CLI_TYPES[type.to_sym]
69
+ fail ArgumentError, "Unknown syntax type #{type.inspect}" unless config_attr
70
+ @configuration.send(config_attr, false)
71
+ end
72
+ end
73
+
74
+ define_option('-n', '--negative-form FORM') do |form|
75
+ @configuration.negative_form_of_to = form
76
+ end
77
+
78
+ define_option('-p', '--no-parentheses-matcher-arg') do
79
+ @configuration.parenthesize_matcher_arg = false
80
+ end
81
+
82
+ define_option('--no-color') do
83
+ Sickill::Rainbow.enabled = false
84
+ end
85
+
86
+ define_option('--version') do
87
+ puts Version.to_s
88
+ exit
89
+ end
90
+ end
91
+ # rubocop:enable MethodLength
92
+
93
+ def create_parser
94
+ banner = "Usage: transpec [options] [files or directories]\n\n"
95
+ summary_width = 32 # Default
96
+ indentation = ' ' * 2
97
+ ::OptionParser.new(banner, summary_width, indentation)
98
+ end
99
+
100
+ def define_option(*options, &block)
101
+ description_lines = descriptions[options.first]
102
+ @parser.on(*options, *description_lines, &block)
103
+ end
104
+
105
+ # rubocop:disable MethodLength, AlignHash
106
+ def descriptions
107
+ @descriptions ||= {
108
+ '-f' => [
109
+ 'Force processing even if the current Git',
110
+ 'repository is not clean.'
111
+ ],
112
+ '-s' => [
113
+ 'Skip dynamic analysis and convert with only',
114
+ 'static analysis. Note that specifying this',
115
+ 'option decreases the conversion accuracy.'
116
+ ],
117
+ '-c' => [
118
+ 'Specify command to run RSpec that is used for',
119
+ 'dynamic analysis.',
120
+ 'Default: "bundle exec rspec"'
121
+ ],
122
+ '-m' => [
123
+ 'Generate commit message that describes',
124
+ 'conversion summary. Only Git is supported.'
125
+ ],
126
+ '-k' => [
127
+ 'Keep specific syntaxes by disabling',
128
+ 'conversions.',
129
+ 'Available syntax types:',
130
+ " #{'should'.bright} (to #{'expect(obj).to'.underline})",
131
+ " #{'should_receive'.bright} (to #{'expect(obj).to receive'.underline})",
132
+ " #{'stub'.bright} (to #{'allow(obj).to receive'.underline})",
133
+ " #{'have_items'.bright} (to #{'expect(obj.size).to eq(x)'.underline})",
134
+ " #{'deprecated'.bright} (e.g. from #{'mock'.underline} to #{'double'.underline})",
135
+ 'These are all converted by default.'
136
+ ],
137
+ '-n' => [
138
+ "Specify negative form of #{'to'.underline} that is used in",
139
+ "#{'expect(...).to'.underline} syntax.",
140
+ "Either #{'not_to'.bright} or #{'to_not'.bright}.",
141
+ "Default: #{'not_to'.bright}"
142
+ ],
143
+ '-p' => [
144
+ 'Suppress parenthesizing argument of matcher',
145
+ 'when converting operator to non-operator in',
146
+ "#{'expect'.underline} syntax. Note that it will be",
147
+ 'parenthesized even if this option is',
148
+ 'specified when parentheses are necessary to',
149
+ 'keep the meaning of the expression.',
150
+ 'By default, arguments of the following',
151
+ 'operator matchers will be parenthesized.',
152
+ " #{'== 10'.underline} to #{'eq(10)'.underline}",
153
+ " #{'=~ /pattern/'.underline} to #{'match(/pattern/)'.underline}",
154
+ " #{'=~ [1, 2]'.underline} to #{'match_array([1, 2])'.underline}"
155
+ ],
156
+ '--no-color' => [
157
+ 'Disable color in the output.'
158
+ ],
159
+ '--version' => [
160
+ 'Show Transpec version.'
161
+ ]
162
+ }
163
+ end
164
+ # rubocop:enable MethodLength, AlignHash
165
+ end
166
+ end
@@ -3,7 +3,7 @@
3
3
  require 'transpec/util'
4
4
 
5
5
  module Transpec
6
- class Context
6
+ class StaticContextInspector
7
7
  include Util
8
8
 
9
9
  SCOPE_TYPES = [:module, :class, :sclass, :def, :defs, :block].freeze
@@ -67,7 +67,7 @@ module Transpec
67
67
  @expectation_available = match_scopes(NON_MONKEY_PATCH_EXPECTATION_AVAILABLE_CONTEXT)
68
68
  end
69
69
 
70
- alias_method :expect_to_matcher_available?, :non_monkey_patch_expectation_available?
70
+ alias_method :expect_available?, :non_monkey_patch_expectation_available?
71
71
 
72
72
  def non_monkey_patch_mock_available?
73
73
  return @mock_available if instance_variable_defined?(:@mock_available)
@@ -1,11 +1,17 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require 'transpec/syntax'
4
- require 'transpec/syntax/send_node_syntax'
4
+ require 'transpec/syntax/mixin/send'
5
5
 
6
6
  module Transpec
7
7
  class Syntax
8
8
  class BeClose < Syntax
9
+ include Mixin::Send
10
+
11
+ def self.target_method?(receiver_node, method_name)
12
+ receiver_node.nil? && method_name == :be_close
13
+ end
14
+
9
15
  def convert_to_be_within!
10
16
  _receiver_node, _method_name, expected_node, delta_node = *node
11
17
 
@@ -22,14 +28,6 @@ module Transpec
22
28
 
23
29
  private
24
30
 
25
- def self.target_receiver_node?(node)
26
- node.nil?
27
- end
28
-
29
- def self.target_method_names
30
- [:be_close]
31
- end
32
-
33
31
  def register_record
34
32
  @report.records << Record.new('be_close(expected, delta)', 'be_within(delta).of(expected)')
35
33
  end
@@ -1,12 +1,16 @@
1
1
  # coding: utf-8
2
2
 
3
3
  require 'transpec/syntax'
4
- require 'transpec/syntax/send_node_syntax'
4
+ require 'transpec/syntax/mixin/send'
5
5
 
6
6
  module Transpec
7
7
  class Syntax
8
8
  class Double < Syntax
9
- include SendNodeSyntax
9
+ include Mixin::Send
10
+
11
+ def self.target_method?(receiver_node, method_name)
12
+ receiver_node.nil? && [:double, :mock, :stub].include?(method_name)
13
+ end
10
14
 
11
15
  def convert_to_double!
12
16
  return if method_name == :double
@@ -16,14 +20,6 @@ module Transpec
16
20
 
17
21
  private
18
22
 
19
- def self.target_receiver_node?(node)
20
- node.nil?
21
- end
22
-
23
- def self.target_method_names
24
- [:double, :mock, :stub]
25
- end
26
-
27
23
  def register_record
28
24
  @report.records << Record.new("#{method_name}('something')", "double('something')")
29
25
  end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+
3
+ require 'transpec/syntax'
4
+ require 'transpec/syntax/mixin/send'
5
+ require 'transpec/syntax/mixin/have_matcher'
6
+
7
+ module Transpec
8
+ class Syntax
9
+ class Expect < Syntax
10
+ include Mixin::Send, Mixin::HaveMatcher
11
+
12
+ def self.target_method?(receiver_node, method_name)
13
+ receiver_node.nil? && method_name == :expect
14
+ end
15
+
16
+ def register_request_for_dynamic_analysis(rewriter)
17
+ have_matcher.register_request_for_dynamic_analysis(rewriter) if have_matcher
18
+ end
19
+
20
+ def current_syntax_type
21
+ :expect
22
+ end
23
+
24
+ def matcher_node
25
+ parent_node.children[2]
26
+ end
27
+
28
+ alias_method :subject_node, :arg_node
29
+
30
+ def subject_range
31
+ subject_node.loc.expression
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,195 @@
1
+ # coding: utf-8
2
+
3
+ require 'transpec/syntax'
4
+ require 'transpec/syntax/mixin/send'
5
+
6
+ module Transpec
7
+ class Syntax
8
+ class Have < Syntax
9
+ include Mixin::Send
10
+
11
+ # String#count is not query method, and there's no way to determine
12
+ # whether a method is query method.
13
+ # Method#arity and Method#parameters return same results
14
+ # for Array#count (0+ args) and String#count (1+ args).
15
+ #
16
+ # So I make #size a priority over #count so that #count won't be chosen
17
+ # for String (String responds to #size).
18
+ QUERY_METHOD_PRIORITIES = [:size, :count, :length].freeze
19
+
20
+ def self.standalone?
21
+ false
22
+ end
23
+
24
+ def self.target_method?(have_node, items_method_name)
25
+ return false unless have_node
26
+ have_receiver_node, have_method_name, *_ = *have_node
27
+ return false if have_receiver_node
28
+ [:have, :have_exactly, :have_at_least, :have_at_most].include?(have_method_name)
29
+ end
30
+
31
+ def initialize(node, expectation, source_rewriter = nil, runtime_data = nil, report = nil)
32
+ @node = node
33
+ @expectation = expectation
34
+ @source_rewriter = source_rewriter
35
+ @runtime_data = runtime_data
36
+ @report = report || Report.new
37
+ end
38
+
39
+ def register_request_for_dynamic_analysis(rewriter)
40
+ node = @expectation.subject_node
41
+
42
+ # `expect(owner).to have(n).things` invokes private owner#things with Object#__send__
43
+ # if the owner does not respond to any of #size, #count and #length.
44
+ #
45
+ # rubocop:disable LineLength
46
+ # https://github.com/rspec/rspec-expectations/blob/v2.14.3/lib/rspec/matchers/built_in/have.rb#L48-L58
47
+ # rubocop:enable LineLength
48
+ key = :subject_is_owner_of_collection?
49
+ code = "respond_to?(#{items_name.inspect}) || " +
50
+ "(methods & #{QUERY_METHOD_PRIORITIES.inspect}).empty?"
51
+ rewriter.register_request(node, key, code)
52
+
53
+ key = :available_query_methods
54
+ code = "target = #{code} ? #{items_name} : self; " +
55
+ "target.methods & #{QUERY_METHOD_PRIORITIES.inspect}"
56
+ rewriter.register_request(node, key, code)
57
+
58
+ key = :collection_accessor_is_private?
59
+ code = "private_methods.include?(#{items_name.inspect})"
60
+ rewriter.register_request(node, key, code)
61
+ end
62
+
63
+ def convert_to_standard_expectation!
64
+ replace(@expectation.subject_range, replacement_subject_source)
65
+ replace(expression_range, replacement_matcher_source(size_source))
66
+ register_record
67
+ end
68
+
69
+ def have_node
70
+ node.children.first
71
+ end
72
+
73
+ def size_node
74
+ have_node.children[2]
75
+ end
76
+
77
+ alias_method :items_node, :node
78
+
79
+ def have_method_name
80
+ have_node.children[1]
81
+ end
82
+
83
+ def items_name
84
+ items_node.children[1]
85
+ end
86
+
87
+ def subject_is_owner_of_collection?
88
+ node_data = runtime_node_data(@expectation.subject_node)
89
+ node_data && node_data[:subject_is_owner_of_collection?].result
90
+ end
91
+
92
+ def collection_accessor_is_private?
93
+ node_data = runtime_node_data(@expectation.subject_node)
94
+ node_data && node_data[:collection_accessor_is_private?].result
95
+ end
96
+
97
+ def query_method
98
+ node_data = runtime_node_data(@expectation.subject_node)
99
+ if node_data
100
+ (QUERY_METHOD_PRIORITIES & node_data[:available_query_methods].result).first
101
+ else
102
+ default_query_method
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def default_query_method
109
+ QUERY_METHOD_PRIORITIES.first
110
+ end
111
+
112
+ def replacement_subject_source
113
+ source = @expectation.subject_range.source
114
+ if subject_is_owner_of_collection?
115
+ if collection_accessor_is_private?
116
+ source << ".send(#{items_name.inspect})"
117
+ else
118
+ source << ".#{items_name}"
119
+ end
120
+ end
121
+ source << ".#{query_method}"
122
+ end
123
+
124
+ def replacement_matcher_source(size_source)
125
+ case @expectation.current_syntax_type
126
+ when :should
127
+ case have_method_name
128
+ when :have, :have_exactly then "== #{size_source}"
129
+ when :have_at_least then ">= #{size_source}"
130
+ when :have_at_most then "<= #{size_source}"
131
+ end
132
+ when :expect
133
+ case have_method_name
134
+ when :have, :have_exactly then "eq(#{size_source})"
135
+ when :have_at_least then "be >= #{size_source}"
136
+ when :have_at_most then "be <= #{size_source}"
137
+ end
138
+ end
139
+ end
140
+
141
+ def size_source
142
+ size_node.loc.expression.source
143
+ end
144
+
145
+ def dot_items_range
146
+ map = items_node.loc
147
+ map.dot.join(map.selector)
148
+ end
149
+
150
+ def register_record
151
+ @report.records << Record.new(original_syntax, converted_syntax)
152
+ end
153
+
154
+ def original_syntax
155
+ if subject_is_owner_of_collection?
156
+ subject = 'obj'
157
+ items = items_name
158
+ else
159
+ subject = 'collection'
160
+ items = 'items'
161
+ end
162
+
163
+ syntax = case @expectation
164
+ when Should
165
+ "#{subject}.should"
166
+ when Expect
167
+ "expect(#{subject}).to"
168
+ end
169
+
170
+ syntax << " #{have_method_name}(n).#{items}"
171
+ end
172
+
173
+ def converted_syntax
174
+ subject = if subject_is_owner_of_collection?
175
+ if collection_accessor_is_private?
176
+ "obj.send(#{items_name.inspect}).#{query_method}"
177
+ else
178
+ "obj.#{items_name}.#{query_method}"
179
+ end
180
+ else
181
+ "collection.#{default_query_method}"
182
+ end
183
+
184
+ syntax = case @expectation.current_syntax_type
185
+ when :should
186
+ "#{subject}.should"
187
+ when :expect
188
+ "expect(#{subject}).to"
189
+ end
190
+
191
+ syntax << " #{replacement_matcher_source('n')}"
192
+ end
193
+ end
194
+ end
195
+ end