Skip to content

Commit

Permalink
Disallow weird assignments (crystal-lang#14815)
Browse files Browse the repository at this point in the history
  • Loading branch information
FnControlOption authored Nov 27, 2024
1 parent 6928ca7 commit b87d3e8
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 8 deletions.
47 changes: 47 additions & 0 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ module Crystal

it_parses "a = 1", Assign.new("a".var, 1.int32)
it_parses "a = b = 2", Assign.new("a".var, Assign.new("b".var, 2.int32))
it_parses "a[] = 1", Call.new("a".call, "[]=", 1.int32)
it_parses "a.[] = 1", Call.new("a".call, "[]=", 1.int32)

it_parses "a, b = 1, 2", MultiAssign.new(["a".var, "b".var] of ASTNode, [1.int32, 2.int32] of ASTNode)
it_parses "a, b = 1", MultiAssign.new(["a".var, "b".var] of ASTNode, [1.int32] of ASTNode)
Expand Down Expand Up @@ -276,6 +278,10 @@ module Crystal
assert_syntax_error "a.b() += 1"
assert_syntax_error "a.[]() += 1"

assert_syntax_error "a.[] 0 = 1"
assert_syntax_error "a.[] 0 += 1"
assert_syntax_error "a b: 0 = 1"

it_parses "def foo\n1\nend", Def.new("foo", body: 1.int32)
it_parses "def downto(n)\n1\nend", Def.new("downto", ["n".arg], 1.int32)
it_parses "def foo ; 1 ; end", Def.new("foo", body: 1.int32)
Expand Down Expand Up @@ -524,11 +530,15 @@ module Crystal
it_parses "foo &.+(2)", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "+", 2.int32)))
it_parses "foo &.bar.baz", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Call.new(Var.new("__arg0"), "bar"), "baz")))
it_parses "foo(&.bar.baz)", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Call.new(Var.new("__arg0"), "bar"), "baz")))
it_parses "foo &.block[]", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Call.new(Var.new("__arg0"), "block"), "[]")))
it_parses "foo &.block[0]", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Call.new(Var.new("__arg0"), "block"), "[]", 0.int32)))
it_parses "foo &.block=(0)", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "block=", 0.int32)))
it_parses "foo &.block = 0", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "block=", 0.int32)))
it_parses "foo &.block[] = 1", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Call.new(Var.new("__arg0"), "block"), "[]=", 1.int32)))
it_parses "foo &.block[0] = 1", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Call.new(Var.new("__arg0"), "block"), "[]=", 0.int32, 1.int32)))
it_parses "foo &.[]", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "[]")))
it_parses "foo &.[0]", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "[]", 0.int32)))
it_parses "foo &.[] = 1", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "[]=", 1.int32)))
it_parses "foo &.[0] = 1", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "[]=", 0.int32, 1.int32)))
it_parses "foo(&.is_a?(T))", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], IsA.new(Var.new("__arg0"), "T".path)))
it_parses "foo(&.!)", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Not.new(Var.new("__arg0"))))
Expand Down Expand Up @@ -2226,6 +2236,31 @@ module Crystal

assert_syntax_error "lib Foo%end", %(unexpected token: "%")

assert_syntax_error "foo.[]? = 1"
assert_syntax_error "foo.[]? += 1"
assert_syntax_error "foo[0]? = 1"
assert_syntax_error "foo[0]? += 1"
assert_syntax_error "foo.[0]? = 1"
assert_syntax_error "foo.[0]? += 1"
assert_syntax_error "foo &.[0]? = 1"
assert_syntax_error "foo &.[0]? += 1"

assert_syntax_error "foo &.[]?=(1)"
assert_syntax_error "foo &.[]? = 1"
assert_syntax_error "foo &.[]? 0 =(1)"
assert_syntax_error "foo &.[]? 0 = 1"
assert_syntax_error "foo &.[]?(0)=(1)"
assert_syntax_error "foo &.[]?(0) = 1"
assert_syntax_error "foo &.[] 0 =(1)"
assert_syntax_error "foo &.[] 0 = 1"
assert_syntax_error "foo &.[](0)=(1)"
assert_syntax_error "foo &.[](0) = 1"

assert_syntax_error "foo &.bar.[] 0 =(1)"
assert_syntax_error "foo &.bar.[] 0 = 1"
assert_syntax_error "foo &.bar.[](0)=(1)"
assert_syntax_error "foo &.bar.[](0) = 1"

describe "end locations" do
assert_end_location "nil"
assert_end_location "false"
Expand Down Expand Up @@ -3021,5 +3056,17 @@ module Crystal
node = Parser.parse(source).as(Annotation).path
node_source(source, node).should eq("::Foo")
end

it "sets args_in_brackets to false for `a.b`" do
parser = Parser.new("a.b")
node = parser.parse.as(Call)
node.args_in_brackets?.should be_false
end

it "sets args_in_brackets to true for `a[b]`" do
parser = Parser.new("a[b]")
node = parser.parse.as(Call)
node.args_in_brackets?.should be_true
end
end
end
4 changes: 2 additions & 2 deletions spec/support/syntax.cr
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ class Crystal::ASTNode
end
end

def assert_syntax_error(str, message = nil, line = nil, column = nil, metafile = __FILE__, metaline = __LINE__, metaendline = __END_LINE__)
it "says syntax error on #{str.inspect}", metafile, metaline, metaendline do
def assert_syntax_error(str, message = nil, line = nil, column = nil, metafile = __FILE__, metaline = __LINE__, metaendline = __END_LINE__, *, focus : Bool = false)
it "says syntax error on #{str.inspect}", metafile, metaline, metaendline, focus: focus do
begin
parse str
fail "Expected SyntaxException to be raised", metafile, metaline
Expand Down
1 change: 1 addition & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ module Crystal
property visibility = Visibility::Public
property? global : Bool
property? expansion = false
property? args_in_brackets = false
property? has_parentheses = false

def initialize(@obj, @name, @args = [] of ASTNode, @block = nil, @block_arg = nil, @named_args = nil, @global : Bool = false)
Expand Down
26 changes: 20 additions & 6 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,9 @@ module Crystal

case @token.type
when .op_eq?
atomic = Call.new(atomic, name)
unexpected_token unless can_be_assigned?(atomic)

# Rewrite 'f.x = arg' as f.x=(arg)
next_token

Expand All @@ -760,15 +763,20 @@ module Crystal
end_location = arg.end_location
end

atomic = Call.new(atomic, "#{name}=", arg).at(location).at_end(end_location)
atomic.at(location).at_end(end_location)
atomic.name = "#{name}="
atomic.args = [arg] of ASTNode
atomic.name_location = name_location
next
when .assignment_operator?
call = Call.new(atomic, name)
unexpected_token unless can_be_assigned?(call)

op_name_location = @token.location
method = @token.type.to_s.byte_slice(0, @token.type.to_s.size - 1)
next_token_skip_space_or_newline
value = parse_op_assign
call = Call.new(atomic, name).at(location)
call.at(location)
call.name_location = name_location
atomic = OpAssign.new(call, method, value).at(location)
atomic.name_location = op_name_location
Expand Down Expand Up @@ -848,7 +856,8 @@ module Crystal
atomic = Call.new(atomic, method_name, (args || [] of ASTNode), block, block_arg, named_args).at(location)
atomic.name_location = name_location
atomic.end_location = end_location
atomic.name_size = 0 if atomic.is_a?(Call)
atomic.name_size = 0
atomic.args_in_brackets = true
atomic
else
break
Expand Down Expand Up @@ -1622,7 +1631,7 @@ module Crystal
elsif @token.type.op_lsquare?
call = parse_atomic_method_suffix obj, location

if @token.type.op_eq? && call.is_a?(Call)
if @token.type.op_eq? && call.is_a?(Call) && can_be_assigned?(call)
next_token_skip_space
exp = parse_op_assign
call.name = "#{call.name}="
Expand All @@ -1643,6 +1652,8 @@ module Crystal
call = call.as(Call)

if @token.type.op_eq?
unexpected_token unless can_be_assigned?(call)

next_token_skip_space
if @token.type.op_lparen?
next_token_skip_space
Expand All @@ -1660,7 +1671,7 @@ module Crystal
else
call = parse_atomic_method_suffix call, location

if @token.type.op_eq? && call.is_a?(Call) && call.name == "[]"
if @token.type.op_eq? && call.is_a?(Call) && can_be_assigned?(call)
next_token_skip_space
exp = parse_op_assign
call.name = "#{call.name}="
Expand Down Expand Up @@ -6187,7 +6198,10 @@ module Crystal
when Var, InstanceVar, ClassVar, Path, Global, Underscore
true
when Call
!node.has_parentheses? && ((node.obj.nil? && node.args.empty? && node.block.nil?) || node.name == "[]")
return false if node.has_parentheses?
no_args = node.args.empty? && node.named_args.nil? && node.block.nil?
return true if Lexer.ident?(node.name) && no_args
node.name == "[]" && (node.args_in_brackets? || no_args)
else
false
end
Expand Down

0 comments on commit b87d3e8

Please sign in to comment.