Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow weird assignments #14815

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2821,5 +2856,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 @@ -6185,7 +6196,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
Loading