better_html 0.0.12 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/better_html.rb +0 -2
  3. data/lib/better_html/ast/iterator.rb +32 -0
  4. data/lib/better_html/ast/node.rb +14 -0
  5. data/lib/better_html/better_erb/runtime_checks.rb +3 -3
  6. data/lib/better_html/config.rb +12 -0
  7. data/lib/better_html/parser.rb +286 -0
  8. data/lib/better_html/test_helper/ruby_expr.rb +8 -5
  9. data/lib/better_html/test_helper/safe_erb_tester.rb +121 -108
  10. data/lib/better_html/test_helper/safe_lodash_tester.rb +44 -42
  11. data/lib/better_html/tokenizer/base_erb.rb +79 -0
  12. data/lib/better_html/tokenizer/html_erb.rb +31 -0
  13. data/lib/better_html/{node_iterator → tokenizer}/html_lodash.rb +30 -34
  14. data/lib/better_html/tokenizer/javascript_erb.rb +15 -0
  15. data/lib/better_html/{node_iterator → tokenizer}/location.rb +9 -3
  16. data/lib/better_html/tokenizer/token.rb +16 -0
  17. data/lib/better_html/tokenizer/token_array.rb +54 -0
  18. data/lib/better_html/tree/attribute.rb +31 -0
  19. data/lib/better_html/tree/attributes_list.rb +25 -0
  20. data/lib/better_html/tree/tag.rb +39 -0
  21. data/lib/better_html/version.rb +1 -1
  22. data/test/better_html/parser_test.rb +279 -0
  23. data/test/better_html/test_helper/safe_erb_tester_test.rb +11 -0
  24. data/test/better_html/test_helper/safe_lodash_tester_test.rb +11 -1
  25. data/test/better_html/tokenizer/html_erb_test.rb +158 -0
  26. data/test/better_html/tokenizer/html_lodash_test.rb +98 -0
  27. data/test/better_html/tokenizer/location_test.rb +57 -0
  28. data/test/better_html/tokenizer/token_array_test.rb +144 -0
  29. data/test/better_html/tokenizer/token_test.rb +15 -0
  30. metadata +45 -30
  31. data/lib/better_html/node_iterator.rb +0 -144
  32. data/lib/better_html/node_iterator/attribute.rb +0 -34
  33. data/lib/better_html/node_iterator/base.rb +0 -27
  34. data/lib/better_html/node_iterator/cdata.rb +0 -8
  35. data/lib/better_html/node_iterator/comment.rb +0 -8
  36. data/lib/better_html/node_iterator/content_node.rb +0 -13
  37. data/lib/better_html/node_iterator/element.rb +0 -26
  38. data/lib/better_html/node_iterator/html_erb.rb +0 -70
  39. data/lib/better_html/node_iterator/javascript_erb.rb +0 -55
  40. data/lib/better_html/node_iterator/text.rb +0 -8
  41. data/lib/better_html/node_iterator/token.rb +0 -8
  42. data/lib/better_html/tree.rb +0 -113
  43. data/test/better_html/node_iterator/html_erb_test.rb +0 -116
  44. data/test/better_html/node_iterator/html_lodash_test.rb +0 -132
  45. data/test/better_html/node_iterator/location_test.rb +0 -36
  46. data/test/better_html/node_iterator_test.rb +0 -221
  47. data/test/better_html/tree_test.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b31eee572de6668e1791c716a07c9d93658aa566
4
- data.tar.gz: 6dd1906d36f68f59fbe4d6386f0d0e7e94ce244b
3
+ metadata.gz: 16bc95c4f55d720a177e9849dea00e7d83c66455
4
+ data.tar.gz: 376b80e51f2379ce6c5640e834de1c62aa990ae5
5
5
  SHA512:
6
- metadata.gz: 1c96cfded69fd94221810490e37b279da7e57323062a06416dd3522d8b58971cd2bb727690d0373af647871170698ef4d4b8830f13e8fa3538699224b9c33dc8
7
- data.tar.gz: 49d55494ae87ce77e2cd45883a8b37b1c999badf3fc8e0a7c06ba4631a4c1d0a698243a4810ec2f6fbd80f88508b3ed4b61e0e54e1d8c08a036c01bea664aa14
6
+ metadata.gz: bf07d75143cfde43414bb3e8a5c2520b527f047d0c796c7ff97e60aa2d23d0bc03ea1f06c72eab5db5f4c10cdb01eccf0c6abe585b0ae394dc662731599e296b
7
+ data.tar.gz: b7dd6bfc56b0a36d773dabbe13a7ad922bc9c5cd322f4a240ba8d9064c8753ec4efca3fb5b1a7c63d36291b60dd48ec142521b3af98811f3a017d5d62f90ab97
@@ -21,7 +21,5 @@ require 'better_html/config'
21
21
  require 'better_html/helpers'
22
22
  require 'better_html/errors'
23
23
  require 'better_html/html_attributes'
24
- require 'better_html/node_iterator'
25
- require 'better_html/tree'
26
24
 
27
25
  require 'better_html/railtie' if defined?(Rails)
@@ -0,0 +1,32 @@
1
+ require 'ast'
2
+ require 'active_support/core_ext/array/wrap'
3
+
4
+ module BetterHtml
5
+ module AST
6
+ class Iterator
7
+ def initialize(types, &block)
8
+ @types = Array.wrap(types)
9
+ @block = block
10
+ end
11
+
12
+ def traverse(node)
13
+ return unless node.is_a?(::AST::Node)
14
+ @block.call(node) if @types.include?(node.type)
15
+ traverse_all(node)
16
+ end
17
+
18
+ def traverse_all(nodes)
19
+ nodes.to_a.each do |node|
20
+ traverse(node) if node.is_a?(::AST::Node)
21
+ end
22
+ end
23
+
24
+ def self.descendants(root_node, type, &block)
25
+ Enumerator.new do |yielder|
26
+ t = new(type) { |node| yielder << node }
27
+ t.traverse(root_node)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ require 'ast'
2
+ require_relative 'iterator'
3
+
4
+ module BetterHtml
5
+ module AST
6
+ class Node < ::AST::Node
7
+ attr_reader :loc
8
+
9
+ def descendants(type, &block)
10
+ AST::Iterator.descendants(self, type, &block)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -111,7 +111,7 @@ class BetterHtml::BetterErb
111
111
  end
112
112
 
113
113
  def check_tag_name(type, start, stop, line, column)
114
- text = @parser.extract(start, stop)
114
+ text = @parser.document[start...stop]
115
115
  return if text.upcase == "!DOCTYPE"
116
116
  return if @config.partial_tag_name_pattern === text
117
117
 
@@ -122,7 +122,7 @@ class BetterHtml::BetterErb
122
122
  end
123
123
 
124
124
  def check_attribute_name(type, start, stop, line, column)
125
- text = @parser.extract(start, stop)
125
+ text = @parser.document[start...stop]
126
126
  return if @config.partial_attribute_name_pattern === text
127
127
 
128
128
  s = "Invalid attribute name #{text.inspect} does not match "\
@@ -133,7 +133,7 @@ class BetterHtml::BetterErb
133
133
 
134
134
  def check_quoted_value(type, start, stop, line, column)
135
135
  return if @config.allow_single_quoted_attributes
136
- text = @parser.extract(start, stop)
136
+ text = @parser.document[start...stop]
137
137
  return if text == '"'
138
138
 
139
139
  s = "Single-quoted attributes are not allowed\n"
@@ -12,5 +12,17 @@ module BetterHtml
12
12
  property :javascript_attribute_names, default: [/\Aon/i]
13
13
  property :template_exclusion_filter
14
14
  property :lodash_safe_javascript_expression, default: [/\AJSON\.stringify\(/]
15
+
16
+ def javascript_attribute_name?(name)
17
+ javascript_attribute_names.any?{ |other| other === name.to_s }
18
+ end
19
+
20
+ def lodash_safe_javascript_expression?(code)
21
+ lodash_safe_javascript_expression.any?{ |other| other === code }
22
+ end
23
+
24
+ def javascript_safe_method?(name)
25
+ javascript_safe_methods.include?(name.to_s)
26
+ end
15
27
  end
16
28
  end
@@ -0,0 +1,286 @@
1
+ require_relative 'tokenizer/javascript_erb'
2
+ require_relative 'tokenizer/html_erb'
3
+ require_relative 'tokenizer/html_lodash'
4
+ require_relative 'tokenizer/location'
5
+ require_relative 'tokenizer/token_array'
6
+ require_relative 'ast/node'
7
+
8
+ module BetterHtml
9
+ class Parser
10
+ attr_reader :template_language
11
+
12
+ def initialize(document, template_language: :html)
13
+ @document = document
14
+ @template_language = template_language
15
+ @erb = case template_language
16
+ when :html
17
+ Tokenizer::HtmlErb.new(@document)
18
+ when :lodash
19
+ Tokenizer::HtmlLodash.new(@document)
20
+ when :javascript
21
+ Tokenizer::JavascriptErb.new(@document)
22
+ else
23
+ raise ArgumentError, "template_language can be :html or :javascript"
24
+ end
25
+ end
26
+
27
+ def nodes_with_type(*type)
28
+ types = Array.wrap(type)
29
+ ast.children.select{ |node| node.is_a?(::AST::Node) && types.include?(node.type) }
30
+ end
31
+
32
+ def ast
33
+ @ast ||= build_document_node
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class.name} ast=#{ast.inspect}>"
38
+ end
39
+
40
+ private
41
+
42
+ INTERPOLATION_TYPES = [:erb_begin, :lodash_begin]
43
+
44
+ def build_document_node
45
+ children = []
46
+ tokens = Tokenizer::TokenArray.new(@erb.tokens)
47
+ while tokens.any?
48
+ case tokens.current.type
49
+ when :cdata_start
50
+ children << build_cdata_node(tokens)
51
+ when :comment_start
52
+ children << build_comment_node(tokens)
53
+ when :tag_start
54
+ children << build_tag_node(tokens)
55
+ when :text, *INTERPOLATION_TYPES
56
+ children << build_text_node(tokens)
57
+ else
58
+ raise RuntimeError, "Unhandled token #{tokens.current.type} line #{tokens.current.loc.line} column #{tokens.current.loc.column}, #{children.inspect}"
59
+ end
60
+ end
61
+
62
+ build_node(:document, children.empty? ? nil : children)
63
+ end
64
+
65
+ def build_erb_node(tokens)
66
+ erb_begin = shift_single(tokens, :erb_begin)
67
+ children = [
68
+ shift_single(tokens, :indicator),
69
+ shift_single(tokens, :trim),
70
+ shift_single(tokens, :code),
71
+ shift_single(tokens, :trim),
72
+ ]
73
+ erb_end = shift_single(tokens, :erb_end)
74
+
75
+ build_node(:erb, children, pre: erb_begin, post: erb_end)
76
+ end
77
+
78
+ def build_lodash_node(tokens)
79
+ lodash_begin = shift_single(tokens, :lodash_begin)
80
+ children = [
81
+ shift_single(tokens, :indicator),
82
+ shift_single(tokens, :code),
83
+ ]
84
+ lodash_end = shift_single(tokens, :lodash_end)
85
+
86
+ build_node(:lodash, children, pre: lodash_begin, post: lodash_end)
87
+ end
88
+
89
+ def build_cdata_node(tokens)
90
+ cdata_start, children, cdata_end = shift_between_with_interpolation(tokens, :cdata_start, :cdata_end)
91
+ build_node(:cdata, children, pre: cdata_start, post: cdata_end)
92
+ end
93
+
94
+ def build_comment_node(tokens)
95
+ comment_start, children, comment_end = shift_between_with_interpolation(tokens, :comment_start, :comment_end)
96
+ build_node(:comment, children, pre: comment_start, post: comment_end)
97
+ end
98
+
99
+ def build_tag_node(tokens)
100
+ tag_start, tag_content, tag_end = shift_between(tokens, :tag_start, :tag_end)
101
+ tag_tokens = Tokenizer::TokenArray.new(tag_content)
102
+ tag_tokens.trim(:whitespace)
103
+
104
+ children = [
105
+ shift_single(tag_tokens, :solidus),
106
+ build_tag_name_node(tag_tokens),
107
+ build_tag_attributes_node(tag_tokens),
108
+ shift_single(tag_tokens, :solidus),
109
+ ]
110
+
111
+ build_node(:tag, children, pre: tag_start, post: tag_end)
112
+ end
113
+
114
+ def build_tag_name_node(tokens)
115
+ children = shift_all_with_interpolation(tokens, :tag_name)
116
+ build_node(:tag_name, children) if children.any?
117
+ end
118
+
119
+ def build_tag_attributes_node(tokens)
120
+ attributes_tokens = []
121
+ while tokens.any?
122
+ break if tokens.size == 1 && tokens.last.type == :solidus
123
+ if tokens.current.type == :attribute_name
124
+ attributes_tokens << build_attribute_node(tokens)
125
+ elsif tokens.current.type == :attribute_quoted_value_start
126
+ attributes_tokens << build_nameless_attribute_node(tokens)
127
+ else
128
+ # todo: warn about ignored things
129
+ tokens.shift
130
+ end
131
+ end
132
+
133
+ build_node(:tag_attributes, attributes_tokens) if attributes_tokens.any?
134
+ end
135
+
136
+ def build_nameless_attribute_node(tokens)
137
+ value_node = build_attribute_value_node(tokens)
138
+ build_node(:attribute, [nil, nil, value_node])
139
+ end
140
+
141
+ def build_attribute_node(tokens)
142
+ name_node = build_attribute_name_node(tokens)
143
+ shift_all(tokens, :whitespace)
144
+ equal_token = shift_single(tokens, :equal)
145
+ shift_all(tokens, :whitespace)
146
+ value_node = build_attribute_value_node(tokens) if equal_token.present?
147
+
148
+ build_node(:attribute, [name_node, equal_token, value_node])
149
+ end
150
+
151
+ def build_attribute_name_node(tokens)
152
+ children = shift_all_with_interpolation(tokens, :attribute_name)
153
+ build_node(:attribute_name, children)
154
+ end
155
+
156
+ def build_attribute_value_node(tokens)
157
+ children = shift_all_with_interpolation(tokens,
158
+ :attribute_quoted_value_start, :attribute_quoted_value,
159
+ :attribute_quoted_value_end, :attribute_unquoted_value
160
+ )
161
+
162
+ build_node(:attribute_value, children)
163
+ end
164
+
165
+ def build_text_node(tokens)
166
+ text_tokens = shift_all_with_interpolation(tokens, :text)
167
+ build_node(:text, text_tokens)
168
+ end
169
+
170
+ def build_node(type, tokens, pre: nil, post: nil)
171
+ BetterHtml::AST::Node.new(
172
+ type,
173
+ tokens.present? ? wrap_tokens(tokens) : [],
174
+ loc: tokens.present? ? build_location([pre, *tokens, post]) : empty_location
175
+ )
176
+ end
177
+
178
+ def build_location(enumerable)
179
+ enumerable = enumerable.compact
180
+ raise ArgumentError, "cannot build location for #{enumerable.inspect}" unless enumerable.first && enumerable.last
181
+ Tokenizer::Location.new(@document, enumerable.first.loc.start, enumerable.last.loc.stop)
182
+ end
183
+
184
+ def empty_location
185
+ Tokenizer::Location.new(@document, 0, 0)
186
+ end
187
+
188
+ def shift_all(tokens, *types)
189
+ [].tap do |items|
190
+ while tokens.any?
191
+ if types.include?(tokens.current.type)
192
+ items << tokens.shift
193
+ else
194
+ break
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ def shift_single(tokens, *types)
201
+ tokens.shift if tokens.any? && types.include?(tokens.current.type)
202
+ end
203
+
204
+ def shift_until(tokens, *types)
205
+ [].tap do |items|
206
+ while tokens.any?
207
+ if !types.include?(tokens.current.type)
208
+ items << tokens.shift
209
+ else
210
+ break
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ def build_interpolation_node(tokens)
217
+ if tokens.current.type == :erb_begin
218
+ build_erb_node(tokens)
219
+ elsif tokens.current.type == :lodash_begin
220
+ build_lodash_node(tokens)
221
+ else
222
+ tokens.shift
223
+ end
224
+ end
225
+
226
+ def shift_all_with_interpolation(tokens, *types)
227
+ types = [*INTERPOLATION_TYPES, *types]
228
+ [].tap do |result|
229
+ while tokens.any?
230
+ if types.include?(tokens.current.type)
231
+ result << build_interpolation_node(tokens)
232
+ else
233
+ break
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ def shift_until_with_interpolation(tokens, *types)
240
+ [].tap do |result|
241
+ while tokens.any?
242
+ if !types.include?(tokens.current.type)
243
+ result << build_interpolation_node(tokens)
244
+ else
245
+ break
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def shift_between(tokens, start_type, end_type)
252
+ start_token = shift_single(tokens, start_type)
253
+ children = shift_until(tokens, end_type)
254
+ end_token = shift_single(tokens, end_type)
255
+
256
+ [start_token, children, end_token]
257
+ end
258
+
259
+ def shift_between_with_interpolation(tokens, start_type, end_type)
260
+ start_token = shift_single(tokens, start_type)
261
+ children = shift_until_with_interpolation(tokens, end_type)
262
+ end_token = shift_single(tokens, end_type)
263
+
264
+ [start_token, children, end_token]
265
+ end
266
+
267
+ def wrap_token(object)
268
+ return unless object
269
+ if object.is_a?(::AST::Node)
270
+ object
271
+ elsif [:text, :tag_name, :attribute_name, :attribute_quoted_value, :attribute_unquoted_value].include?(object.type)
272
+ object.loc.source
273
+ elsif [:attribute_quoted_value_start, :attribute_quoted_value_end].include?(object.type)
274
+ BetterHtml::AST::Node.new(:quote, [object.loc.source], loc: object.loc)
275
+ elsif [:indicator, :code].include?(object.type)
276
+ BetterHtml::AST::Node.new(object.type, [object.loc.source], loc: object.loc)
277
+ else
278
+ BetterHtml::AST::Node.new(object.type, [], loc: object.loc)
279
+ end
280
+ end
281
+
282
+ def wrap_tokens(enumerable)
283
+ enumerable.map { |object| wrap_token(object) }
284
+ end
285
+ end
286
+ end
@@ -22,14 +22,17 @@ module BetterHtml
22
22
  end
23
23
 
24
24
  def initialize(ast)
25
- raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(Parser::AST::Node)
25
+ raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(::Parser::AST::Node)
26
26
  @ast = ast
27
27
  end
28
28
 
29
29
  def self.parse(code)
30
- parser = Parser::CurrentRuby.new
31
- parser.diagnostics.consumer = lambda { |diag| }
32
- buf = Parser::Source::Buffer.new('(string)')
30
+ parser = ::Parser::CurrentRuby.new
31
+ parser.diagnostics.ignore_warnings = true
32
+ parser.diagnostics.all_errors_are_fatal = false
33
+ parser.diagnostics.consumer = nil
34
+
35
+ buf = ::Parser::Source::Buffer.new('(string)')
33
36
  buf.source = code.sub(BLOCK_EXPR, '')
34
37
  parsed = parser.parse(buf)
35
38
  raise ParseError, "error parsing code: #{code.inspect}" unless parsed
@@ -53,7 +56,7 @@ module BetterHtml
53
56
 
54
57
  def each_child_node(current=@ast, only: nil, range: (0..-1))
55
58
  current.children[range].each do |child|
56
- if child.is_a?(Parser::AST::Node) && node_match?(child, only)
59
+ if child.is_a?(::Parser::AST::Node) && node_match?(child, only)
57
60
  yield child
58
61
  end
59
62
  end
@@ -1,6 +1,7 @@
1
1
  require 'better_html/test_helper/ruby_expr'
2
2
  require 'better_html/test_helper/safety_error'
3
- require 'parser/current'
3
+ require 'better_html/parser'
4
+ require 'better_html/tree/tag'
4
5
 
5
6
  module BetterHtml
6
7
  module TestHelper
@@ -58,7 +59,7 @@ EOF
58
59
  @errors = Errors.new
59
60
  @options = options.present? ? options.dup : {}
60
61
  @options[:template_language] ||= :html
61
- @nodes = BetterHtml::NodeIterator.new(data, @options.slice(:template_language))
62
+ @parser = BetterHtml::Parser.new(data, @options.slice(:template_language))
62
63
  validate!
63
64
  end
64
65
 
@@ -67,86 +68,98 @@ EOF
67
68
  end
68
69
 
69
70
  def validate!
70
- @nodes.each_with_index do |node, index|
71
- case node
72
- when BetterHtml::NodeIterator::Element
73
- validate_element(node)
74
-
75
- if node.name == 'script'
76
- next_node = @nodes[index + 1]
77
- if next_node.is_a?(BetterHtml::NodeIterator::ContentNode) && !node.closing?
78
- if javascript_tag_type?(node, "text/javascript")
79
- validate_script_tag_content(next_node)
80
- end
81
- validate_no_statements(next_node) unless javascript_tag_type?(node, "text/html")
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)
82
83
  end
83
-
84
- validate_javascript_tag_type(node) unless node.closing?
85
- end
86
- when BetterHtml::NodeIterator::Text
87
- validate_text_content(node)
88
-
89
- if @nodes.template_language == :javascript
90
- validate_script_tag_content(node)
91
- validate_no_statements(node)
92
- else
93
- validate_no_javascript_tag(node)
84
+ validate_no_statements(next_node) unless tag.attributes['type']&.value == "text/html"
94
85
  end
95
- when BetterHtml::NodeIterator::CData, BetterHtml::NodeIterator::Comment
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
96
  validate_no_statements(node)
97
+ else
98
+ validate_no_javascript_tag(node)
97
99
  end
98
100
  end
99
- end
100
101
 
101
- def javascript_tag_type?(element, which)
102
- typeattr = element['type']
103
- value = typeattr&.unescaped_value || "text/javascript"
104
- value == which
102
+ @parser.nodes_with_type(:cdata, :comment).each do |node|
103
+ validate_no_statements(node)
104
+ end
105
105
  end
106
106
 
107
- def validate_javascript_tag_type(element)
108
- typeattr = element['type']
109
- return if typeattr.nil?
110
- if !VALID_JAVASCRIPT_TAG_TYPES.include?(typeattr.unescaped_value)
111
- add_error(
112
- "#{typeattr.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}",
113
- location: typeattr.value_parts.first.location
114
- )
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
115
114
  end
116
115
  end
117
116
 
118
- def validate_element(element)
119
- element.attributes.each do |attr_token|
120
- attr_token.value_parts.each do |value_token|
121
- case value_token.type
122
- when :expr_literal
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 == '='
123
136
  begin
124
- expr = RubyExpr.parse(value_token.code)
125
- validate_tag_expression(value_token, expr, attr_token.name)
137
+ expr = RubyExpr.parse(source)
138
+ validate_tag_expression(code_node, expr, attribute.name)
126
139
  rescue RubyExpr::ParseError
127
140
  nil
128
141
  end
129
- when :expr_escaped
142
+ elsif indicator == '=='
130
143
  add_error(
131
144
  "erb interpolation with '<%==' inside html attribute is never safe",
132
- location: value_token.location
145
+ location: erb_node.loc
133
146
  )
134
147
  end
135
148
  end
136
149
  end
137
150
  end
138
151
 
139
- def validate_text_content(text)
140
- text.content_parts.each do |text_token|
141
- case text_token.type
142
- when :stmt, :expr_literal, :expr_escaped
143
- next if text_token.type == :stmt && text_token.code.start_with?('#')
144
- begin
145
- expr = RubyExpr.parse(text_token.code)
146
- validate_ruby_helper(text_token, expr)
147
- rescue RubyExpr::ParseError
148
- nil
149
- end
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
150
163
  end
151
164
  end
152
165
  end
@@ -185,13 +198,13 @@ EOF
185
198
  def validate_tag_expression(parent_token, expr, attr_name)
186
199
  return if expr.static_value?
187
200
 
188
- if javascript_attribute_name?(attr_name) && expr.calls.empty?
201
+ if @config.javascript_attribute_name?(attr_name) && expr.calls.empty?
189
202
  add_error(
190
203
  "erb interpolation in javascript attribute must call '(...).to_json'",
191
- location: NodeIterator::Location.new(
204
+ location: Tokenizer::Location.new(
192
205
  @data,
193
- parent_token.code_location.start + expr.start,
194
- parent_token.code_location.start + expr.end
206
+ parent_token.loc.start + expr.start,
207
+ parent_token.loc.start + expr.end - 1
195
208
  )
196
209
  )
197
210
  return
@@ -201,57 +214,54 @@ EOF
201
214
  if call.method == :raw
202
215
  add_error(
203
216
  "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe",
204
- location: NodeIterator::Location.new(
217
+ location: Tokenizer::Location.new(
205
218
  @data,
206
- parent_token.code_location.start + expr.start,
207
- parent_token.code_location.start + expr.end
219
+ parent_token.loc.start + expr.start,
220
+ parent_token.loc.start + expr.end - 1
208
221
  )
209
222
  )
210
223
  elsif call.method == :html_safe
211
224
  add_error(
212
225
  "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe",
213
- location: NodeIterator::Location.new(
226
+ location: Tokenizer::Location.new(
214
227
  @data,
215
- parent_token.code_location.start + expr.start,
216
- parent_token.code_location.start + expr.end
228
+ parent_token.loc.start + expr.start,
229
+ parent_token.loc.start + expr.end - 1
217
230
  )
218
231
  )
219
- elsif javascript_attribute_name?(attr_name) && !javascript_safe_method?(call.method)
232
+ elsif @config.javascript_attribute_name?(attr_name) && !@config.javascript_safe_method?(call.method)
220
233
  add_error(
221
234
  "erb interpolation in javascript attribute must call '(...).to_json'",
222
- location: NodeIterator::Location.new(
235
+ location: Tokenizer::Location.new(
223
236
  @data,
224
- parent_token.code_location.start + expr.start,
225
- parent_token.code_location.start + expr.end
237
+ parent_token.loc.start + expr.start,
238
+ parent_token.loc.start + expr.end - 1
226
239
  )
227
240
  )
228
241
  end
229
242
  end
230
243
  end
231
244
 
232
- def javascript_attribute_name?(name)
233
- @config.javascript_attribute_names.any?{ |other| other === name.to_s }
234
- end
235
-
236
- def javascript_safe_method?(name)
237
- @config.javascript_safe_methods.include?(name.to_s)
238
- end
239
-
240
245
  def validate_script_tag_content(node)
241
- node.content_parts.each do |token|
242
- case token.type
243
- when :expr_literal, :expr_escaped
244
- expr = RubyExpr.parse(token.code)
245
- validate_script_expression(node, token, expr)
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
246
256
  end
247
257
  end
248
258
  end
249
259
 
250
- def validate_script_expression(node, token, expr)
260
+ def validate_script_expression(parent_node, expr)
251
261
  if expr.calls.empty?
252
262
  add_error(
253
263
  "erb interpolation in javascript tag must call '(...).to_json'",
254
- location: token.location,
264
+ location: parent_node.loc,
255
265
  )
256
266
  return
257
267
  end
@@ -260,46 +270,49 @@ EOF
260
270
  if call.method == :raw
261
271
  call.arguments.each do |argument_node|
262
272
  arguments_expr = RubyExpr.new(argument_node)
263
- validate_script_expression(node, token, arguments_expr)
273
+ validate_script_expression(parent_node, arguments_expr)
264
274
  end
265
275
  elsif call.method == :html_safe
266
276
  instance_expr = RubyExpr.new(call.instance)
267
- validate_script_expression(node, token, instance_expr)
268
- elsif !javascript_safe_method?(call.method)
277
+ validate_script_expression(parent_node, instance_expr)
278
+ elsif !@config.javascript_safe_method?(call.method)
269
279
  add_error(
270
280
  "erb interpolation in javascript tag must call '(...).to_json'",
271
- location: token.location,
281
+ location: parent_node.loc,
272
282
  )
273
283
  end
274
284
  end
275
285
  end
276
286
 
277
287
  def validate_no_statements(node)
278
- node.content_parts.each do |token|
279
- if token.type == :stmt && !(/\A\s*end/m === token.code) && !token.code.start_with?('#')
280
- add_error(
281
- "erb statement not allowed here; did you mean '<%=' ?",
282
- location: token.location,
283
- )
284
- end
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
+ )
285
297
  end
286
298
  end
287
299
 
288
300
  def validate_no_javascript_tag(node)
289
- node.content_parts.each do |token|
290
- next if token.type == :stmt && token.code.start_with?('#')
291
- if [:stmt, :expr_literal, :expr_escaped].include?(token.type)
292
- expr = begin
293
- RubyExpr.parse(token.code)
294
- rescue RubyExpr::ParseError
295
- next
296
- end
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
+
297
309
  if expr.calls.size == 1 && expr.calls.first.method == :javascript_tag
298
310
  add_error(
299
311
  "'javascript_tag do' syntax is deprecated; use inline <script> instead",
300
- location: token.location,
312
+ location: erb_node.loc,
301
313
  )
302
314
  end
315
+ rescue RubyExpr::ParseError
303
316
  end
304
317
  end
305
318
  end