Skip to content

Commit e809da0

Browse files
Merge pull request #88 from reliantsolutions/fix-codegen
Fix codegen
2 parents e65f59e + 331b9e1 commit e809da0

File tree

11 files changed

+177
-33
lines changed

11 files changed

+177
-33
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ jobs:
2121
uses: actions/checkout@v3
2222
- name: Install Crystal
2323
uses: crystal-lang/install-crystal@v1
24+
- name: Check formatting
25+
run: crystal tool format --check
2426
- name: Install shards
2527
run: shards update
2628
- name: Run tests
2729
run: crystal spec --order=random
28-
- name: Check formatting
29-
run: crystal tool format --check
3030
if: matrix.os == 'ubuntu-latest'

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*~
88
\#*\#
99

10+
/spec/integration/codegen-test-*
11+
1012
# Libraries don't need dependency lock
1113
# Dependencies will be locked in application that uses them
1214
/shard.lock

spec/integration/integration_spec.cr

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
require "yaml"
22
require "../spec_helper"
33

4+
# [golden-liquid](https://github.com/jg-rp/golden-liquid) is a test suite for
5+
# liquid template, tests are found in spec/integration/golden_liquid.yaml, a
6+
# list of tests that are expected to fail can be found at
7+
# spec/integration/golden_liquid.pending.
8+
#
9+
# All golden liquid tests are tagged with `golden`. Tests are run in two modes,
10+
# using the render visitor directly (tagged with `render`) and using the
11+
# codegen visitor (tagged with `codegen`), besides a numeric tag for each test.
12+
#
13+
# For code gen tests Crystal code is written in files like
14+
# `spec/integration/codegen-test-XXX.cr` where XXX is the test number.
415
class GoldenTest
516
include YAML::Serializable
617

@@ -25,6 +36,40 @@ class GoldenTest
2536
ctx
2637
end
2738

39+
private def context_to_code(context : Liquid::Context) : String
40+
String.build do |str|
41+
str << "Liquid::Context{"
42+
context.each do |key, value|
43+
key.inspect(str)
44+
str << " => " << any_to_code(value)
45+
str << ", "
46+
end
47+
str << "}"
48+
end
49+
end
50+
51+
private def any_to_code(any : Liquid::Any) : String
52+
raw = any.raw
53+
String.build do |str|
54+
str << "Liquid::Any.new("
55+
56+
if raw.is_a?(Array)
57+
str << "["
58+
raw.each { |item| str << any_to_code(item) << ", " }
59+
str << "] of Liquid::Any"
60+
elsif raw.is_a?(Hash)
61+
str << "{"
62+
raw.each do |key, value|
63+
str << key.inspect << "=>" << any_to_code(value) << ", "
64+
end
65+
str << "} of String => Liquid::Any"
66+
else
67+
raw.inspect(str)
68+
end
69+
str << ")"
70+
end
71+
end
72+
2873
def test!
2974
if @error
3075
expect_raises(LiquidException) do
@@ -34,6 +79,55 @@ class GoldenTest
3479
Parser.parse(@template).render(context).should eq(@want)
3580
end
3681
end
82+
83+
def codegen_test!(test_group, test_number)
84+
test_path = Path[__DIR__, "codegen-test-#{test_number}.cr"]
85+
test = File.open(test_path, "w")
86+
test.puts("# #{test_group.name}.#{@name}\n\n")
87+
generate_codegen_test_source(test)
88+
output = `crystal run #{Process.quote(test.path)} --error-trace`
89+
$?.exit_code.should eq(0)
90+
output.should eq(@want) unless @error
91+
end
92+
93+
private def generate_codegen_test_source(io) : Nil
94+
error_mode = @strict || @error ? Context::ErrorMode::Strict : Context::ErrorMode::Lax
95+
96+
io.puts(<<-CRYSTAL)
97+
require "../../src/liquid"
98+
99+
TEMPLATE =<<-LIQUID
100+
#{Liquid::CodeGenVisitor.escape(@template)}
101+
LIQUID
102+
103+
WANT =<<-TEXT
104+
#{Liquid::CodeGenVisitor.escape(@want)}
105+
TEXT
106+
107+
# CONTEXT
108+
expects_error = #{@error}
109+
context = #{context_to_code(context)}
110+
context.error_mode = :#{error_mode}
111+
112+
# CODEGEN OUTPUT
113+
CRYSTAL
114+
115+
tpl = Liquid::Template.parse(@template)
116+
visitor = CodeGenVisitor.new(io)
117+
tpl.root.accept(visitor)
118+
io.puts(<<-CRYSTAL)
119+
begin
120+
Liquid::Template.new(root).render(context, STDOUT)
121+
rescue ex : Liquid::InvalidExpression
122+
raise ex unless expects_error
123+
end
124+
CRYSTAL
125+
126+
rescue ex : Liquid::LiquidException
127+
io << "abort(" << ex.message.inspect << ") unless expects_error\n"
128+
ensure
129+
io.close
130+
end
37131
end
38132

39133
class GoldenTestGroup
@@ -68,7 +162,6 @@ private def yaml_any_to_liquid_any(yaml : YAML::Any) : Liquid::Any
68162
end
69163
end
70164

71-
# FIXME: One all tests pass we must remove this class
72165
class PendingGold
73166
@@pending : Array(String)?
74167

@@ -84,15 +177,22 @@ end
84177
describe "Golden Liquid Tests" do
85178
i = 1
86179
skip_pending_tests = ENV["SKIP_PENDING"]?
180+
87181
GoldenLiquid.from_yaml(File.read(File.join(__DIR__, "golden_liquid.yaml"))).test_groups.each do |test_group|
88-
describe test_group.name do
182+
describe test_group.name, tags: "golden" do
89183
test_group.tests.each do |test|
90184
if PendingGold.pending?(test_group.name, test.name)
91185
pending(test.name, line: i) unless skip_pending_tests
92186
else
93-
it test.name, line: i do
187+
it "#{test.name} [test-#{i} render]", tags: ["test-#{i}", "render"] do
94188
test.test!
95189
end
190+
191+
i += 1
192+
dup_i = i
193+
it "#{test.name} [test-#{i} codegen]", tags: ["test-#{i}", "codegen"] do
194+
test.codegen_test!(test_group, dup_i)
195+
end
96196
end
97197
i += 1
98198
end

src/liquid/blank.cr

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@ module Liquid
1717
def ==(other : Any)
1818
self == other.raw
1919
end
20+
21+
def inspect(io : IO)
22+
io << "Liquid::Blank.new"
23+
end
2024
end
2125
end

src/liquid/blocks/block.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module Liquid::Block
2323
end
2424
end
2525

26-
protected def inspect(io : IO)
26+
protected def inspect(io : IO, &)
2727
io << '<'
2828
io << '-' if lstrip?
2929
io << ' '

src/liquid/blocks/when.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ require "./block"
22

33
module Liquid::Block
44
class When < InlineBlock
5-
@when_expressions : Array(Expression)
5+
getter when_expressions : Array(Expression)
66

77
def initialize(content : String)
88
@when_expressions = Array(Expression).new

src/liquid/codegen_visitor.cr

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,21 @@ module Liquid
4141
@io << new_var << " = " << some << "\n"
4242
end
4343

44-
def escape(some : String)
45-
some.gsub '"', "\\\""
44+
def escape(text : String) : String
45+
CodeGenVisitor.escape(text)
46+
end
47+
48+
def self.escape(text : String) : String
49+
text.gsub do |char|
50+
case char
51+
when '"' then "\\\""
52+
when '\n' then "\\n"
53+
when '\r' then "\\r"
54+
when '\t' then "\\t"
55+
else
56+
char
57+
end
58+
end
4659
end
4760

4861
def visit(node : Node)
@@ -54,8 +67,7 @@ module Liquid
5467
end
5568

5669
def visit(node : Assign)
57-
to_io %(Liquid::Block::Assign.new("#{escape node.varname}",
58-
Liquid::Block::ExpressionNode.new("#{escape node.value.var}")))
70+
to_io %(Liquid::Block::Assign.new("#{escape node.varname}", Liquid::Expression.new("#{escape node.value.expression}")))
5971
end
6072

6173
def visit(node : Include)
@@ -70,9 +82,7 @@ module Liquid
7082
end
7183

7284
def visit(node : Case)
73-
def_to_io %(Liquid::Block::ExpressionNode.new(
74-
"#{escape node.expression.not_nil!.var}"))
75-
def_to_io "Liquid::Block::Case.new(#{@last_var})"
85+
def_to_io %(Liquid::Block::Case.new("#{escape node.expression.expression}"))
7686
push
7787
node.children.each &.accept self
7888
if arr = node.when
@@ -84,26 +94,42 @@ module Liquid
8494
pop
8595
end
8696

87-
def visit(node : For)
88-
if node.begin && node.end
89-
def_to_io %(Liquid::Block::For.new("#{node.loop_var}",
90-
#{node.begin}, #{node.end}))
91-
else
92-
def_to_io "Liquid::Block::For.new(\"#{node.loop_var}\", \"#{node.loop_over}\")"
97+
def visit(node : When)
98+
expressions = node.when_expressions.map do |expression|
99+
escape(expression.expression)
93100
end
101+
def_to_io %(Liquid::Block::When.new("#{expressions.join(", ")}"))
102+
push
103+
node.children.each &.accept(self)
104+
pop
105+
end
106+
107+
def visit(node : For)
108+
def_to_io %(Liquid::Block::For.new("#{node.loop_var}", #{node.loop_over.inspect}))
94109
push
95110
node.children.each &.accept(self)
96111
pop
97112
end
98113

99114
def visit(node : ExpressionNode)
100-
to_io %(Liquid::Block::ExpressionNode.new("#{escape node.var}"))
115+
to_io %(Liquid::Block::ExpressionNode.new("#{escape node.expression}"))
101116
end
102117

103118
def visit(node : If)
104-
def_to_io %(Liquid::Block::ExpressionNode.new(
105-
"#{escape node.expression.not_nil!.var}"))
106-
def_to_io "Liquid::Block::If.new(#{@last_var})"
119+
def_to_io %(Liquid::Block::If.new("#{escape node.expression.expression}"))
120+
push
121+
node.children.each &.accept self
122+
if arr = node.elsif
123+
arr.each &.accept self
124+
end
125+
if e = node.else
126+
e.accept self
127+
end
128+
pop
129+
end
130+
131+
def visit(node : Unless)
132+
def_to_io %(Liquid::Block::Unless.new("#{escape node.expression.expression}"))
107133
push
108134
node.children.each &.accept self
109135
if arr = node.elsif
@@ -116,7 +142,7 @@ module Liquid
116142
end
117143

118144
def visit(node : ElsIf)
119-
def_to_io "Liquid::Block::ElsIf.new( ExpressionNode.new(\"#{escape node.expression.var}\"))"
145+
def_to_io %(Liquid::Block::ElsIf.new("#{escape node.expression.expression}"))
120146
push
121147
node.children.each &.accept self
122148
pop

src/liquid/context.cr

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module Liquid
3838
delegate :strict?, to: @error_mode
3939
delegate :warn?, to: @error_mode
4040
delegate :lax?, to: @error_mode
41+
delegate :each, to: @data
4142

4243
@[Deprecated("Use `initialize(ErrorMode)` instead.")]
4344
def initialize(strict : Bool)
@@ -105,6 +106,17 @@ module Liquid
105106
@data[var] = value
106107
end
107108

109+
@[Deprecated("Use `set(String, ::Liquid::Any)`")]
110+
def set(var : String, value : Hash(String, T)) : Any forall T
111+
mapped_values = value.transform_values { |v| Any.new(v) }
112+
set(var, mapped_values)
113+
end
114+
115+
# Sets the value for *var* to the given *value*.
116+
def set(var : String, value : Hash(String, Any)) : Any
117+
set(var, Any.new(value))
118+
end
119+
108120
# Sets the value for *var* to an instance of `Liquid::Any` generated from *value*.
109121
def set(var : String, value : Any::Type) : Any
110122
set(var, Any.new(value))

src/liquid/for_loop.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Liquid
1010
end
1111

1212
@[Ignore]
13-
def each
13+
def each(&)
1414
collection = @collection
1515
if collection.is_a?(Array) || collection.is_a?(Range)
1616
collection.each do |val|

src/liquid/template.cr

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ module Liquid
4949

5050
unless context
5151
context = "context"
52-
io.puts <<-EOF
53-
context = Liquid::Context.new
54-
{% for var in @type.instance_vars %}
55-
context.set {{var.id.stringify}}, @{{var.id}}
56-
{% end %}
57-
EOF
52+
io.puts <<-CRYSTAL
53+
context = Liquid::Context.new
54+
{% for var in @type.instance_vars %}
55+
context.set({{var.id.stringify}}, @{{var.id}})
56+
{% end %}
57+
CRYSTAL
5858
end
5959

6060
root.accept visitor

0 commit comments

Comments
 (0)