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
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