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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/README.md +111 -56
- data/README.md.erb +117 -62
- data/lib/transpec/ast/node.rb +41 -0
- data/lib/transpec/base_rewriter.rb +55 -0
- data/lib/transpec/cli.rb +43 -153
- data/lib/transpec/configuration.rb +13 -9
- data/lib/transpec/{rewriter.rb → converter.rb} +44 -71
- data/lib/transpec/dynamic_analyzer/rewriter.rb +94 -0
- data/lib/transpec/dynamic_analyzer/runtime_data.rb +27 -0
- data/lib/transpec/dynamic_analyzer.rb +166 -0
- data/lib/transpec/file_finder.rb +53 -0
- data/lib/transpec/option_parser.rb +166 -0
- data/lib/transpec/{context.rb → static_context_inspector.rb} +2 -2
- data/lib/transpec/syntax/be_close.rb +7 -9
- data/lib/transpec/syntax/double.rb +6 -10
- data/lib/transpec/syntax/expect.rb +35 -0
- data/lib/transpec/syntax/have.rb +195 -0
- data/lib/transpec/syntax/method_stub.rb +22 -27
- data/lib/transpec/syntax/mixin/allow_no_message.rb +73 -0
- data/lib/transpec/syntax/mixin/any_instance.rb +22 -0
- data/lib/transpec/syntax/mixin/expectizable.rb +26 -0
- data/lib/transpec/syntax/mixin/have_matcher.rb +23 -0
- data/lib/transpec/syntax/mixin/monkey_patch.rb +37 -0
- data/lib/transpec/syntax/mixin/send.rb +109 -0
- data/lib/transpec/syntax/{matcher.rb → operator_matcher.rb} +27 -14
- data/lib/transpec/syntax/raise_error.rb +6 -10
- data/lib/transpec/syntax/rspec_configure.rb +29 -28
- data/lib/transpec/syntax/should.rb +45 -15
- data/lib/transpec/syntax/should_receive.rb +44 -16
- data/lib/transpec/syntax.rb +29 -21
- data/lib/transpec/util.rb +12 -2
- data/lib/transpec/version.rb +3 -3
- data/spec/spec_helper.rb +8 -6
- data/spec/support/cache_helper.rb +50 -0
- data/spec/support/shared_context.rb +49 -1
- data/spec/transpec/ast/node_spec.rb +65 -0
- data/spec/transpec/cli_spec.rb +33 -242
- data/spec/transpec/commit_message_spec.rb +2 -2
- data/spec/transpec/configuration_spec.rb +12 -8
- data/spec/transpec/{rewriter_spec.rb → converter_spec.rb} +198 -148
- data/spec/transpec/dynamic_analyzer/rewriter_spec.rb +183 -0
- data/spec/transpec/dynamic_analyzer_spec.rb +164 -0
- data/spec/transpec/file_finder_spec.rb +118 -0
- data/spec/transpec/option_parser_spec.rb +185 -0
- data/spec/transpec/{context_spec.rb → static_context_inspector_spec.rb} +27 -12
- data/spec/transpec/syntax/be_close_spec.rb +8 -4
- data/spec/transpec/syntax/double_spec.rb +105 -12
- data/spec/transpec/syntax/expect_spec.rb +83 -0
- data/spec/transpec/syntax/have_spec.rb +599 -0
- data/spec/transpec/syntax/method_stub_spec.rb +276 -115
- data/spec/transpec/syntax/{matcher_spec.rb → operator_matcher_spec.rb} +277 -98
- data/spec/transpec/syntax/raise_error_spec.rb +92 -46
- data/spec/transpec/syntax/should_receive_spec.rb +298 -92
- data/spec/transpec/syntax/should_spec.rb +230 -44
- data/spec/transpec/util_spec.rb +2 -9
- data/tasks/lib/transpec_demo.rb +1 -1
- data/tasks/lib/transpec_test.rb +5 -7
- data/tasks/test.rake +5 -1
- data/transpec.gemspec +1 -1
- metadata +46 -22
- data/lib/transpec/syntax/able_to_allow_no_message.rb +0 -73
- data/lib/transpec/syntax/able_to_target_any_instance.rb +0 -24
- data/lib/transpec/syntax/expectizable.rb +0 -27
- 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
|
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 :
|
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/
|
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/
|
4
|
+
require 'transpec/syntax/mixin/send'
|
5
5
|
|
6
6
|
module Transpec
|
7
7
|
class Syntax
|
8
8
|
class Double < Syntax
|
9
|
-
include
|
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
|