better_html 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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