better_html 0.0.8 → 0.0.9

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