better_html 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a3a50dbfab3ea7cc2e0f83c166345072bb1fa1f
4
- data.tar.gz: 1fb0f5196c7069c9ad05af5b13b9993c744d2fbf
3
+ metadata.gz: 17ea4709e995bab97c314fd892b1926582788ab4
4
+ data.tar.gz: 497c12a9e1e060b4564d2310695fe06d54e3af86
5
5
  SHA512:
6
- metadata.gz: 7de6bd1ecece7d662029e21911a89e54cb0534f07648638ee0e97ef45c1ededddc09256662a49c7b47bc71a21afaca7ccb35b9eebf77ae9fa87fadb99e9d3a61
7
- data.tar.gz: d6214164d502b2e00f0b95f3659756f30b49d12e845d825d7946aa1f38c447a3246c8ea09152963c00d13822bf290daa343db45fba806fa7a42448334233896f
6
+ metadata.gz: 6cb55f55293eb9875635480ada4148fd3afb4a148284e24998c5cfa0a0648598f5659a8dccc41ea2329aa630f194fa8fe7d7f08bebea732310e8aebc7f80f5ff
7
+ data.tar.gz: 8786bbc8fe5a6df5da740715413f6ef05e9b05302f4b9cf1fe5672f4eb98ef931519155e3f219b6b3dc53c5fd1142d626e7b8c5c6f008ca655f7e9eab8daba2c
@@ -14,6 +14,7 @@ module BetterHtml
14
14
  def initialize(document)
15
15
  @parser = HtmlTokenizer::Parser.new
16
16
  @tokens = []
17
+ @document = document
17
18
  super(document, regexp: REGEXP_WITHOUT_TRIM, trim: false)
18
19
  end
19
20
 
@@ -29,7 +30,8 @@ module BetterHtml
29
30
  type: :stmt,
30
31
  code: code,
31
32
  text: text,
32
- location: Location.new(start, stop, @parser.line_number, @parser.column_number)
33
+ location: Location.new(@document, start, stop, @parser.line_number, @parser.column_number),
34
+ code_location: Location.new(@document, start+2, stop-2, @parser.line_number, @parser.column_number+2)
33
35
  )
34
36
  @parser.append_placeholder(text)
35
37
  end
@@ -42,7 +44,8 @@ module BetterHtml
42
44
  type: indicator == '=' ? :expr_literal : :expr_escaped,
43
45
  code: code,
44
46
  text: text,
45
- location: Location.new(start, stop, @parser.line_number, @parser.column_number)
47
+ location: Location.new(@document, start, stop, @parser.line_number, @parser.column_number),
48
+ code_location: Location.new(@document, start+2+indicator.size, stop-2, @parser.line_number, @parser.column_number+2+indicator.size)
46
49
  )
47
50
  @parser.append_placeholder(text)
48
51
  end
@@ -58,7 +61,7 @@ module BetterHtml
58
61
  @tokens << Token.new(
59
62
  type: type,
60
63
  text: @parser.extract(start, stop),
61
- location: Location.new(start, stop, line, column),
64
+ location: Location.new(@document, start, stop, line, column),
62
65
  **(extra_attributes || {})
63
66
  )
64
67
  end
@@ -92,7 +92,7 @@ module BetterHtml
92
92
  type: type,
93
93
  text: text,
94
94
  code: code,
95
- location: Location.new(start, stop, line || @parser.line_number, column || @parser.column_number),
95
+ location: Location.new(@source, start, stop, line || @parser.line_number, column || @parser.column_number),
96
96
  **(extra_attributes || {})
97
97
  )
98
98
  end
@@ -7,10 +7,11 @@ module BetterHtml
7
7
  class JavascriptErb < ::Erubi::Engine
8
8
  attr_reader :tokens
9
9
 
10
- def initialize(document)
11
- @document = ""
10
+ def initialize(source)
11
+ @source = source
12
+ @parsed_document = ""
12
13
  @tokens = []
13
- super(document, regexp: HtmlErb::REGEXP_WITHOUT_TRIM, trim: false)
14
+ super(source, regexp: HtmlErb::REGEXP_WITHOUT_TRIM, trim: false)
14
15
  end
15
16
 
16
17
  def add_text(text)
@@ -33,21 +34,21 @@ module BetterHtml
33
34
  private
34
35
 
35
36
  def add_token(type, text, code = nil)
36
- start = @document.size
37
+ start = @parsed_document.size
37
38
  stop = start + text.size
38
- lines = @document.split("\n", -1)
39
+ lines = @parsed_document.split("\n", -1)
39
40
  line = lines.empty? ? 1 : lines.size
40
41
  column = lines.empty? ? 0 : lines.last.size
41
42
  @tokens << Token.new(
42
43
  type: type,
43
44
  text: text,
44
45
  code: code,
45
- location: Location.new(start, stop, line, column)
46
+ location: Location.new(@source, start, stop, line, column)
46
47
  )
47
48
  end
48
49
 
49
50
  def append(text)
50
- @document << text
51
+ @parsed_document << text
51
52
  end
52
53
  end
53
54
  end
@@ -1,14 +1,50 @@
1
1
  module BetterHtml
2
2
  class NodeIterator
3
3
  class Location
4
- attr_accessor :start, :stop, :line, :column
4
+ attr_accessor :start, :stop
5
5
 
6
- def initialize(start, stop, line, column)
6
+ def initialize(document, start, stop, line = nil, column = nil)
7
+ @document = document
7
8
  @start = start
8
9
  @stop = stop
9
10
  @line = line
10
11
  @column = column
11
12
  end
13
+
14
+ def range
15
+ Range.new(start, stop-1)
16
+ end
17
+
18
+ def source
19
+ @document[range]
20
+ end
21
+
22
+ def line
23
+ @line ||= calculate_line
24
+ end
25
+
26
+ def column
27
+ @column ||= calculate_column
28
+ end
29
+
30
+ def line_source_with_underline
31
+ line_content = @document.lines[line-1]
32
+ line_content = line_content.nil? ? "" : line_content.gsub(/\n$/, '')
33
+ spaces = line_content.scan(/\A\s*/).first
34
+ column_without_spaces = column - spaces.length
35
+ underscore_length = [[stop - start, line_content.length - column_without_spaces].min, 1].max
36
+ "#{line_content.gsub(/\A\s*/, '')}\n#{' ' * column_without_spaces}#{'^' * underscore_length}"
37
+ end
38
+
39
+ private
40
+
41
+ def calculate_line
42
+ @document[0..start-1].scan("\n").count + 1
43
+ end
44
+
45
+ def calculate_column
46
+ @document[0..start-1]&.split("\n", -1)&.last&.length || 0
47
+ end
12
48
  end
13
49
  end
14
50
  end
@@ -1,88 +1,113 @@
1
- require 'ripper'
2
- require 'pp'
1
+ require 'parser/current'
3
2
 
4
3
  module BetterHtml
5
4
  module TestHelper
6
5
  class RubyExpr
7
- attr_reader :calls
8
-
9
6
  BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
10
7
 
11
8
  class ParseError < RuntimeError; end
12
9
 
13
10
  class MethodCall
14
11
  attr_accessor :instance, :method, :arguments
15
- end
16
12
 
17
- def initialize(code: nil, tree: nil)
18
- if code
19
- code = code.gsub(BLOCK_EXPR, '')
20
- tree = Ripper.sexp(code)
21
- raise ParseError, "cannot parse code" unless tree && tree.last.first.is_a?(Array)
22
- @tree = tree.last.first
23
- else
24
- @tree = tree
13
+ def initialize(instance, method, arguments)
14
+ @instance = instance
15
+ @method = method
16
+ @arguments = arguments
25
17
  end
26
- @calls = []
27
- parse!
18
+
19
+ def self.from_ast_node(node)
20
+ new(node.children[0], node.children[1], node.children[2..-1])
21
+ end
22
+ end
23
+
24
+ def initialize(ast)
25
+ raise ArgumentError, "expect first argument to be Parser::AST::Node" unless ast.is_a?(Parser::AST::Node)
26
+ @ast = ast
28
27
  end
29
28
 
30
- private
29
+ def self.parse(code)
30
+ parser = Parser::CurrentRuby.new
31
+ parser.diagnostics.consumer = lambda { |diag| }
32
+ buf = Parser::Source::Buffer.new('(string)')
33
+ buf.source = code.sub(BLOCK_EXPR, '')
34
+ parsed = parser.parse(buf)
35
+ raise ParseError, "error parsing code: #{code.inspect}" unless parsed
36
+ new(parsed)
37
+ end
38
+
39
+ def start
40
+ @ast.loc.expression.begin_pos
41
+ end
31
42
 
32
- def parse!
33
- parse_expr(@tree)
43
+ def end
44
+ @ast.loc.expression.end_pos
34
45
  end
35
46
 
36
- def parse_expr(expr)
37
- case expr.first
38
- when :var_ref
39
- parse_expr(expr[1])
40
- when :paren
41
- parse_expr(expr[1].first)
42
- when :string_literal
43
- parse_expr(expr[1])
44
- when :string_content
45
- expr[1..-1].each do |subexpr|
46
- parse_expr(subexpr)
47
+ def traverse(current=@ast, only: nil, &block)
48
+ yield current if node_match?(current, only)
49
+ each_child_node(current) do |child|
50
+ traverse(child, only: only, &block)
51
+ end
52
+ end
53
+
54
+ def each_child_node(current=@ast, only: nil, range: (0..-1))
55
+ current.children[range].each do |child|
56
+ if child.is_a?(Parser::AST::Node) && node_match?(child, only)
57
+ yield child
47
58
  end
48
- when :string_embexpr
49
- expr[1].each do |subexpr|
50
- parse_expr(subexpr)
59
+ end
60
+ end
61
+
62
+ def node_match?(current, type)
63
+ type.nil? || Array[type].flatten.include?(current.type)
64
+ end
65
+
66
+ STATIC_TYPES = [:str, :int, :true, :false, :nil]
67
+
68
+ def each_return_value_recursive(current=@ast, only: nil, &block)
69
+ case current.type
70
+ when :send, :csend, :ivar, *STATIC_TYPES
71
+ yield current if node_match?(current, only)
72
+ when :if, :masgn, :lvasgn
73
+ # first child is ignored as it does not contain return values
74
+ # for example, in `foo ? x : y` we only care about x and y, not foo
75
+ each_child_node(current, range: 1..-1) do |child|
76
+ each_return_value_recursive(child, only: only, &block)
51
77
  end
52
- when :assign, :massign
53
- parse_expr(expr[2])
54
- when :call
55
- @calls << obj = MethodCall.new
56
- obj.instance = expr[1]
57
- obj.method = parse_expr(expr[3])
58
- obj
59
- when :fcall, :vcall
60
- @calls << obj = MethodCall.new
61
- obj.method = parse_expr(expr[1])
62
- obj
63
- when :method_add_arg
64
- # foo(bar) -> foo=expr[1], bar=expr[2]
65
- obj = parse_expr(expr[1])
66
- obj.arguments = parse_expr(expr[2])
67
- obj
68
- when :command
69
- # foo bar -> foo=expr[1], bar=expr[2]
70
- @calls << obj = MethodCall.new
71
- obj.method = parse_expr(expr[1])
72
- obj.arguments = parse_expr(expr[2])
73
- obj
74
- when :arg_paren
75
- parse_expr(expr[1]) unless expr[1].nil?
76
- when :if_mod, :unless_mod
77
- # foo if bar -> bar=expr[1], foo=expr[2]
78
- parse_expr(expr[2])
79
- when :ifop
80
- # foo ? bar : baz -> foo=expr[1], bar=expr[2], baz=expr[3]
81
- parse_expr(expr[2])
82
- parse_expr(expr[3])
83
78
  else
84
- expr[1]
79
+ each_child_node(current) do |child|
80
+ each_return_value_recursive(child, only: only, &block)
81
+ end
82
+ end
83
+ end
84
+
85
+ def static_value?
86
+ returns = []
87
+ each_return_value_recursive do |node|
88
+ returns << node
89
+ end
90
+ return false if returns.size == 0
91
+ returns.each do |node|
92
+ if STATIC_TYPES.include?(node.type)
93
+ next
94
+ elsif node.type == :dstr
95
+ each_child_node(node) do |child|
96
+ return false if child.type != :str
97
+ end
98
+ else
99
+ return false
100
+ end
101
+ end
102
+ true
103
+ end
104
+
105
+ def calls
106
+ calls = []
107
+ each_return_value_recursive(only: [:send, :csend]) do |node|
108
+ calls << MethodCall.from_ast_node(node)
85
109
  end
110
+ calls
86
111
  end
87
112
  end
88
113
  end
@@ -1,11 +1,10 @@
1
1
  require 'better_html/test_helper/ruby_expr'
2
- require_relative 'safety_tester_base'
2
+ require 'better_html/test_helper/safety_error'
3
+ require 'parser/current'
3
4
 
4
5
  module BetterHtml
5
6
  module TestHelper
6
7
  module SafeErbTester
7
- include SafetyTesterBase
8
-
9
8
  SAFETY_TIPS = <<-EOF
10
9
  -----------
11
10
 
@@ -34,7 +33,11 @@ EOF
34
33
 
35
34
  message = ""
36
35
  tester.errors.each do |error|
37
- message << format_safety_error(data, error)
36
+ message << <<~EOL
37
+ On line #{error.location.line}
38
+ #{error.message}
39
+ #{error.location.line_source_with_underline}\n
40
+ EOL
38
41
  end
39
42
 
40
43
  message << SAFETY_TIPS
@@ -58,8 +61,8 @@ EOF
58
61
  validate!
59
62
  end
60
63
 
61
- def add_error(token, message)
62
- @errors.add(SafetyTesterBase::SafetyError.new(token, message))
64
+ def add_error(message, location:)
65
+ @errors.add(SafetyError.new(message, location: location))
63
66
  end
64
67
 
65
68
  def validate!
@@ -80,6 +83,8 @@ EOF
80
83
  validate_javascript_tag_type(node) unless node.closing?
81
84
  end
82
85
  when BetterHtml::NodeIterator::Text
86
+ validate_text_content(node)
87
+
83
88
  if @nodes.template_language == :javascript
84
89
  validate_script_tag_content(node)
85
90
  validate_no_statements(node)
@@ -102,7 +107,10 @@ EOF
102
107
  typeattr = element['type']
103
108
  return if typeattr.nil?
104
109
  if !VALID_JAVASCRIPT_TAG_TYPES.include?(typeattr.unescaped_value)
105
- add_error(typeattr.value_parts.first, "#{typeattr.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}")
110
+ add_error(
111
+ "#{typeattr.value} is not a valid type, valid types are #{VALID_JAVASCRIPT_TAG_TYPES.join(', ')}",
112
+ location: typeattr.value_parts.first.location
113
+ )
106
114
  end
107
115
  end
108
116
 
@@ -111,48 +119,132 @@ EOF
111
119
  attr_token.value_parts.each do |value_token|
112
120
  case value_token.type
113
121
  when :expr_literal
114
- validate_tag_expression(element, attr_token.name, value_token)
122
+ begin
123
+ expr = RubyExpr.parse(value_token.code)
124
+ validate_tag_expression(value_token, expr, attr_token.name)
125
+ rescue RubyExpr::ParseError
126
+ nil
127
+ end
115
128
  when :expr_escaped
116
- add_error(value_token, "erb interpolation with '<%==' inside html attribute is never safe")
129
+ add_error(
130
+ "erb interpolation with '<%==' inside html attribute is never safe",
131
+ location: value_token.location
132
+ )
117
133
  end
118
134
  end
119
135
  end
120
136
  end
121
137
 
122
- def validate_tag_expression(node, attr_name, value_token)
123
- expr = RubyExpr.new(code: value_token.code)
138
+ def validate_text_content(text)
139
+ text.content_parts.each do |text_token|
140
+ case text_token.type
141
+ when :stmt, :expr_literal, :expr_escaped
142
+ begin
143
+ expr = RubyExpr.parse(text_token.code)
144
+ validate_ruby_helper(text_token, expr)
145
+ rescue RubyExpr::ParseError
146
+ nil
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def validate_ruby_helper(parent_token, expr)
153
+ expr.traverse(only: [:send, :csend]) do |send_node|
154
+ expr.each_child_node(send_node, only: :hash) do |hash_node|
155
+ expr.each_child_node(hash_node, only: :pair) do |pair_node|
156
+ validate_ruby_helper_hash_entry(parent_token, expr, nil, *pair_node.children)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def validate_ruby_helper_hash_entry(parent_token, expr, key_prefix, key_node, value_node)
163
+ return unless [:sym, :str].include?(key_node.type)
164
+ key = [key_prefix, key_node.children.first.to_s].compact.join('-').dasherize
165
+ case value_node.type
166
+ when :dstr
167
+ validate_ruby_helper_hash_value(parent_token, expr, key, value_node)
168
+ when :hash
169
+ if key == 'data'
170
+ expr.each_child_node(value_node, only: :pair) do |pair_node|
171
+ validate_ruby_helper_hash_entry(parent_token, expr, key, *pair_node.children)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def validate_ruby_helper_hash_value(parent_token, expr, attr_name, hash_value)
178
+ expr.each_child_node(hash_value, only: :begin) do |child|
179
+ validate_tag_expression(parent_token, RubyExpr.new(child), attr_name)
180
+ end
181
+ end
182
+
183
+ def validate_tag_expression(parent_token, expr, attr_name)
184
+ return if expr.static_value?
124
185
 
125
186
  if javascript_attribute_name?(attr_name) && expr.calls.empty?
126
- add_error(value_token, "erb interpolation in javascript attribute must call '(...).to_json'")
187
+ add_error(
188
+ "erb interpolation in javascript attribute must call '(...).to_json'",
189
+ location: NodeIterator::Location.new(
190
+ @data,
191
+ parent_token.code_location.start + expr.start,
192
+ parent_token.code_location.start + expr.end
193
+ )
194
+ )
127
195
  return
128
196
  end
129
197
 
130
198
  expr.calls.each do |call|
131
- if call.method == 'raw'
132
- add_error(value_token, "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe")
133
- elsif call.method == 'html_safe'
134
- add_error(value_token, "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe")
199
+ if call.method == :raw
200
+ add_error(
201
+ "erb interpolation with '<%= raw(...) %>' inside html attribute is never safe",
202
+ location: NodeIterator::Location.new(
203
+ @data,
204
+ parent_token.code_location.start + expr.start,
205
+ parent_token.code_location.start + expr.end
206
+ )
207
+ )
208
+ elsif call.method == :html_safe
209
+ add_error(
210
+ "erb interpolation with '<%= (...).html_safe %>' inside html attribute is never safe",
211
+ location: NodeIterator::Location.new(
212
+ @data,
213
+ parent_token.code_location.start + expr.start,
214
+ parent_token.code_location.start + expr.end
215
+ )
216
+ )
135
217
  elsif javascript_attribute_name?(attr_name) && !javascript_safe_method?(call.method)
136
- add_error(value_token, "erb interpolation in javascript attribute must call '(...).to_json'")
218
+ add_error(
219
+ "erb interpolation in javascript attribute must call '(...).to_json'",
220
+ location: NodeIterator::Location.new(
221
+ @data,
222
+ parent_token.code_location.start + expr.start,
223
+ parent_token.code_location.start + expr.end
224
+ )
225
+ )
137
226
  end
138
227
  end
139
228
  end
140
229
 
141
230
  def javascript_attribute_name?(name)
142
- BetterHtml.config.javascript_attribute_names.any?{ |other| other === name }
231
+ BetterHtml.config.javascript_attribute_names.any?{ |other| other === name.to_s }
143
232
  end
144
233
 
145
234
  def javascript_safe_method?(name)
146
- BetterHtml.config.javascript_safe_methods.include?(name)
235
+ BetterHtml.config.javascript_safe_methods.include?(name.to_s)
147
236
  end
148
237
 
149
238
  def validate_script_tag_content(node)
150
239
  node.content_parts.each do |token|
151
240
  case token.type
152
241
  when :expr_literal, :expr_escaped
153
- expr = RubyExpr.new(code: token.code)
242
+ expr = RubyExpr.parse(token.code)
154
243
  if expr.calls.empty?
155
- add_error(token, "erb interpolation in javascript tag must call '(...).to_json'")
244
+ add_error(
245
+ "erb interpolation in javascript tag must call '(...).to_json'",
246
+ location: token.location,
247
+ )
156
248
  else
157
249
  validate_script_expression(node, token, expr)
158
250
  end
@@ -162,14 +254,19 @@ EOF
162
254
 
163
255
  def validate_script_expression(node, token, expr)
164
256
  expr.calls.each do |call|
165
- if call.method == 'raw'
166
- arguments_expr = RubyExpr.new(tree: call.arguments)
167
- validate_script_expression(node, token, arguments_expr)
168
- elsif call.method == 'html_safe'
169
- instance_expr = RubyExpr.new(tree: call.instance)
257
+ if call.method == :raw
258
+ call.arguments.each do |argument_node|
259
+ arguments_expr = RubyExpr.new(argument_node)
260
+ validate_script_expression(node, token, arguments_expr)
261
+ end
262
+ elsif call.method == :html_safe
263
+ instance_expr = RubyExpr.new(call.instance)
170
264
  validate_script_expression(node, token, instance_expr)
171
265
  elsif !javascript_safe_method?(call.method)
172
- add_error(token, "erb interpolation in javascript tag must call '(...).to_json'")
266
+ add_error(
267
+ "erb interpolation in javascript tag must call '(...).to_json'",
268
+ location: token.location,
269
+ )
173
270
  end
174
271
  end
175
272
  end
@@ -177,7 +274,10 @@ EOF
177
274
  def validate_no_statements(node)
178
275
  node.content_parts.each do |token|
179
276
  if token.type == :stmt && !(/\A\s*end/m === token.code)
180
- add_error(token, "erb statement not allowed here; did you mean '<%=' ?")
277
+ add_error(
278
+ "erb statement not allowed here; did you mean '<%=' ?",
279
+ location: token.location,
280
+ )
181
281
  end
182
282
  end
183
283
  end
@@ -186,12 +286,15 @@ EOF
186
286
  node.content_parts.each do |token|
187
287
  if [:stmt, :expr_literal, :expr_escaped].include?(token.type)
188
288
  expr = begin
189
- RubyExpr.new(code: token.code)
289
+ RubyExpr.parse(token.code)
190
290
  rescue RubyExpr::ParseError
191
291
  next
192
292
  end
193
- if expr.calls.size == 1 && expr.calls.first.method == 'javascript_tag'
194
- add_error(token, "'javascript_tag do' syntax is deprecated; use inline <script> instead")
293
+ if expr.calls.size == 1 && expr.calls.first.method == :javascript_tag
294
+ add_error(
295
+ "'javascript_tag do' syntax is deprecated; use inline <script> instead",
296
+ location: token.location,
297
+ )
195
298
  end
196
299
  end
197
300
  end