better_html 1.0.1 → 1.0.2

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/lib/better_html/ast/iterator.rb +3 -3
  3. data/lib/better_html/ast/node.rb +6 -2
  4. data/lib/better_html/errors.rb +2 -11
  5. data/lib/better_html/test_helper/ruby_node.rb +103 -0
  6. data/lib/better_html/test_helper/safe_erb/allowed_script_type.rb +29 -0
  7. data/lib/better_html/test_helper/safe_erb/base.rb +56 -0
  8. data/lib/better_html/test_helper/safe_erb/no_javascript_tag_helper.rb +34 -0
  9. data/lib/better_html/test_helper/safe_erb/no_statements.rb +40 -0
  10. data/lib/better_html/test_helper/safe_erb/script_interpolation.rb +65 -0
  11. data/lib/better_html/test_helper/safe_erb/tag_interpolation.rb +163 -0
  12. data/lib/better_html/test_helper/safe_erb_tester.rb +32 -285
  13. data/lib/better_html/tokenizer/location.rb +1 -1
  14. data/lib/better_html/version.rb +1 -1
  15. data/test/better_html/errors_test.rb +13 -0
  16. data/test/better_html/test_helper/ruby_node_test.rb +288 -0
  17. data/test/better_html/test_helper/safe_erb/allowed_script_type_test.rb +45 -0
  18. data/test/better_html/test_helper/safe_erb/no_javascript_tag_helper_test.rb +37 -0
  19. data/test/better_html/test_helper/safe_erb/no_statements_test.rb +128 -0
  20. data/test/better_html/test_helper/safe_erb/script_interpolation_test.rb +149 -0
  21. data/test/better_html/test_helper/safe_erb/tag_interpolation_test.rb +295 -0
  22. data/test/test_helper.rb +1 -0
  23. metadata +23 -7
  24. data/lib/better_html/test_helper/ruby_expr.rb +0 -117
  25. data/test/better_html/test_helper/ruby_expr_test.rb +0 -283
  26. data/test/better_html/test_helper/safe_erb_tester_test.rb +0 -450
@@ -1,6 +1,11 @@
1
- require 'better_html/test_helper/ruby_expr'
2
- require 'better_html/test_helper/safety_error'
3
1
  require 'better_html/parser'
2
+ require 'better_html/test_helper/safety_error'
3
+ require 'better_html/test_helper/safe_erb/base'
4
+ require 'better_html/test_helper/safe_erb/no_statements'
5
+ require 'better_html/test_helper/safe_erb/allowed_script_type'
6
+ require 'better_html/test_helper/safe_erb/no_javascript_tag_helper'
7
+ require 'better_html/test_helper/safe_erb/tag_interpolation'
8
+ require 'better_html/test_helper/safe_erb/script_interpolation'
4
9
  require 'better_html/tree/tag'
5
10
 
6
11
  module BetterHtml
@@ -30,292 +35,34 @@ Always use raw and to_json together within <script> tags:
30
35
  EOF
31
36
 
32
37
  def assert_erb_safety(data, **options)
33
- tester = Tester.new(data, **options)
34
-
35
- message = ""
36
- tester.errors.each do |error|
37
- message << <<~EOL
38
- On line #{error.location.line}
39
- #{error.message}
40
- #{error.location.line_source_with_underline}\n
38
+ options = options.present? ? options.dup : {}
39
+ options[:template_language] ||= :html
40
+ parser = BetterHtml::Parser.new(data, options)
41
+
42
+ tester_classes = [
43
+ SafeErb::NoStatements,
44
+ SafeErb::AllowedScriptType,
45
+ SafeErb::NoJavascriptTagHelper,
46
+ SafeErb::TagInterpolation,
47
+ SafeErb::ScriptInterpolation,
48
+ ]
49
+
50
+ testers = tester_classes.map do |tester_klass|
51
+ tester = tester_klass.new(parser)
52
+ end
53
+ testers.each(&:validate)
54
+ errors = testers.map(&:errors).flatten
55
+
56
+ messages = errors.map do |error|
57
+ <<~EOL
58
+ On line #{error.location.line}
59
+ #{error.message}
60
+ #{error.location.line_source_with_underline}\n
41
61
  EOL
42
62
  end
63
+ messages << SAFETY_TIPS
43
64
 
44
- message << SAFETY_TIPS
45
-
46
- assert_predicate tester.errors, :empty?, message
47
- end
48
-
49
- private
50
-
51
- class Tester
52
- attr_reader :errors
53
-
54
- VALID_JAVASCRIPT_TAG_TYPES = ['text/javascript', 'text/template', 'text/html']
55
-
56
- def initialize(data, config: BetterHtml.config, **options)
57
- @data = data
58
- @config = config
59
- @errors = Errors.new
60
- @options = options.present? ? options.dup : {}
61
- @options[:template_language] ||= :html
62
- @parser = BetterHtml::Parser.new(data, @options.slice(:template_language))
63
- validate!
64
- end
65
-
66
- def add_error(message, location:)
67
- @errors.add(SafetyError.new(message, location: location))
68
- end
69
-
70
- def validate!
71
- @parser.nodes_with_type(:tag).each do |tag_node|
72
- tag = Tree::Tag.from_node(tag_node)
73
- next if tag.closing?
74
-
75
- validate_tag_attributes(tag)
76
-
77
- if tag.name == 'script'
78
- index = @parser.ast.to_a.find_index(tag_node)
79
- next_node = @parser.ast.to_a[index + 1]
80
- if next_node.type == :text
81
- if (tag.attributes['type']&.value || "text/javascript") == "text/javascript"
82
- validate_script_tag_content(next_node)
83
- end
84
- validate_no_statements(next_node) unless tag.attributes['type']&.value == "text/html"
85
- end
86
-
87
- validate_javascript_tag_type(tag)
88
- end
89
- end
90
-
91
- @parser.nodes_with_type(:text).each do |node|
92
- validate_text_node(node)
93
-
94
- if @parser.template_language == :javascript
95
- validate_script_tag_content(node)
96
- validate_no_statements(node)
97
- else
98
- validate_no_javascript_tag(node)
99
- end
100
- end
101
-
102
- @parser.nodes_with_type(:cdata, :comment).each do |node|
103
- validate_no_statements(node)
104
- end
105
- end
106
-
107
- def erb_nodes(node)
108
- Enumerator.new do |yielder|
109
- next if node.nil?
110
- node.descendants(:erb).each do |erb_node|
111
- indicator_node, _, code_node, _ = *erb_node
112
- yielder.yield(erb_node, indicator_node, code_node)
113
- end
114
- end
115
- end
116
-
117
- def validate_javascript_tag_type(tag)
118
- return unless type_attribute = tag.attributes['type']
119
- return if VALID_JAVASCRIPT_TAG_TYPES.include?(type_attribute.value)
120
-
121
- add_error(
122
- "#{type_attribute.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}",
123
- location: type_attribute.loc
124
- )
125
- end
126
-
127
- def validate_tag_attributes(tag)
128
- tag.attributes.each do |attribute|
129
- erb_nodes(attribute.value_node).each do |erb_node, indicator_node, code_node|
130
- next if indicator_node.nil?
131
-
132
- indicator = indicator_node.loc.source
133
- source = code_node.loc.source
134
-
135
- if indicator == '='
136
- begin
137
- expr = RubyExpr.parse(source)
138
- validate_tag_expression(code_node, expr, attribute.name)
139
- rescue RubyExpr::ParseError
140
- nil
141
- end
142
- elsif indicator == '=='
143
- add_error(
144
- "erb interpolation with '<%==' inside html attribute is never safe",
145
- location: erb_node.loc
146
- )
147
- end
148
- end
149
- end
150
- end
151
-
152
- def validate_text_node(text_node)
153
- erb_nodes(text_node).each do |erb_node, indicator_node, code_node|
154
- indicator = indicator_node&.loc&.source
155
- next if indicator == '#'
156
- source = code_node.loc.source
157
-
158
- begin
159
- expr = RubyExpr.parse(source)
160
- validate_ruby_helper(code_node, expr)
161
- rescue RubyExpr::ParseError
162
- nil
163
- end
164
- end
165
- end
166
-
167
- def validate_ruby_helper(parent_token, expr)
168
- expr.traverse(only: [:send, :csend]) do |send_node|
169
- expr.each_child_node(send_node, only: :hash) do |hash_node|
170
- expr.each_child_node(hash_node, only: :pair) do |pair_node|
171
- validate_ruby_helper_hash_entry(parent_token, expr, nil, *pair_node.children)
172
- end
173
- end
174
- end
175
- end
176
-
177
- def validate_ruby_helper_hash_entry(parent_token, expr, key_prefix, key_node, value_node)
178
- return unless [:sym, :str].include?(key_node.type)
179
- key = [key_prefix, key_node.children.first.to_s].compact.join('-').dasherize
180
- case value_node.type
181
- when :dstr
182
- validate_ruby_helper_hash_value(parent_token, expr, key, value_node)
183
- when :hash
184
- if key == 'data'
185
- expr.each_child_node(value_node, only: :pair) do |pair_node|
186
- validate_ruby_helper_hash_entry(parent_token, expr, key, *pair_node.children)
187
- end
188
- end
189
- end
190
- end
191
-
192
- def validate_ruby_helper_hash_value(parent_token, expr, attr_name, hash_value)
193
- expr.each_child_node(hash_value, only: :begin) do |child|
194
- validate_tag_expression(parent_token, RubyExpr.new(child), attr_name)
195
- end
196
- end
197
-
198
- def validate_tag_expression(parent_token, expr, attr_name)
199
- return if expr.static_value?
200
-
201
- if @config.javascript_attribute_name?(attr_name) && expr.calls.empty?
202
- add_error(
203
- "erb interpolation in javascript attribute must call '(...).to_json'",
204
- location: Tokenizer::Location.new(
205
- @data,
206
- parent_token.loc.start + expr.start,
207
- parent_token.loc.start + expr.end - 1
208
- )
209
- )
210
- return
211
- end
212
-
213
- expr.calls.each do |call|
214
- if call.method == :raw
215
- add_error(
216
- "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe",
217
- location: Tokenizer::Location.new(
218
- @data,
219
- parent_token.loc.start + expr.start,
220
- parent_token.loc.start + expr.end - 1
221
- )
222
- )
223
- elsif call.method == :html_safe
224
- add_error(
225
- "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe",
226
- location: Tokenizer::Location.new(
227
- @data,
228
- parent_token.loc.start + expr.start,
229
- parent_token.loc.start + expr.end - 1
230
- )
231
- )
232
- elsif @config.javascript_attribute_name?(attr_name) && !@config.javascript_safe_method?(call.method)
233
- add_error(
234
- "erb interpolation in javascript attribute must call '(...).to_json'",
235
- location: Tokenizer::Location.new(
236
- @data,
237
- parent_token.loc.start + expr.start,
238
- parent_token.loc.start + expr.end - 1
239
- )
240
- )
241
- end
242
- end
243
- end
244
-
245
- def validate_script_tag_content(node)
246
- erb_nodes(node).each do |erb_node, indicator_node, code_node|
247
- next unless indicator_node.present?
248
- indicator = indicator_node.loc.source
249
- next if indicator == '#'
250
- source = code_node.loc.source
251
-
252
- begin
253
- expr = RubyExpr.parse(source)
254
- validate_script_expression(erb_node, expr)
255
- rescue RubyExpr::ParseError
256
- end
257
- end
258
- end
259
-
260
- def validate_script_expression(parent_node, expr)
261
- if expr.calls.empty?
262
- add_error(
263
- "erb interpolation in javascript tag must call '(...).to_json'",
264
- location: parent_node.loc,
265
- )
266
- return
267
- end
268
-
269
- expr.calls.each do |call|
270
- if call.method == :raw
271
- call.arguments.each do |argument_node|
272
- arguments_expr = RubyExpr.new(argument_node)
273
- validate_script_expression(parent_node, arguments_expr)
274
- end
275
- elsif call.method == :html_safe
276
- instance_expr = RubyExpr.new(call.instance)
277
- validate_script_expression(parent_node, instance_expr)
278
- elsif !@config.javascript_safe_method?(call.method)
279
- add_error(
280
- "erb interpolation in javascript tag must call '(...).to_json'",
281
- location: parent_node.loc,
282
- )
283
- end
284
- end
285
- end
286
-
287
- def validate_no_statements(node)
288
- erb_nodes(node).each do |erb_node, indicator_node, code_node|
289
- next unless indicator_node.nil?
290
- source = code_node.loc.source
291
- next if /\A\s*end/m === source
292
-
293
- add_error(
294
- "erb statement not allowed here; did you mean '<%=' ?",
295
- location: erb_node.loc,
296
- )
297
- end
298
- end
299
-
300
- def validate_no_javascript_tag(node)
301
- erb_nodes(node).each do |erb_node, indicator_node, code_node|
302
- indicator = indicator_node&.loc&.source
303
- next if indicator == '#'
304
- source = code_node.loc.source
305
-
306
- begin
307
- expr = RubyExpr.parse(source)
308
-
309
- if expr.calls.size == 1 && expr.calls.first.method == :javascript_tag
310
- add_error(
311
- "'javascript_tag do' syntax is deprecated; use inline <script> instead",
312
- location: erb_node.loc,
313
- )
314
- end
315
- rescue RubyExpr::ParseError
316
- end
317
- end
318
- end
65
+ assert_predicate errors, :empty?, messages.join
319
66
  end
320
67
  end
321
68
  end
@@ -1,7 +1,7 @@
1
1
  module BetterHtml
2
2
  module Tokenizer
3
3
  class Location
4
- attr_accessor :start, :stop
4
+ attr_accessor :document, :start, :stop
5
5
 
6
6
  def initialize(document, start, stop)
7
7
  raise ArgumentError, "start location #{start} is out of range for document of size #{document.size}" if start > document.size
@@ -1,3 +1,3 @@
1
1
  module BetterHtml
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.2"
3
3
  end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+ require 'better_html/errors'
3
+
4
+ module BetterHtml
5
+ class ErrorsTest < ActiveSupport::TestCase
6
+ test "add" do
7
+ e = Errors.new
8
+ e.add("foo")
9
+ assert_equal 1, e.size
10
+ assert_equal "foo", e.first
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,288 @@
1
+ require 'test_helper'
2
+ require 'better_html/test_helper/ruby_node'
3
+
4
+ module BetterHtml
5
+ module TestHelper
6
+ class RubyNodeTest < ActiveSupport::TestCase
7
+ include ::AST::Sexp
8
+
9
+ test "simple call" do
10
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo")
11
+ assert_equal 1, expr.return_values.count
12
+ assert_nil expr.return_values.first.receiver
13
+ assert_equal :foo, expr.return_values.first.method_name
14
+ assert_equal [], expr.return_values.first.arguments
15
+ refute_predicate expr, :static_return_value?
16
+ end
17
+
18
+ test "instance call" do
19
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo.bar")
20
+ assert_equal 1, expr.return_values.count
21
+ assert_equal s(:send, nil, :foo), expr.return_values.first.receiver
22
+ assert_equal :bar, expr.return_values.first.method_name
23
+ assert_equal [], expr.return_values.first.arguments
24
+ refute_predicate expr, :static_return_value?
25
+ end
26
+
27
+ test "instance call with arguments" do
28
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo(x).bar")
29
+ assert_equal 1, expr.return_values.count
30
+ assert_equal s(:send, nil, :foo, s(:send, nil, :x)), expr.return_values.first.receiver
31
+ assert_equal :bar, expr.return_values.first.method_name
32
+ assert_equal [], expr.return_values.first.arguments
33
+ refute_predicate expr, :static_return_value?
34
+ end
35
+
36
+ test "instance call with parenthesis" do
37
+ expr = BetterHtml::TestHelper::RubyNode.parse("(foo).bar")
38
+ assert_equal 1, expr.return_values.count
39
+ assert_equal s(:begin, s(:send, nil, :foo)), expr.return_values.first.receiver
40
+ assert_equal :bar, expr.return_values.first.method_name
41
+ assert_equal [], expr.return_values.first.arguments
42
+ refute_predicate expr, :static_return_value?
43
+ end
44
+
45
+ test "instance call with parenthesis 2" do
46
+ expr = BetterHtml::TestHelper::RubyNode.parse("(foo)")
47
+ assert_equal 1, expr.return_values.count
48
+ assert_nil expr.return_values.first.receiver
49
+ assert_equal :foo, expr.return_values.first.method_name
50
+ assert_equal [], expr.return_values.first.arguments
51
+ refute_predicate expr, :static_return_value?
52
+ end
53
+
54
+ test "command call" do
55
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo bar")
56
+ assert_equal 1, expr.return_values.count
57
+ assert_nil expr.return_values.first.receiver
58
+ assert_equal :foo, expr.return_values.first.method_name
59
+ assert_equal [s(:send, nil, :bar)], expr.return_values.first.arguments
60
+ refute_predicate expr, :static_return_value?
61
+ end
62
+
63
+ test "command call with block" do
64
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo bar do")
65
+ assert_equal 1, expr.return_values.count
66
+ assert_nil expr.return_values.first.receiver
67
+ assert_equal :foo, expr.return_values.first.method_name
68
+ assert_equal [s(:send, nil, :bar)], expr.return_values.first.arguments
69
+ refute_predicate expr, :static_return_value?
70
+ end
71
+
72
+ test "call with parameters" do
73
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo(bar)")
74
+ assert_equal 1, expr.return_values.count
75
+ assert_nil expr.return_values.first.receiver
76
+ assert_equal :foo, expr.return_values.first.method_name
77
+ assert_equal [s(:send, nil, :bar)], expr.return_values.first.arguments
78
+ refute_predicate expr, :static_return_value?
79
+ end
80
+
81
+ test "instance call with parameters" do
82
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo.bar(baz, x)")
83
+ assert_equal 1, expr.return_values.count
84
+ assert_equal s(:send, nil, :foo), expr.return_values.first.receiver
85
+ assert_equal :bar, expr.return_values.first.method_name
86
+ assert_equal [s(:send, nil, :baz), s(:send, nil, :x)], expr.return_values.first.arguments
87
+ refute_predicate expr, :static_return_value?
88
+ end
89
+
90
+ test "call with parameters with if conditional modifier" do
91
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo(bar) if something?")
92
+ assert_equal 1, expr.return_values.count
93
+ assert_nil expr.return_values.first.receiver
94
+ assert_equal :foo, expr.return_values.first.method_name
95
+ assert_equal [s(:send, nil, :bar)], expr.return_values.first.arguments
96
+ refute_predicate expr, :static_return_value?
97
+ end
98
+
99
+ test "call with parameters with unless conditional modifier" do
100
+ expr = BetterHtml::TestHelper::RubyNode.parse("foo(bar) unless something?")
101
+ assert_equal 1, expr.return_values.count
102
+ assert_nil expr.return_values.first.receiver
103
+ assert_equal :foo, expr.return_values.first.method_name
104
+ assert_equal [s(:send, nil, :bar)], expr.return_values.first.arguments
105
+ refute_predicate expr, :static_return_value?
106
+ end
107
+
108
+ test "expression call in ternary" do
109
+ expr = BetterHtml::TestHelper::RubyNode.parse("something? ? foo : bar")
110
+ assert_equal 2, expr.return_values.count
111
+ refute_predicate expr, :static_return_value?
112
+
113
+ assert_nil expr.return_values.to_a[0].receiver
114
+ assert_equal :foo, expr.return_values.to_a[0].method_name
115
+ assert_equal [], expr.return_values.to_a[0].arguments
116
+
117
+ assert_nil expr.return_values.to_a[1].receiver
118
+ assert_equal :bar, expr.return_values.to_a[1].method_name
119
+ assert_equal [], expr.return_values.to_a[1].arguments
120
+ end
121
+
122
+ test "expression call with args in ternary" do
123
+ expr = BetterHtml::TestHelper::RubyNode.parse("something? ? foo(x) : bar(x)")
124
+ assert_equal 2, expr.return_values.count
125
+
126
+ assert_nil expr.return_values.to_a[0].receiver
127
+ assert_equal :foo, expr.return_values.to_a[0].method_name
128
+ assert_equal [s(:send, nil, :x)], expr.return_values.to_a[0].arguments
129
+
130
+ assert_nil expr.return_values.to_a[1].receiver
131
+ assert_equal :bar, expr.return_values.to_a[1].method_name
132
+ assert_equal [s(:send, nil, :x)], expr.return_values.to_a[1].arguments
133
+ refute_predicate expr, :static_return_value?
134
+ end
135
+
136
+ test "string without interpolation" do
137
+ expr = BetterHtml::TestHelper::RubyNode.parse('"foo"')
138
+ assert_equal 1, expr.return_values.count
139
+ assert_equal [s(:str, "foo")], expr.return_values.to_a
140
+ assert_predicate expr, :static_return_value?
141
+ end
142
+
143
+ test "string with interpolation" do
144
+ expr = BetterHtml::TestHelper::RubyNode.parse('"foo #{bar}"')
145
+ method_calls = expr.return_values.select(&:method_call?)
146
+ assert_equal 1, method_calls.count
147
+ assert_nil method_calls.first.receiver
148
+ assert_equal :bar, method_calls.first.method_name
149
+ assert_equal [], method_calls.first.arguments
150
+ refute_predicate expr, :static_return_value?
151
+ end
152
+
153
+ test "ternary in string with interpolation" do
154
+ expr = BetterHtml::TestHelper::RubyNode.parse('"foo #{foo? ? bar : baz}"')
155
+ method_calls = expr.return_values.select(&:method_call?)
156
+ assert_equal 2, method_calls.count
157
+
158
+ assert_nil method_calls.first.receiver
159
+ assert_equal :bar, method_calls.first.method_name
160
+ assert_equal [], method_calls.first.arguments
161
+
162
+ assert_nil method_calls.last.receiver
163
+ assert_equal :baz, method_calls.last.method_name
164
+ assert_equal [], method_calls.first.arguments
165
+ refute_predicate expr, :static_return_value?
166
+ end
167
+
168
+ test "assignment to variable" do
169
+ expr = BetterHtml::TestHelper::RubyNode.parse('x = foo.bar')
170
+ assert_equal 1, expr.return_values.count
171
+ assert_equal s(:send, nil, :foo), expr.return_values.first.receiver
172
+ assert_equal :bar, expr.return_values.first.method_name
173
+ assert_equal [], expr.return_values.first.arguments
174
+ refute_predicate expr, :static_return_value?
175
+ end
176
+
177
+ test "assignment to variable with command call" do
178
+ expr = BetterHtml::TestHelper::RubyNode.parse('raw x = foo.bar')
179
+ assert_equal 1, expr.return_values.count
180
+ assert_nil expr.return_values.first.receiver
181
+ assert_equal :raw, expr.return_values.first.method_name
182
+ assert_equal [s(:lvasgn, :x, s(:send, s(:send, nil, :foo), :bar))], expr.return_values.first.arguments
183
+ refute_predicate expr, :static_return_value?
184
+ end
185
+
186
+ test "assignment with instance call" do
187
+ expr = BetterHtml::TestHelper::RubyNode.parse('(x = foo).bar')
188
+ assert_equal 1, expr.return_values.count
189
+ assert_equal s(:begin, s(:lvasgn, :x, s(:send, nil, :foo))), expr.return_values.first.receiver
190
+ assert_equal :bar, expr.return_values.first.method_name
191
+ assert_equal [], expr.return_values.first.arguments
192
+ refute_predicate expr, :static_return_value?
193
+ end
194
+
195
+ test "assignment to multiple variables" do
196
+ expr = BetterHtml::TestHelper::RubyNode.parse('x, y = foo.bar')
197
+ assert_equal 1, expr.return_values.count
198
+ assert_equal s(:send, nil, :foo), expr.return_values.first.receiver
199
+ assert_equal :bar, expr.return_values.first.method_name
200
+ assert_equal [], expr.return_values.first.arguments
201
+ refute_predicate expr, :static_return_value?
202
+ end
203
+
204
+ test "safe navigation operator" do
205
+ expr = BetterHtml::TestHelper::RubyNode.parse('foo&.bar')
206
+ assert_equal 1, expr.return_values.count
207
+ assert_equal s(:send, nil, :foo), expr.return_values.to_a[0].receiver
208
+ assert_equal :bar, expr.return_values.to_a[0].method_name
209
+ assert_equal [], expr.return_values.to_a[0].arguments
210
+ refute_predicate expr, :static_return_value?
211
+ end
212
+
213
+ test "instance variable" do
214
+ expr = BetterHtml::TestHelper::RubyNode.parse('@foo')
215
+ assert_equal 0, expr.return_values.select(&:method_call?).count
216
+ refute_predicate expr, :static_return_value?
217
+ end
218
+
219
+ test "instance method on variable" do
220
+ expr = BetterHtml::TestHelper::RubyNode.parse('@foo.bar')
221
+ assert_equal 1, expr.return_values.count
222
+ assert_equal s(:ivar, :@foo), expr.return_values.first.receiver
223
+ assert_equal :bar, expr.return_values.first.method_name
224
+ assert_equal [], expr.return_values.first.arguments
225
+ refute_predicate expr, :static_return_value?
226
+ end
227
+
228
+ test "index into array" do
229
+ expr = BetterHtml::TestHelper::RubyNode.parse('local_assigns[:text_class] if local_assigns[:text_class]')
230
+ assert_equal 1, expr.return_values.count
231
+ assert_equal s(:send, nil, :local_assigns), expr.return_values.first.receiver
232
+ assert_equal :[], expr.return_values.first.method_name
233
+ assert_equal [s(:sym, :text_class)], expr.return_values.first.arguments
234
+ refute_predicate expr, :static_return_value?
235
+ end
236
+
237
+ test "static_return_value? for ivar" do
238
+ expr = BetterHtml::TestHelper::RubyNode.parse('@foo')
239
+ refute_predicate expr, :static_return_value?
240
+ end
241
+
242
+ test "static_return_value? for str" do
243
+ expr = BetterHtml::TestHelper::RubyNode.parse("'str'")
244
+ assert_predicate expr, :static_return_value?
245
+ end
246
+
247
+ test "static_return_value? for int" do
248
+ expr = BetterHtml::TestHelper::RubyNode.parse("1")
249
+ assert_predicate expr, :static_return_value?
250
+ end
251
+
252
+ test "static_return_value? for bool" do
253
+ expr = BetterHtml::TestHelper::RubyNode.parse("true")
254
+ assert_predicate expr, :static_return_value?
255
+ end
256
+
257
+ test "static_return_value? for nil" do
258
+ expr = BetterHtml::TestHelper::RubyNode.parse("nil")
259
+ assert_predicate expr, :static_return_value?
260
+ end
261
+
262
+ test "static_return_value? for dstr without interpolate" do
263
+ expr = BetterHtml::TestHelper::RubyNode.parse('"str"')
264
+ assert_predicate expr, :static_return_value?
265
+ end
266
+
267
+ test "static_return_value? for dstr with interpolate" do
268
+ expr = BetterHtml::TestHelper::RubyNode.parse('"str #{foo}"')
269
+ refute_predicate expr, :static_return_value?
270
+ end
271
+
272
+ test "static_return_value? with safe ternary" do
273
+ expr = BetterHtml::TestHelper::RubyNode.parse('foo ? \'a\' : \'b\'')
274
+ assert_predicate expr, :static_return_value?
275
+ end
276
+
277
+ test "static_return_value? with safe conditional" do
278
+ expr = BetterHtml::TestHelper::RubyNode.parse('\'foo\' if bar?')
279
+ assert_predicate expr, :static_return_value?
280
+ end
281
+
282
+ test "static_return_value? with safe assignment" do
283
+ expr = BetterHtml::TestHelper::RubyNode.parse('x = \'foo\'')
284
+ assert_predicate expr, :static_return_value?
285
+ end
286
+ end
287
+ end
288
+ end