Skip to content

Commit

Permalink
Parser: prevent to finish a macro body when {% if/for/begin %} is nes…
Browse files Browse the repository at this point in the history
…ted (#4801)

Fixed #4769

This introduced `control_nest` counter variable to keep macro control
(if/for/begin) nesting information. When the parser find `end` inside
macro body, it checks `control_nest` and finishes a macro body if and
only if `control_nest` is `0` (and `nest` which is usual block nesting
information is `0` also).

And formatter is fixed also.
  • Loading branch information
makenowjust authored and RX14 committed Aug 10, 2017
1 parent 020796f commit de80a09
Show file tree
Hide file tree
Showing 6 changed files with 31 additions and 14 deletions.
1 change: 1 addition & 0 deletions spec/compiler/formatter/formatter_spec.cr
Expand Up @@ -549,6 +549,7 @@ describe Crystal::Formatter do
assert_format "macro foo\n \\{\nend"
assert_format "macro foo\n {% if 1 %} 2 {% elsif 3 %} 4 {% else %} 5 {% end %}\nend"
assert_format "macro [](x)\nend"
assert_format "macro foo\n {% if true %}if true{% end %}\n {% if true %}end{% end %}\nend"

assert_format "def foo\na = bar do\n1\nend\nend", "def foo\n a = bar do\n 1\n end\nend"
assert_format "def foo\nend\ndef bar\nend", "def foo\nend\n\ndef bar\nend"
Expand Down
1 change: 1 addition & 0 deletions spec/compiler/parser/parser_spec.cr
Expand Up @@ -770,6 +770,7 @@ describe "Parser" do

it_parses "{% for x in y %}body{% end %}", MacroFor.new(["x".var], "y".var, "body".macro_literal)
it_parses "{% if x %}body{% end %}", MacroIf.new("x".var, "body".macro_literal)
it_parses "{% begin %}{% if true %}if true{% end %}\n{% if true %}end{% end %}{% end %}", MacroIf.new(true.bool, [MacroIf.new(true.bool, "if true".macro_literal), "\n".macro_literal, MacroIf.new(true.bool, "end".macro_literal)] of ASTNode)
it_parses "{{ foo }}", MacroExpression.new("foo".var)

it_parses "macro foo;%var;end", Macro.new("foo", [] of Arg, Expressions.from([MacroVar.new("var"), MacroLiteral.new(";")] of ASTNode))
Expand Down
17 changes: 9 additions & 8 deletions src/compiler/crystal/syntax/lexer.cr
Expand Up @@ -1914,6 +1914,7 @@ module Crystal

def next_macro_token(macro_state, skip_whitespace)
nest = macro_state.nest
control_nest = macro_state.control_nest
whitespace = macro_state.whitespace
delimiter_state = macro_state.delimiter_state
beginning_of_line = macro_state.beginning_of_line
Expand Down Expand Up @@ -1964,7 +1965,7 @@ module Crystal

@token.type = :MACRO_LITERAL
@token.value = string_range(start)
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
set_token_raw_from_start(start)
return @token
end
Expand All @@ -1975,7 +1976,7 @@ module Crystal
next_char
@token.type = :MACRO_LITERAL
@token.value = "%"
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
@token.raw = "%"
return @token
end
Expand All @@ -1986,13 +1987,13 @@ module Crystal
beginning_of_line = false
next_char
@token.type = :MACRO_EXPRESSION_START
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
return @token
when '%'
beginning_of_line = false
next_char
@token.type = :MACRO_CONTROL_START
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
return @token
else
# Make sure to decrease the '}' count if inside an interpolation
Expand Down Expand Up @@ -2024,7 +2025,7 @@ module Crystal
end
@token.type = :MACRO_LITERAL
@token.value = string_range(start)
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
set_token_raw_from_start(start)
return @token
end
Expand All @@ -2042,7 +2043,7 @@ module Crystal
beginning_of_line = false
@token.type = :MACRO_VAR
@token.value = string_range_from_pool(start)
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
return @token
end
end
Expand All @@ -2052,7 +2053,7 @@ module Crystal
case next_char
when 'd'
if whitespace && !ident_part_or_end?(peek_next_char)
if nest == 0
if nest == 0 && control_nest == 0
next_char
@token.type = :MACRO_END
@token.macro_state = Token::MacroState.default
Expand Down Expand Up @@ -2192,7 +2193,7 @@ module Crystal

@token.type = :MACRO_LITERAL
@token.value = string_range(start)
@token.macro_state = Token::MacroState.new(whitespace, nest, delimiter_state, beginning_of_line, yields, comment)
@token.macro_state = Token::MacroState.new(whitespace, nest, control_nest, delimiter_state, beginning_of_line, yields, comment)
set_token_raw_from_start(start)

@token
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/crystal/syntax/parser.cr
Expand Up @@ -2896,7 +2896,9 @@ module Crystal

check :"%}"

macro_state.control_nest += 1
body, end_location = parse_macro_body(start_line, start_column, macro_state)
macro_state.control_nest -= 1

check_ident :end
next_token_skip_space
Expand All @@ -2920,7 +2922,9 @@ module Crystal
next_token_skip_space
check :"%}"

macro_state.control_nest += 1
body, end_location = parse_macro_body(start_line, start_column, macro_state)
macro_state.control_nest -= 1

check_ident :end
next_token_skip_space
Expand Down Expand Up @@ -2953,15 +2957,19 @@ module Crystal

check :"%}"

macro_state.control_nest += 1
a_then, end_location = parse_macro_body(start_line, start_column, macro_state)
macro_state.control_nest -= 1

if @token.type == :IDENT
case @token.value
when :else
next_token_skip_space
check :"%}"

macro_state.control_nest += 1
a_else, end_location = parse_macro_body(start_line, start_column, macro_state)
macro_state.control_nest -= 1

if check_end
check_ident :end
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/crystal/syntax/token.cr
Expand Up @@ -18,15 +18,17 @@ module Crystal
record MacroState,
whitespace : Bool,
nest : Int32,
control_nest : Int32,
delimiter_state : DelimiterState?,
beginning_of_line : Bool,
yields : Bool,
comment : Bool do
def self.default
MacroState.new(true, 0, nil, true, false, false)
MacroState.new(true, 0, 0, nil, true, false, false)
end

setter whitespace
setter control_nest
end

record DelimiterState,
Expand Down
14 changes: 9 additions & 5 deletions src/compiler/crystal/tools/formatter.cr
Expand Up @@ -1761,7 +1761,6 @@ module Crystal
end
write "{% "

macro_state = @macro_state
next_token_skip_space_or_newline

if @token.keyword?(:begin)
Expand All @@ -1780,7 +1779,7 @@ module Crystal
outside_macro { indent(@column, node.cond) }
end

format_macro_if_epilogue node, macro_state
format_macro_if_epilogue(node, @macro_state)
end

def format_macro_if_epilogue(node, macro_state, check_end = true)
Expand All @@ -1789,14 +1788,14 @@ module Crystal
write " %}"

@macro_state = macro_state
@macro_state.control_nest += 1
check_macro_whitespace
next_macro_token

inside_macro { no_indent node.then }

unless node.else.is_a?(Nop)
check :MACRO_CONTROL_START
macro_state = @macro_state
next_token_skip_space_or_newline

if @token.keyword?(:elsif)
Expand All @@ -1811,16 +1810,19 @@ module Crystal
check :"%}"

write "{% else %}"

@macro_state = macro_state
@macro_state.control_nest += 1
check_macro_whitespace
next_macro_token

inside_macro { no_indent node.else }
end
end

@macro_state = macro_state
if check_end
check :MACRO_CONTROL_START
macro_state = @macro_state
next_token_skip_space_or_newline

check_end
Expand All @@ -1842,6 +1844,7 @@ module Crystal

def visit(node : MacroFor)
reset_macro_state
old_macro_state = @macro_state

if inside_macro?
check :MACRO_CONTROL_START
Expand Down Expand Up @@ -1876,13 +1879,14 @@ module Crystal
check :"%}"
write " %}"

@macro_state.control_nest += 1
check_macro_whitespace
next_macro_token

inside_macro { no_indent node.body }
@macro_state = old_macro_state

check :MACRO_CONTROL_START
macro_state = @macro_state
next_token_skip_space_or_newline

check_end
Expand Down

0 comments on commit de80a09

Please sign in to comment.