klenod-build 0.0.26 → 0.0.28

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.
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
3
4
  require "ripper"
4
- require "syntax_tree"
5
- require "syntax_tree/dsl"
6
5
  require "syntax_suggest/api"
7
6
  require "syntax_suggest/explain_syntax"
8
7
 
@@ -12,17 +11,18 @@ module Klenod
12
11
  module HamlPlugin
13
12
  class Transformer
14
13
  class RubyBuilder
15
- include SyntaxTree::DSL
14
+ # `node` is the Prism node parsed from user Ruby, for the places
15
+ # that inspect it. `statements` marks a statement list, which is
16
+ # wrapped in `begin`/`end` when it is used as an argument.
17
+ Fragment = Data.define(:source, :node, :statements) do
18
+ def initialize(source:, node: nil, statements: false)
19
+ super
20
+ end
16
21
 
17
- Fragment = Data.define(:source, :node) do
18
22
  def node?
19
23
  !node.nil?
20
24
  end
21
25
 
22
- def statement_body
23
- node.is_a?(SyntaxTree::Statements) ? node.body : [node].compact
24
- end
25
-
26
26
  def to_s
27
27
  source
28
28
  end
@@ -33,7 +33,6 @@ module Klenod
33
33
  @variables = variables || {}
34
34
  @expression_cache = {}
35
35
  @statements_cache = {}
36
- @program_cache = {}
37
36
  @literal_cache = {}
38
37
  end
39
38
 
@@ -81,7 +80,7 @@ module Klenod
81
80
  haml_helper_source = statements_fragment(haml_helper_source) if haml_helper_source
82
81
 
83
82
  header = [
84
- Fragment.new("# frozen_string_literal: true", comment_node("# frozen_string_literal: true")),
83
+ Fragment.new("# frozen_string_literal: true"),
85
84
  constant_assignment(
86
85
  "KlenodImport",
87
86
  call(receiver: nil, name: "method", arguments: [symbol("__klenod_import__")])
@@ -118,7 +117,6 @@ module Klenod
118
117
  static_constants: [],
119
118
  i18n_source: nil
120
119
  )
121
- skeleton = class_skeleton_fragment(component_class_name, component_base_class)
122
120
  body_fragments =
123
121
  [
124
122
  method_definition("module_path", target: "self", body: file_expression),
@@ -141,24 +139,13 @@ module Klenod
141
139
  *static_constants,
142
140
  public_method_definition("render", body: render_source)
143
141
  ]
144
- body =
145
- body_fragments.flat_map { |fragment| statement_body_for(fragment) }
146
142
 
147
143
  Fragment.new(
148
144
  [
149
145
  "class #{to_source(component_class_name)} < #{to_source(component_base_class)}",
150
146
  indent(compact_join(body_fragments), 2),
151
147
  "end"
152
- ].join("\n"),
153
- skeleton.node.copy(
154
- bodystmt: BodyStmt(
155
- Statements(body),
156
- nil,
157
- nil,
158
- nil,
159
- nil
160
- )
161
- )
148
+ ].join("\n")
162
149
  )
163
150
  end
164
151
 
@@ -168,35 +155,24 @@ module Klenod
168
155
 
169
156
  def expression(source, line_no: nil)
170
157
  source = rewrite_ruby_source(source, line_no)
171
- return Fragment.new(source, constant_path(source)) if source.match?(VALID_CONST_PATH)
172
158
 
173
- node = parse_expression(source, context: :expression)
174
-
175
- Fragment.new(source, node)
159
+ Fragment.new(source, parse_expression(source, context: :expression))
176
160
  end
177
161
 
178
162
  def statements(source, line_no: nil)
179
163
  source = rewrite_ruby_source(source, line_no)
180
164
  node = parse_statements(source)
181
165
 
182
- Fragment.new(source, node)
183
- end
184
-
185
- def program(source)
186
- node = parse_program(source)
187
-
188
- Fragment.new(node ? format_node(node) : source, node)
166
+ Fragment.new(source, node, !node.nil?)
189
167
  end
190
168
 
191
169
  def program_from_fragments(*fragments)
192
- fragments = fragments.flatten
193
- body = fragments.flat_map { |fragment| statement_body_for(fragment) }
194
-
195
- Fragment.new(compact_join(fragments), Program(Statements(body)))
170
+ Fragment.new(compact_join(fragments.flatten))
196
171
  end
197
172
 
173
+ # Reprints parsed user Ruby exactly as it was written.
198
174
  def fragment(node)
199
- Fragment.new(format_node(node), node)
175
+ Fragment.new(node.slice, node)
200
176
  end
201
177
 
202
178
  def node_fragment(source, node)
@@ -224,189 +200,103 @@ module Klenod
224
200
  end
225
201
 
226
202
  def literal_fragment(value)
227
- Fragment.new(literal_source(value), literal_node(value))
203
+ Fragment.new(literal_source(value))
228
204
  end
229
205
 
230
206
  def frozen_literal(value)
231
- fragment(frozen_literal_node(value))
207
+ Fragment.new(frozen_literal_source(value))
232
208
  end
233
209
 
234
210
  def import_call(dependency_id)
235
- Fragment.new(
236
- "__klenod_import__(#{literal_source(dependency_id)})",
237
- CallNode(
238
- nil,
239
- nil,
240
- Ident("__klenod_import__"),
241
- ArgParen(Args([literal_node(dependency_id)]))
242
- )
243
- )
211
+ Fragment.new("__klenod_import__(#{literal_source(dependency_id)})")
244
212
  end
245
213
 
246
214
  def constant_assignment(name, value)
247
215
  value = expression_fragment(value)
248
- node = Assign(VarField(Const(name.to_s)), node_for(value))
249
- Fragment.new("#{name} = #{to_source(value)}", node)
216
+ Fragment.new("#{name} = #{to_source(value)}")
250
217
  end
251
218
 
252
219
  def call(receiver:, name:, arguments:)
253
220
  receiver = expression_fragment(receiver) unless receiver.nil?
254
221
  arguments = arguments.map { |argument| expression_fragment(argument) }
255
- receiver_node = receiver.nil? ? nil : node_for(receiver)
256
-
257
- node =
258
- CallNode(
259
- receiver_node,
260
- receiver_node ? Period(".") : nil,
261
- Ident(name.to_s),
262
- ArgParen(Args(arguments.map { |argument| node_for(argument) }))
263
- )
264
222
  receiver_prefix = receiver ? "#{to_source(receiver)}." : nil
265
- source = "#{receiver_prefix}#{name}(#{arguments.map { |argument| to_source(argument) }.join(", ")})"
266
- Fragment.new(source, node)
223
+ Fragment.new("#{receiver_prefix}#{name}(#{arguments.map { |argument| to_source(argument) }.join(", ")})")
267
224
  end
268
225
 
269
226
  def method_definition(name, body:, target: nil, parameters: [])
270
- body = Array(body)
271
- body_source = compact_join(body)
272
- node =
273
- DefNode(
274
- target && node_for(expression_fragment(target)),
275
- target ? Period(".") : nil,
276
- Ident(name.to_s),
277
- Params(parameters.map { |parameter| Ident(parameter.to_s) }, [], nil, [], [], nil, nil),
278
- body_statement(body)
279
- )
227
+ body_source = compact_join(Array(body))
280
228
  target_source = target ? "#{to_source(expression_fragment(target))}." : ""
281
229
  params_source = parameters.empty? ? "" : "(#{parameters.join(", ")})"
282
- source = ["def #{target_source}#{name}#{params_source}", indent(body_source, 2), "end"].join("\n")
283
- Fragment.new(source, node)
230
+ Fragment.new(["def #{target_source}#{name}#{params_source}", indent(body_source, 2), "end"].join("\n"))
284
231
  end
285
232
 
286
233
  def public_method_definition(name, body:, parameters: [])
287
234
  method = method_definition(name, parameters: parameters, body: body)
288
- node =
289
- Command(
290
- Ident("public"),
291
- Args([method.node]),
292
- nil
293
- )
294
- Fragment.new("public #{method.source}", node)
235
+ Fragment.new("public #{method.source}")
295
236
  end
296
237
 
297
238
  def nil_expression
298
- Fragment.new("nil", nil_node)
239
+ Fragment.new("nil")
299
240
  end
300
241
 
301
242
  def file_expression
302
- Fragment.new("__FILE__", VarRef(Kw("__FILE__")))
243
+ Fragment.new("__FILE__")
303
244
  end
304
245
 
305
246
  def symbol(value)
306
- value = value.to_s
307
- Fragment.new(symbol_source(value), symbol_node(value))
247
+ Fragment.new(symbol_source(value.to_s))
308
248
  end
309
249
 
310
250
  def symbol_fragment(value)
311
- Fragment.new(symbol_source(value.to_s), nil)
251
+ Fragment.new(symbol_source(value.to_s))
312
252
  end
313
253
 
314
254
  def styles_lookup(name)
315
- name = name.to_s
316
-
317
- Fragment.new(
318
- "ClassNames[#{symbol_source(name)}]",
319
- ARef(VarRef(Const("ClassNames")), Args([symbol_node(name)]))
320
- )
255
+ Fragment.new("ClassNames[#{symbol_source(name.to_s)}]")
321
256
  end
322
257
 
323
258
  def class_name_lookup(name)
324
- name = name.to_s
325
-
326
- Fragment.new(
327
- "ClassNames[#{symbol_source(name)}]",
328
- ARef(VarRef(Const("ClassNames")), Args([symbol_node(name)]))
329
- )
259
+ Fragment.new("ClassNames[#{symbol_source(name.to_s)}]")
330
260
  end
331
261
 
332
262
  def class_names(values)
333
263
  fragments = values.map { |value| expression_fragment(value) }
334
264
 
335
- Fragment.new(
336
- "ClassNames.class_name(#{fragments.map(&:source).join(", ")})",
337
- CallNode(
338
- constant_path("ClassNames"),
339
- Period("."),
340
- Ident("class_name"),
341
- ArgParen(Args(fragments.map { |fragment| node_for(fragment) }))
342
- )
343
- )
265
+ Fragment.new("ClassNames.class_name(#{fragments.map(&:source).join(", ")})")
344
266
  end
345
267
 
346
268
  def scoped_class_name(values)
347
269
  fragments = values.map { |value| expression_fragment(value) }
348
270
 
349
- Fragment.new(
350
- "ClassNames.class_name(#{fragments.map(&:source).join(", ")})",
351
- nil
352
- )
271
+ Fragment.new("ClassNames.class_name(#{fragments.map(&:source).join(", ")})")
353
272
  end
354
273
 
355
274
  def parenthesized_expression(source, line_no: nil)
356
275
  source = rewrite_ruby_source(source, line_no)
357
- node = parse_expression(source, context: :parenthesized_expression)
358
- unless node
276
+ result = parse_result(source)
277
+ unless result
359
278
  raise_ruby_parse_error(source, line_no: line_no, context: "Could not parse Haml output script", syntax_error: true)
360
279
  end
361
280
 
362
- fragment(Paren(LParen("("), Statements([node])))
281
+ # A trailing comment would otherwise swallow the closing parenthesis.
282
+ closing = result.comments.empty? ? ")" : "\n)"
283
+ Fragment.new("(#{source}#{closing}", result.value.statements.body.first)
363
284
  end
364
285
 
365
286
  def hash_expression(source, line_no: nil)
366
287
  source = rewrite_ruby_source(source, line_no)
367
288
  node = parse_expression(source, context: :hash_expression)
368
- return nil unless node.is_a?(SyntaxTree::HashLiteral)
289
+ return nil unless node.is_a?(Prism::HashNode)
369
290
 
370
291
  Fragment.new(source, node)
371
292
  end
372
293
 
373
- def class_skeleton_fragment(component_class_name, component_base_class)
374
- Fragment.new(
375
- "",
376
- ClassDeclaration(
377
- constant_path(component_class_name, declaration: true),
378
- constant_path(component_base_class),
379
- BodyStmt(
380
- Statements([]),
381
- nil,
382
- nil,
383
- nil,
384
- nil
385
- )
386
- )
387
- )
388
- end
389
-
390
- def constant_path(value, declaration: false)
391
- parts = to_source(value).split("::")
392
- raise ArgumentError, "Expected constant path: #{to_source(value).inspect}" if parts.empty? || parts.any?(&:empty?)
393
-
394
- return ConstRef(Const(parts.fetch(0))) if declaration && parts.length == 1
395
- return VarRef(Const(parts.fetch(0))) if parts.length == 1
396
-
397
- parts.drop(1).reduce(VarRef(Const(parts.fetch(0)))) do |parent, part|
398
- ConstPathRef(parent, Const(part))
399
- end
400
- end
401
-
402
294
  def source_mark(line_no, _source)
403
295
  "# #{Runtime::SourceMap::MARK_PREFIX}:#{line_no}"
404
296
  end
405
297
 
406
298
  def marked_expression(mark, expression)
407
- source = to_source(expression)
408
-
409
- source_marked_fragment(mark, source, node_for(expression))
299
+ Fragment.new("#{mark}\n#{to_source(expression)}", nil, true)
410
300
  end
411
301
 
412
302
  def factory_call(factory:, tag:, children:, props:, mark: nil)
@@ -415,7 +305,7 @@ module Klenod
415
305
 
416
306
  def component_factory_call(factory:, tag:, children:, props:, mark: nil)
417
307
  call = source_factory_call(factory: factory, tag: tag, children: [], props: props, mark: mark)
418
- body = Fragment.new("[#{children.map { |child| argument_source(child) }.join(", ")}]", nil)
308
+ body = Fragment.new("[#{children.map { |child| argument_source(child) }.join(", ")}]")
419
309
  script_block("#{call.source} do", body)
420
310
  end
421
311
 
@@ -435,7 +325,9 @@ module Klenod
435
325
 
436
326
  def script_block(source, body, line_no: nil)
437
327
  source = rewrite_ruby_source(source, nil)
438
- ast_script_block(source, body) || raise_ruby_parse_error(source, line_no: line_no, context: "Could not build Ruby block from Haml script")
328
+ return Fragment.new(block_source(source, body)) if block_script?(source)
329
+
330
+ raise_ruby_parse_error(source, line_no: line_no, context: "Could not build Ruby block from Haml script")
439
331
  end
440
332
 
441
333
  def silent_script_block(source, body, line_no: nil)
@@ -473,10 +365,9 @@ module Klenod
473
365
  raise_ruby_parse_error(source, line_no: line_no, context: "Could not parse Haml silent script", syntax_error: true)
474
366
  end
475
367
 
476
- return Fragment.new(source, statements) if return_with_children
368
+ return Fragment.new(source, statements, true) if return_with_children
477
369
 
478
- node = ast_begin([*statement_body_for(statements), *statement_body_for(body)])
479
- Fragment.new(["begin", indent(source, 2), indent(to_source(body), 2), "end"].join("\n"), node)
370
+ Fragment.new(["begin", indent(source, 2), indent(to_source(body), 2), "end"].join("\n"))
480
371
  end
481
372
 
482
373
  def keyword_script?(source)
@@ -508,7 +399,7 @@ module Klenod
508
399
  end
509
400
 
510
401
  def silent_branches(branches, line_no: nil)
511
- fragment = ast_silent_branches(branches)
402
+ fragment = ast_branches(branches)
512
403
  return fragment if fragment
513
404
 
514
405
  raise_ruby_parse_error(branch_source(branches), line_no: line_no, context: "Could not parse Haml silent branches", syntax_error: true)
@@ -517,32 +408,17 @@ module Klenod
517
408
  def ruby_filters(nodes)
518
409
  return "" if nodes.empty?
519
410
 
520
- ast_ruby_filters(nodes) || statements(nodes.map { |node| "begin\n#{indent(node, 2)}\nend" }.join("\n"))
411
+ Fragment.new(nodes.map { |node| ["begin", indent(to_source(node), 2), "end"].join("\n") }.join("\n"), nil, true)
521
412
  end
522
413
 
523
414
  def render_ruby_filter(node)
524
415
  source = to_source(node)
525
- parsed = (node if node.is_a?(Fragment) && node.node?) || statements(source)
526
- unless parsed&.node?
416
+ parsed = node.is_a?(Fragment) && node.node?
417
+ unless parsed || parse_statements(source)
527
418
  raise_ruby_parse_error(source, line_no: nil, context: "Could not parse Ruby filter")
528
419
  end
529
420
 
530
- fragment(
531
- ast_begin([
532
- *statement_body_for(parsed),
533
- nil_node
534
- ])
535
- )
536
- end
537
-
538
- def format_node(node)
539
- if @profiler
540
- @profiler.measure(:haml_format_node, node: node.class.name) do
541
- SyntaxTree::Formatter.format(+"", node, 0)
542
- end
543
- else
544
- SyntaxTree::Formatter.format(+"", node, 0)
545
- end
421
+ Fragment.new(["begin", indent(source.rstrip, 2), " nil", "end"].join("\n"))
546
422
  end
547
423
 
548
424
  def indent(value, spaces)
@@ -564,11 +440,12 @@ module Klenod
564
440
  raise_ruby_parse_error(source, line_no: line_no, context: context)
565
441
  end
566
442
 
443
+ # Whether the script opens a literal block for its Haml children.
444
+ # Calls, `super`, and zsuper can take one; lambdas cannot.
567
445
  def block_script?(source)
568
- fixed_source = fix_syntax_by_adding_missing_pairs(source)
569
- node = parse_expression(fixed_source, context: :block_script_predicate)
446
+ node = parse_expression(fix_syntax_by_adding_missing_pairs(source), context: :block_script)
570
447
 
571
- node.is_a?(SyntaxTree::MethodAddBlock)
448
+ node.respond_to?(:block) && node.block.is_a?(Prism::BlockNode)
572
449
  end
573
450
 
574
451
  private
@@ -616,14 +493,20 @@ module Klenod
616
493
  .filter_map do |((line, column), type, token, _state), index|
617
494
  kind, prefix, pattern = VARIABLE_TOKEN_KINDS[type]
618
495
  receiver = @variables[kind]
619
- if type == :on_gvar && token == "$*" && receiver
620
- [offset_for(line_offsets, line, column), token.bytesize, receiver]
621
- elsif receiver && token.match?(pattern)
622
- name = token.delete_prefix(prefix)
623
- replacement = "(#{receiver})[#{symbol_source(name)}]"
624
- replacement = "{#{replacement}}" if tokens[index - 1]&.fetch(1) == :on_embvar
625
- [offset_for(line_offsets, line, column), token.bytesize, replacement]
626
- end
496
+ next unless receiver
497
+
498
+ # Parenthesize the receiver so a low-precedence expression
499
+ # such as `a || b` stays one operand, e.g. in `$*.fetch(:x)`.
500
+ replacement =
501
+ if type == :on_gvar && token == "$*"
502
+ "(#{receiver})"
503
+ elsif token.match?(pattern)
504
+ "(#{receiver})[#{symbol_source(token.delete_prefix(prefix))}]"
505
+ end
506
+ next unless replacement
507
+
508
+ replacement = "{#{replacement}}" if tokens[index - 1]&.fetch(1) == :on_embvar
509
+ [offset_for(line_offsets, line, column), token.bytesize, replacement]
627
510
  end
628
511
  .reverse_each
629
512
  .each_with_object(source.dup) do |(offset, length, replacement), rewritten|
@@ -644,7 +527,7 @@ module Klenod
644
527
 
645
528
  expression.is_a?(Fragment) ? expression : self.expression(to_source(expression))
646
529
  else
647
- Fragment.new("[#{expressions.map { |item| argument_source(item) }.join(", ")}]", nil)
530
+ Fragment.new("[#{expressions.map { |item| argument_source(item) }.join(", ")}]")
648
531
  end
649
532
  end
650
533
 
@@ -656,39 +539,23 @@ module Klenod
656
539
  attribute_splats = props.delete(:__klenod_attribute_splats__) || []
657
540
  prop_sources = ["{#{keyword_props_source(props, mark: mark).join(", ")}}", *attribute_splats.map { |value| argument_source(value) }]
658
541
 
659
- source_parts = [
660
- to_source(tag),
661
- *children.map { |child| argument_source(child) },
662
- "**HamlHelper.merge_props(self.class, #{prop_sources.join(", ")})"
663
- ].compact
664
- Fragment.new("#{to_source(factory)}[#{source_parts.join(", ")}]", nil)
665
- end
542
+ leading_source = [to_source(tag), *children.map { |child| argument_source(child) }].join(", ")
543
+ props_source = "**HamlHelper.merge_props(self.class, #{prop_sources.join(", ")})"
666
544
 
667
- def ast_silent_script(source)
668
- statements = parse_statements(source) || control_flow_statements(source)
669
- return nil unless statements
670
-
671
- node = ast_begin([*statement_body_for(statements), nil_node])
672
-
673
- Fragment.new(["begin", indent(source, 2), " nil", "end"].join("\n"), node)
674
- end
675
-
676
- # SyntaxTree rejects `next` and `break` at top level even though a
677
- # silent script can be nested in an iterator or keyword loop. Parse
678
- # those statements inside a harmless temporary loop, then retain
679
- # only its body for the surrounding generated block.
680
- def control_flow_statements(source)
681
- return unless source.match?(/\A(?:next|break)\b/)
682
-
683
- wrapper = parse_expression(["loop do", indent(source, 2), "end"].join("\n"), context: :control_flow_statement)
684
- wrapper&.block&.bodystmt&.statements
545
+ # Multi-line children carry their own source marks. Repeat the
546
+ # tag's mark so errors raised while merging props map to the tag
547
+ # rather than to its last child.
548
+ if mark && leading_source.include?("\n")
549
+ Fragment.new("#{to_source(factory)}[#{leading_source},\n#{mark}\n#{props_source}]")
550
+ else
551
+ Fragment.new("#{to_source(factory)}[#{leading_source}, #{props_source}]")
552
+ end
685
553
  end
686
554
 
687
- def ast_script_block(source, body)
688
- node = block_script_node(source, body)
689
- return nil unless node
555
+ def ast_silent_script(source)
556
+ return nil unless parse_statements(source)
690
557
 
691
- Fragment.new(block_source(source, body), node)
558
+ Fragment.new(["begin", indent(source, 2), " nil", "end"].join("\n"))
692
559
  end
693
560
 
694
561
  def ast_silent_script_block(source, body)
@@ -707,48 +574,6 @@ module Klenod
707
574
  Fragment.new(source, node)
708
575
  end
709
576
 
710
- def ast_silent_branches(branches)
711
- node = branch_node(branches)
712
- return nil unless node
713
-
714
- Fragment.new(branch_source(branches), node)
715
- end
716
-
717
- def branch_node(branches)
718
- parse_expression(branch_source(branches), context: :branches)
719
- end
720
-
721
- def ast_ruby_filters(nodes)
722
- begins =
723
- nodes.map do |node|
724
- statements =
725
- if node.is_a?(Fragment)
726
- node
727
- else
728
- parse_statements(node)
729
- end
730
- return nil unless statements
731
-
732
- ast_begin(statement_body_for(statements))
733
- end
734
-
735
- source = nodes.map { |node| ["begin", indent(to_source(node), 2), "end"].join("\n") }.join("\n")
736
-
737
- Fragment.new(source, Statements(begins))
738
- end
739
-
740
- def ast_begin(statement_nodes)
741
- Begin(
742
- BodyStmt(
743
- Statements(statement_nodes),
744
- nil,
745
- nil,
746
- nil,
747
- nil
748
- )
749
- )
750
- end
751
-
752
577
  def block_source(source, body)
753
578
  body_source = to_source(body)
754
579
 
@@ -765,7 +590,7 @@ module Klenod
765
590
  # in their original Ruby block/loop context.
766
591
  def captured_block_source(source, body)
767
592
  captured_body = "HamlHelper.append_capture(#{argument_source(body)})"
768
- script = block_source(source, Fragment.new(captured_body, nil))
593
+ script = block_source(source, Fragment.new(captured_body))
769
594
  ["HamlHelper.capture do", indent(script, 2), "end"].join("\n")
770
595
  end
771
596
 
@@ -787,48 +612,6 @@ module Klenod
787
612
  "#{body}\nend"
788
613
  end
789
614
 
790
- def body_statement(body)
791
- BodyStmt(
792
- Statements(Array(body).flat_map { |statement| statement_body_for(statement) }),
793
- nil,
794
- nil,
795
- nil,
796
- nil
797
- )
798
- end
799
-
800
- def block_script_node(source, body)
801
- node = parse_expression(fix_syntax_by_adding_missing_pairs(source), context: :block_script)
802
- return nil unless node.is_a?(SyntaxTree::MethodAddBlock)
803
-
804
- MethodAddBlock(
805
- node.call,
806
- SyntaxTree::BlockNode.new(
807
- opening: node.block.opening,
808
- block_var: node.block.block_var,
809
- bodystmt: block_body_for(node.block, body),
810
- location: node.block.location
811
- )
812
- )
813
- end
814
-
815
- def block_body_for(block, body)
816
- statements = Statements(Array(body).flat_map { |statement| statement_body_for(statement) })
817
-
818
- block.opening.is_a?(SyntaxTree::LBrace) ? statements : body_statement(body)
819
- end
820
-
821
- def ast_keyword_props(props, mark:)
822
- return nil if props.empty?
823
-
824
- AssocSplat(
825
- HashLiteral(
826
- LBrace("{"),
827
- props.map { |name, value| Assoc(prop_key_node(name), argument_node(value, mark: mark)) }
828
- )
829
- )
830
- end
831
-
832
615
  def keyword_props_source(props, mark:)
833
616
  return [] if props.empty?
834
617
 
@@ -844,42 +627,14 @@ module Klenod
844
627
  "#{symbol_source(name)} =>"
845
628
  end
846
629
 
847
- def frozen_literal_node(value)
630
+ def frozen_literal_source(value)
848
631
  case value
849
632
  when Hash
850
- freeze_node(
851
- HashLiteral(
852
- LBrace("{"),
853
- value.map { |key, child| Assoc(literal_node(key), frozen_literal_node(child)) }
854
- )
855
- )
633
+ "{#{value.map { |key, child| "#{literal_source(key)} => #{frozen_literal_source(child)}" }.join(", ")}}.freeze"
856
634
  when Array
857
- freeze_node(ArrayLiteral(LBracket("["), Args(value.map { |child| frozen_literal_node(child) })))
635
+ "[#{value.map { |child| frozen_literal_source(child) }.join(", ")}].freeze"
858
636
  else
859
- literal_node(value)
860
- end
861
- end
862
-
863
- def literal_node(value)
864
- case value
865
- when String
866
- if value.match?(/\\|#[@${]/)
867
- expression_node(value.inspect)
868
- else
869
- StringLiteral([TStringContent(value)], "\"")
870
- end
871
- when Integer
872
- Int(value.to_s)
873
- when Float
874
- FloatLiteral(value.to_s)
875
- when true
876
- VarRef(Kw("true"))
877
- when false
878
- VarRef(Kw("false"))
879
- when nil
880
- nil_node
881
- else
882
- expression_node(value.inspect)
637
+ literal_source(value)
883
638
  end
884
639
  end
885
640
 
@@ -900,22 +655,6 @@ module Klenod
900
655
  end
901
656
  end
902
657
 
903
- def freeze_node(node)
904
- CallNode(node, Period("."), Ident("freeze"), nil)
905
- end
906
-
907
- def nil_node
908
- VarRef(Kw("nil"))
909
- end
910
-
911
- def symbol_node(value)
912
- if value.match?(/\A[a-zA-Z_]\w*[!?=]?\z/)
913
- SymbolLiteral(Ident(value))
914
- else
915
- DynaSymbol([TStringContent(value)], ":\"")
916
- end
917
- end
918
-
919
658
  def symbol_source(value)
920
659
  if value.match?(/\A[a-zA-Z_]\w*[!?=]?\z/)
921
660
  ":#{value}"
@@ -924,51 +663,6 @@ module Klenod
924
663
  end
925
664
  end
926
665
 
927
- def prop_key_node(name)
928
- name = name.to_s
929
- return Label("#{name}:") if name.match?(/\A[a-zA-Z_]\w*\z/)
930
-
931
- symbol_node(name)
932
- end
933
-
934
- def source_marked_fragment(mark, source, node)
935
- marked_source = "#{mark}\n#{source}"
936
- return Fragment.new(marked_source, nil) unless node
937
-
938
- node = Statements([comment_node(mark), node])
939
-
940
- Fragment.new(marked_source, node)
941
- end
942
-
943
- def comment_node(value)
944
- Comment(value, false)
945
- end
946
-
947
- def node_for(value)
948
- return value.node if value.is_a?(Fragment)
949
-
950
- parse_expression(to_source(value), context: :node_for)
951
- end
952
-
953
- def argument_node(value, mark: nil)
954
- fragment = expression_fragment(value)
955
- statements = []
956
- statements << comment_node(mark) if mark
957
-
958
- if fragment.node.is_a?(SyntaxTree::Statements)
959
- statements.concat(fragment.node.body)
960
- elsif mark
961
- node = node_for(fragment)
962
- return nil unless node
963
-
964
- statements << node
965
- else
966
- return node_for(fragment)
967
- end
968
-
969
- ast_begin(statements)
970
- end
971
-
972
666
  def argument_source(value, mark: nil)
973
667
  fragment = expression_fragment(value)
974
668
  source = to_source(fragment)
@@ -977,7 +671,7 @@ module Klenod
977
671
  source = "#{mark}\n#{source}"
978
672
  end
979
673
 
980
- if fragment.node.is_a?(SyntaxTree::Statements) || mark || (source.include?("\n") && !multiline_argument_expression?(source))
674
+ if fragment.statements || mark || (source.include?("\n") && !multiline_argument_expression?(source))
981
675
  ["begin", indent(source, 2), "end"].join("\n")
982
676
  else
983
677
  source
@@ -988,41 +682,33 @@ module Klenod
988
682
  source.start_with?("if ", "unless ", "case", "begin")
989
683
  end
990
684
 
991
- def statement_body_for(value)
992
- return value.statement_body if value.is_a?(Fragment)
993
-
994
- value.is_a?(SyntaxTree::Statements) ? value.body : [value].compact
995
- end
996
-
997
- def expression_node(source)
998
- source = source.to_s
999
- return constant_path(source) if source.match?(VALID_CONST_PATH)
1000
-
1001
- parse_expression(source, context: :expression_node) || raise(ArgumentError, "Could not parse Ruby expression: #{source.inspect}")
1002
- end
1003
-
1004
685
  def parse_expression(source, context:)
1005
686
  cached_parse(@expression_cache, source, :"haml_parse_expression:#{context}") do
1006
- SyntaxTree
1007
- .parse(source)
1008
- &.statements
1009
- &.body
1010
- &.find { |node| !node.instance_of?(SyntaxTree::Comment) }
687
+ parse_result(source)&.value&.statements&.body&.first
1011
688
  end
1012
- rescue SyntaxTree::Parser::ParseError
1013
- nil
1014
689
  end
1015
690
 
1016
691
  def parse_statements(source)
1017
- cached_parse(@statements_cache, source, :haml_parse_statements) { SyntaxTree.parse(source)&.statements }
1018
- rescue SyntaxTree::Parser::ParseError
1019
- nil
692
+ cached_parse(@statements_cache, source, :haml_parse_statements) { parse_result(source)&.value&.statements }
1020
693
  end
1021
694
 
1022
- def parse_program(source)
1023
- cached_parse(@program_cache, source, :haml_parse_program) { SyntaxTree.parse(source) }
1024
- rescue SyntaxTree::Parser::ParseError
1025
- nil
695
+ def parse_result(source)
696
+ result = prism_parse(source)
697
+ result if result.success?
698
+ end
699
+
700
+ # Haml scripts are fragments of a larger render method, so accept
701
+ # `yield`, `next`, and `break` outside of a block or loop.
702
+ #
703
+ # Parse each fragment as the line it came from. Haml strips script
704
+ # text, and Prism 1.9 rejects a hash pattern with several keys at
705
+ # the very end of its input (`props => title:, count:`) while
706
+ # accepting the same line followed by a newline.
707
+ def prism_parse(source)
708
+ source = source.to_s
709
+ source = "#{source}\n" unless source.end_with?("\n")
710
+
711
+ Prism.parse(source, partial_script: true)
1026
712
  end
1027
713
 
1028
714
  def cached_parse(cache, source, event_name)
@@ -1045,7 +731,7 @@ module Klenod
1045
731
  end
1046
732
 
1047
733
  def raise_ruby_parse_error(source, line_no:, context:, syntax_error: false)
1048
- parse_error = syntax_tree_parse_error(source)
734
+ error_line = parse_error_line(source)
1049
735
  explain =
1050
736
  SyntaxSuggest::ExplainSyntax.new(
1051
737
  code_lines: SyntaxSuggest::CodeLine.from_source(source)
@@ -1058,20 +744,21 @@ module Klenod
1058
744
  message << "Errors:\n #{errors.join("\n ")}" unless errors.empty?
1059
745
  message << "Missing:\n #{missing.join("\n ")}" unless missing.empty?
1060
746
 
1061
- raise RubyParseError.new(message.join("\n\n"), line: source_line_for_parse_error(line_no, parse_error))
747
+ raise RubyParseError.new(message.join("\n\n"), line: source_line_for_parse_error(line_no, error_line))
1062
748
  end
1063
749
 
1064
- def syntax_tree_parse_error(source)
1065
- SyntaxTree.parse(source)
1066
- nil
1067
- rescue SyntaxTree::Parser::ParseError => error
1068
- error
750
+ # Prism first reports an unclosed `{` or `(` at the line that opens
751
+ # it. The unexpected token that broke it is the line to show.
752
+ def parse_error_line(source)
753
+ errors = prism_parse(source).errors
754
+ error = errors.reject { it.type.end_with?("_term") }.min_by { it.location.start_offset } || errors.first
755
+ error&.location&.start_line
1069
756
  end
1070
757
 
1071
- def source_line_for_parse_error(line_no, parse_error)
1072
- return line_no unless line_no && parse_error&.lineno
758
+ def source_line_for_parse_error(line_no, error_line)
759
+ return line_no unless line_no && error_line
1073
760
 
1074
- line_no + parse_error.lineno - 1
761
+ line_no + error_line - 1
1075
762
  end
1076
763
  end
1077
764
  end