transpec 0.2.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
data/lib/transpec/cli.rb CHANGED
@@ -1,27 +1,17 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'transpec/configuration'
4
3
  require 'transpec/commit_message'
5
- require 'transpec/git'
4
+ require 'transpec/configuration'
5
+ require 'transpec/converter'
6
+ require 'transpec/dynamic_analyzer'
7
+ require 'transpec/file_finder'
8
+ require 'transpec/option_parser'
6
9
  require 'transpec/report'
7
- require 'transpec/rewriter'
8
- require 'transpec/version'
9
- require 'optparse'
10
- require 'find'
11
10
  require 'rainbow'
12
11
 
13
12
  module Transpec
14
13
  class CLI
15
- CONFIG_ATTRS_FOR_CLI_TYPES = {
16
- expect_to_matcher: :convert_to_expect_to_matcher=,
17
- expect_to_receive: :convert_to_expect_to_receive=,
18
- allow_to_receive: :convert_to_allow_to_receive=,
19
- deprecated: :replace_deprecated_method=
20
- }
21
-
22
- attr_reader :configuration, :forced, :generates_commit_message
23
- alias_method :forced?, :forced
24
- alias_method :generates_commit_message?, :generates_commit_message
14
+ attr_reader :configuration
25
15
 
26
16
  def self.run(args = ARGV)
27
17
  new.run(args)
@@ -29,172 +19,67 @@ module Transpec
29
19
 
30
20
  def initialize
31
21
  @configuration = Configuration.new
32
- @forced = false
33
- @generates_commit_message = false
34
22
  @report = Report.new
35
23
  end
36
24
 
37
25
  def run(args)
38
- non_option_args = parse_options(args)
39
-
40
- fail_if_should_not_continue!
41
-
42
- base_paths = base_target_paths(non_option_args)
43
-
44
- target_files(base_paths).each do |file_path|
45
- process_file(file_path)
26
+ begin
27
+ paths = OptionParser.new(@configuration).parse(args)
28
+ fail_if_should_not_continue!
29
+ rescue => error
30
+ warn error.message
31
+ return false
46
32
  end
47
33
 
34
+ process(paths)
35
+
48
36
  display_summary
49
- generate_commit_message if generates_commit_message?
37
+ generate_commit_message if @configuration.generate_commit_message?
38
+ display_final_guide
50
39
 
51
40
  true
52
- rescue => error
53
- warn error.message
54
- false
55
41
  end
56
42
 
57
- def process_file(file_path)
58
- puts "Processing #{file_path}"
59
-
60
- rewriter = Rewriter.new(@configuration, @report)
61
- rewriter.rewrite_file!(file_path)
62
-
63
- @report.invalid_context_errors.concat(rewriter.invalid_context_errors)
43
+ def process(paths)
44
+ runtime_data = nil
64
45
 
65
- rewriter.invalid_context_errors.each do |error|
66
- warn_invalid_context_error(error)
67
- end
68
- rescue Parser::SyntaxError => error
69
- @report.syntax_errors << error
70
- warn_syntax_error(error)
71
- end
72
-
73
- # rubocop:disable MethodLength
74
- def parse_options(args)
75
- parser = OptionParser.new
76
- parser.banner = "Usage: transpec [options] [files or directories]\n\n"
77
-
78
- parser.on(
79
- '-f', '--force',
80
- 'Force processing even if the current Git',
81
- 'repository is not clean.'
82
- ) do
83
- @forced = true
84
- end
85
-
86
- parser.on(
87
- '-m', '--commit-message',
88
- 'Generate commit message that describes',
89
- 'conversion summary. Only Git is supported.'
90
- ) do
91
- unless Git.inside_of_repository?
92
- fail '-m/--commit-message option is specified but not in a Git repository'
93
- end
94
-
95
- @generates_commit_message = true
96
- end
97
-
98
- parser.on(
99
- '-d', '--disable TYPE[,TYPE...]',
100
- 'Disable specific conversions.',
101
- 'Available conversion types:',
102
- ' expect_to_matcher (from `should`)',
103
- ' expect_to_receive (from `should_receive`)',
104
- ' allow_to_receive (from `stub`)',
105
- ' deprecated (e.g. from `stub!` to `stub`)',
106
- 'These are all enabled by default.'
107
- ) do |types|
108
- types.split(',').each do |type|
109
- config_attr = CONFIG_ATTRS_FOR_CLI_TYPES[type.to_sym]
110
- fail ArgumentError, "Unknown conversion type #{type.inspect}" unless config_attr
111
- @configuration.send(config_attr, false)
46
+ unless @configuration.skip_dynamic_analysis?
47
+ puts 'Copying project for dynamic analysis...'
48
+ DynamicAnalyzer.new(rspec_command: @configuration.rspec_command) do |analyzer|
49
+ puts "Running dynamic analysis with command \"#{analyzer.rspec_command}\"..."
50
+ runtime_data = analyzer.analyze(paths)
112
51
  end
52
+ puts
113
53
  end
114
54
 
115
- parser.on(
116
- '-n', '--negative-form FORM',
117
- 'Specify negative form of `to` that is used',
118
- 'in `expect(...).to` syntax.',
119
- 'Either `not_to` or `to_not`.',
120
- 'Default: not_to'
121
- ) do |form|
122
- @configuration.negative_form_of_to = form
55
+ FileFinder.find(paths).each do |file_path|
56
+ convert_file(file_path, runtime_data)
123
57
  end
58
+ end
124
59
 
125
- parser.on(
126
- '-p', '--no-parentheses-matcher-arg',
127
- 'Suppress parenthesizing argument of matcher',
128
- 'when converting operator to non-operator',
129
- 'in `expect` syntax. Note that it will be',
130
- 'parenthesized even if this option is',
131
- 'specified when parentheses are necessary to',
132
- 'keep the meaning of the expression.',
133
- 'By default, arguments of the following',
134
- 'operator matchers will be parenthesized.',
135
- ' `== 10` to `eq(10)`',
136
- ' `=~ /pattern/` to `match(/pattern/)`',
137
- ' `=~ [1, 2]` to `match_array([1, 2])`'
138
- ) do
139
- @configuration.parenthesize_matcher_arg = false
140
- end
60
+ def convert_file(file_path, runtime_data = nil)
61
+ puts "Converting #{file_path}"
141
62
 
142
- parser.on('--no-color', 'Disable color in the output.') do
143
- Sickill::Rainbow.enabled = false
144
- end
63
+ converter = Converter.new(@configuration, runtime_data, @report)
64
+ converter.convert_file!(file_path)
145
65
 
146
- parser.on('--version', 'Show Transpec version.') do
147
- puts Version.to_s
148
- exit
149
- end
66
+ @report.invalid_context_errors.concat(converter.invalid_context_errors)
150
67
 
151
- args = args.dup
152
- parser.parse!(args)
153
- args
154
- end
155
- # rubocop:enable MethodLength
156
-
157
- def target_files(paths)
158
- paths.reduce([]) do |file_paths, path|
159
- if File.directory?(path)
160
- file_paths.concat(ruby_files_in_directory(path))
161
- elsif File.file?(path)
162
- file_paths << path
163
- elsif !File.exists?(path)
164
- fail ArgumentError, "No such file or directory #{path.inspect}"
165
- end
68
+ converter.invalid_context_errors.each do |error|
69
+ warn_invalid_context_error(error)
166
70
  end
71
+ rescue Parser::SyntaxError => error
72
+ @report.syntax_errors << error
73
+ warn_syntax_error(error)
167
74
  end
168
75
 
169
76
  private
170
77
 
171
- def base_target_paths(args)
172
- return args unless args.empty?
173
- return ['spec'] if Dir.exists?('spec')
174
- fail ArgumentError, 'Specify target files or directories.'
175
- end
176
-
177
- def ruby_files_in_directory(directory_path)
178
- ruby_file_paths = []
179
-
180
- Find.find(directory_path) do |path|
181
- next unless File.file?(path)
182
- next unless File.extname(path) == '.rb'
183
- ruby_file_paths << path
184
- end
185
-
186
- ruby_file_paths
187
- end
188
-
189
78
  def fail_if_should_not_continue!
190
- return if forced?
191
-
192
- # TODO: Check each repository of target files / directories,
193
- # not only the current working directory.
79
+ return if @configuration.forced?
194
80
  return unless Git.command_available?
195
81
  return unless Git.inside_of_repository?
196
82
  return if Git.clean?
197
-
198
83
  fail 'The current Git repository is not clean. Aborting.'
199
84
  end
200
85
 
@@ -223,6 +108,11 @@ module Transpec
223
108
  puts ' git commit -eF .git/COMMIT_EDITMSG'
224
109
  end
225
110
 
111
+ def display_final_guide
112
+ puts
113
+ puts "Done! Now run #{'rspec'.bright} and check if all the converted specs pass."
114
+ end
115
+
226
116
  def warn_syntax_error(error)
227
117
  warn "Syntax error at #{error.diagnostic.location}. Skipping the file.".color(:red)
228
118
  end
@@ -5,23 +5,27 @@ module Transpec
5
5
  NEGATIVE_FORMS_OF_TO = ['not_to', 'to_not'].freeze
6
6
 
7
7
  PREDICATES = [
8
- :convert_to_expect_to_matcher,
9
- :convert_to_expect_to_receive,
10
- :convert_to_allow_to_receive,
11
- :replace_deprecated_method,
12
- :parenthesize_matcher_arg
8
+ [:convert_should, true],
9
+ [:convert_should_receive, true],
10
+ [:convert_stub, true],
11
+ [:convert_have_items, true],
12
+ [:convert_deprecated_method, true],
13
+ [:parenthesize_matcher_arg, true],
14
+ [:forced, false],
15
+ [:skip_dynamic_analysis, false],
16
+ [:generate_commit_message, false]
13
17
  ].freeze
14
18
 
15
- PREDICATES.each do |predicate|
19
+ PREDICATES.each do |predicate, _|
16
20
  attr_accessor predicate
17
21
  alias_method predicate.to_s + '?', predicate
18
22
  end
19
23
 
20
- attr_accessor :negative_form_of_to
24
+ attr_accessor :negative_form_of_to, :rspec_command
21
25
 
22
26
  def initialize
23
- PREDICATES.each do |predicate|
24
- instance_variable_set('@' + predicate.to_s, true)
27
+ PREDICATES.each do |predicate, default_value|
28
+ instance_variable_set('@' + predicate.to_s, default_value)
25
29
  end
26
30
 
27
31
  self.negative_form_of_to = 'not_to'
@@ -1,83 +1,48 @@
1
1
  # coding: utf-8
2
2
 
3
+ require 'transpec/base_rewriter'
4
+ require 'transpec/configuration'
3
5
  require 'transpec/report'
4
- require 'transpec/ast/builder'
5
6
  require 'transpec/ast/scanner'
6
- require 'transpec/configuration'
7
7
  require 'transpec/syntax'
8
8
  require 'transpec/syntax/be_close'
9
9
  require 'transpec/syntax/double'
10
- require 'transpec/syntax/matcher'
10
+ require 'transpec/syntax/expect'
11
11
  require 'transpec/syntax/method_stub'
12
12
  require 'transpec/syntax/raise_error'
13
13
  require 'transpec/syntax/rspec_configure'
14
14
  require 'transpec/syntax/should'
15
15
  require 'transpec/syntax/should_receive'
16
- require 'parser/current'
17
16
 
18
17
  module Transpec
19
- class Rewriter
20
- attr_reader :report, :invalid_context_errors
18
+ class Converter < BaseRewriter
19
+ attr_reader :configuration, :runtime_data, :report, :invalid_context_errors
21
20
 
22
- def initialize(configuration = Configuration.new, report = Report.new)
23
- @configuration = configuration
24
- @report = report
25
- @invalid_context_errors = []
26
- end
21
+ alias_method :convert_file!, :rewrite_file!
22
+ alias_method :convert, :rewrite
27
23
 
28
- def rewrite_file!(file_path)
29
- source = File.read(file_path)
30
- rewritten_source = rewrite(source, file_path)
31
- return if source == rewritten_source
32
- File.write(file_path, rewritten_source)
24
+ def initialize(configuration = nil, runtime_data = nil, report = nil)
25
+ @configuration = configuration || Configuration.new
26
+ @runtime_data = runtime_data
27
+ @report = report || Report.new
28
+ @invalid_context_errors = []
33
29
  end
34
30
 
35
- def rewrite(source, name = '(string)')
36
- source_buffer = create_source_buffer(source, name)
37
- ast = parse(source_buffer)
38
-
39
- @source_rewriter = Parser::Source::Rewriter.new(source_buffer)
40
- failed_overlapping_rewrite = false
41
- @source_rewriter.diagnostics.consumer = proc do
42
- failed_overlapping_rewrite = true
43
- fail OverlappedRewriteError
44
- end
45
-
31
+ def process(ast, source_rewriter)
46
32
  AST::Scanner.scan(ast) do |node, ancestor_nodes|
47
- dispatch_node(node, ancestor_nodes)
48
- end
49
-
50
- rewritten_source = @source_rewriter.process
51
-
52
- if failed_overlapping_rewrite
53
- rewriter = self.class.new(@configuration, @report)
54
- rewritten_source = rewriter.rewrite(rewritten_source, name)
33
+ dispatch_node(node, ancestor_nodes, source_rewriter)
55
34
  end
56
-
57
- rewritten_source
58
- end
59
-
60
- def create_source_buffer(source, name)
61
- source_buffer = Parser::Source::Buffer.new(name)
62
- source_buffer.source = source
63
- source_buffer
64
35
  end
65
36
 
66
- def parse(source_buffer)
67
- builder = AST::Builder.new
68
- parser = Parser::CurrentRuby.new(builder)
69
- ast = parser.parse(source_buffer)
70
- ast
71
- end
72
-
73
- def dispatch_node(node, ancestor_nodes)
74
- Syntax.all.each do |syntax_class|
75
- next unless syntax_class.target_node?(node)
37
+ def dispatch_node(node, ancestor_nodes, source_rewriter)
38
+ Syntax.standalone_syntaxes.each do |syntax_class|
39
+ next unless syntax_class.target_node?(node, @runtime_data)
76
40
 
77
41
  syntax = syntax_class.new(
78
42
  node,
79
43
  ancestor_nodes,
80
- @source_rewriter,
44
+ source_rewriter,
45
+ @runtime_data,
81
46
  @report
82
47
  )
83
48
 
@@ -92,50 +57,60 @@ module Transpec
92
57
  end
93
58
 
94
59
  def process_should(should)
95
- if @configuration.convert_to_expect_to_matcher?
60
+ if @configuration.convert_should?
96
61
  should.expectize!(
97
62
  @configuration.negative_form_of_to,
98
63
  @configuration.parenthesize_matcher_arg?
99
64
  )
100
65
  end
66
+
67
+ if should.have_matcher && @configuration.convert_have_items?
68
+ should.have_matcher.convert_to_standard_expectation!
69
+ end
70
+ end
71
+
72
+ def process_expect(expect)
73
+ if expect.have_matcher && @configuration.convert_have_items?
74
+ expect.have_matcher.convert_to_standard_expectation!
75
+ end
101
76
  end
102
77
 
103
78
  def process_should_receive(should_receive)
104
79
  if should_receive.useless_expectation?
105
- if @configuration.replace_deprecated_method?
106
- if @configuration.convert_to_allow_to_receive?
80
+ if @configuration.convert_deprecated_method?
81
+ if @configuration.convert_stub?
107
82
  should_receive.allowize_useless_expectation!(@configuration.negative_form_of_to)
108
83
  else
109
84
  should_receive.stubize_useless_expectation!
110
85
  end
111
- elsif @configuration.convert_to_expect_to_receive?
86
+ elsif @configuration.convert_should_receive?
112
87
  should_receive.expectize!(@configuration.negative_form_of_to)
113
88
  end
114
- elsif @configuration.convert_to_expect_to_receive?
89
+ elsif @configuration.convert_should_receive?
115
90
  should_receive.expectize!(@configuration.negative_form_of_to)
116
91
  end
117
92
  end
118
93
 
119
94
  def process_double(double)
120
- double.convert_to_double! if @configuration.replace_deprecated_method?
95
+ double.convert_to_double! if @configuration.convert_deprecated_method?
121
96
  end
122
97
 
123
98
  def process_method_stub(method_stub)
124
- if @configuration.convert_to_allow_to_receive?
99
+ if @configuration.convert_stub?
125
100
  method_stub.allowize!
126
- elsif @configuration.replace_deprecated_method?
127
- method_stub.replace_deprecated_method!
101
+ elsif @configuration.convert_deprecated_method?
102
+ method_stub.convert_deprecated_method!
128
103
  end
129
104
 
130
- method_stub.remove_allowance_for_no_message! if @configuration.replace_deprecated_method?
105
+ method_stub.remove_allowance_for_no_message! if @configuration.convert_deprecated_method?
131
106
  end
132
107
 
133
108
  def process_be_close(be_close)
134
- be_close.convert_to_be_within! if @configuration.replace_deprecated_method?
109
+ be_close.convert_to_be_within! if @configuration.convert_deprecated_method?
135
110
  end
136
111
 
137
112
  def process_raise_error(raise_error)
138
- if @configuration.replace_deprecated_method?
113
+ if @configuration.convert_deprecated_method?
139
114
  raise_error.remove_error_specification_with_negative_expectation!
140
115
  end
141
116
  end
@@ -151,20 +126,18 @@ module Transpec
151
126
  end
152
127
 
153
128
  def need_to_modify_expectation_syntax_configuration?(rspec_configure)
154
- return false unless @configuration.convert_to_expect_to_matcher?
129
+ return false unless @configuration.convert_should?
155
130
  rspec_configure.expectation_syntaxes == [:should]
156
131
  rescue Syntax::RSpecConfigure::UnknownSyntaxError
157
132
  false
158
133
  end
159
134
 
160
135
  def need_to_modify_mock_syntax_configuration?(rspec_configure)
161
- return false if !@configuration.convert_to_expect_to_receive? &&
162
- !@configuration.convert_to_allow_to_receive?
136
+ return false if !@configuration.convert_should_receive? &&
137
+ !@configuration.convert_stub?
163
138
  rspec_configure.mock_syntaxes == [:should]
164
139
  rescue Syntax::RSpecConfigure::UnknownSyntaxError
165
140
  false
166
141
  end
167
-
168
- class OverlappedRewriteError < StandardError; end
169
142
  end
170
143
  end
@@ -0,0 +1,94 @@
1
+ # coding: utf-8
2
+
3
+ require 'transpec/base_rewriter'
4
+ require 'transpec/util'
5
+ require 'transpec/ast/scanner'
6
+
7
+ module Transpec
8
+ class DynamicAnalyzer
9
+ class Rewriter < BaseRewriter
10
+ include Util
11
+
12
+ def process(ast, source_rewriter)
13
+ # TODO: Currently multitheading is not considered...
14
+ clear_requests!
15
+ collect_requests(ast)
16
+ process_requests(source_rewriter)
17
+ end
18
+
19
+ def requests
20
+ @requests ||= {}
21
+ end
22
+
23
+ def clear_requests!
24
+ @requests = nil
25
+ end
26
+
27
+ def register_request(node, key, instance_eval_string, eval_target_type = :object)
28
+ unless EVAL_TARGET_TYPES.include?(eval_target_type)
29
+ fail "Target type must be any of #{EVAL_TARGET_TYPES}"
30
+ end
31
+
32
+ requests[node] ||= {}
33
+ requests[node][key] = [eval_target_type, instance_eval_string]
34
+ end
35
+
36
+ private
37
+
38
+ def collect_requests(ast)
39
+ AST::Scanner.scan(ast) do |node, ancestor_nodes|
40
+ Syntax.standalone_syntaxes.each do |syntax_class|
41
+ syntax_class.register_request_for_dynamic_analysis(node, self)
42
+ next unless syntax_class.target_node?(node)
43
+ syntax = syntax_class.new(node, ancestor_nodes)
44
+ syntax.register_request_for_dynamic_analysis(self)
45
+ end
46
+ end
47
+ end
48
+
49
+ def process_requests(source_rewriter)
50
+ requests.each do |node, analysis_codes|
51
+ inject_analysis_method(node, analysis_codes, source_rewriter)
52
+ end
53
+ end
54
+
55
+ def inject_analysis_method(node, analysis_codes, source_rewriter)
56
+ source_range = node.loc.expression
57
+
58
+ front = "#{ANALYSIS_METHOD}(("
59
+ rear = format(
60
+ '), self, %s, __FILE__, %d, %d)',
61
+ hash_literal(analysis_codes), source_range.begin_pos, source_range.end_pos
62
+ )
63
+ rear = "\n" + indentation_of_line(source_range.end) + rear if contain_here_document?(node)
64
+
65
+ parent_node = node.parent_node
66
+
67
+ if parent_node && parent_node.type == :block && parent_node.children.first.equal?(node)
68
+ source_range = node.parent_node.loc.expression
69
+ end
70
+
71
+ source_rewriter.insert_before(source_range, front)
72
+ source_rewriter.insert_after(source_range, rear)
73
+ rescue OverlappedRewriteError # rubocop:disable HandleExceptions
74
+ end
75
+
76
+ # Hash#inspect generates invalid literal with following example:
77
+ #
78
+ # > eval({ :predicate? => 1 }.inspect)
79
+ # SyntaxError: (eval):1: syntax error, unexpected =>
80
+ # {:predicate?=>1}
81
+ # ^
82
+ def hash_literal(hash)
83
+ literal = '{ '
84
+
85
+ hash.each_with_index do |(key, value), index|
86
+ literal << ', ' unless index == 0
87
+ literal << "#{key.inspect} => #{value.inspect}"
88
+ end
89
+
90
+ literal << ' }'
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+
3
+ require 'pathname'
4
+
5
+ module Transpec
6
+ class DynamicAnalyzer
7
+ class RuntimeData
8
+ attr_reader :hash
9
+
10
+ def initialize(hash = {})
11
+ @hash = hash
12
+ end
13
+
14
+ def [](node)
15
+ @hash[node_id(node)]
16
+ end
17
+
18
+ def node_id(node)
19
+ source_range = node.loc.expression
20
+ source_buffer = source_range.source_buffer
21
+ absolute_path = File.expand_path(source_buffer.name)
22
+ relative_path = Pathname.new(absolute_path).relative_path_from(Pathname.pwd).to_s
23
+ [relative_path, source_range.begin_pos, source_range.end_pos].join('_')
24
+ end
25
+ end
26
+ end
27
+ end