From b87d3e8a0ec7a8439e3e561ddde483e59cf21994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?fn=20=E2=8C=83=20=E2=8C=A5?= <70830482+FnControlOption@users.noreply.github.com> Date: Wed, 27 Nov 2024 01:56:43 -0800 Subject: [PATCH] Disallow weird assignments (#14815) --- spec/compiler/parser/parser_spec.cr | 47 +++++++++++++++++++++++++++ spec/support/syntax.cr | 4 +-- src/compiler/crystal/syntax/ast.cr | 1 + src/compiler/crystal/syntax/parser.cr | 26 +++++++++++---- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr index 09569b88f003..897e5bf7060c 100644 --- a/spec/compiler/parser/parser_spec.cr +++ b/spec/compiler/parser/parser_spec.cr @@ -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) @@ -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) @@ -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")))) @@ -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" @@ -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 diff --git a/spec/support/syntax.cr b/spec/support/syntax.cr index e1fd8f43d951..a6fe6286d11b 100644 --- a/spec/support/syntax.cr +++ b/spec/support/syntax.cr @@ -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 diff --git a/src/compiler/crystal/syntax/ast.cr b/src/compiler/crystal/syntax/ast.cr index f6d314371034..9ccd8dda1f69 100644 --- a/src/compiler/crystal/syntax/ast.cr +++ b/src/compiler/crystal/syntax/ast.cr @@ -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) diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr index 1f0b6160a363..da2a6b7a4f42 100644 --- a/src/compiler/crystal/syntax/parser.cr +++ b/src/compiler/crystal/syntax/parser.cr @@ -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 @@ -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 @@ -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 @@ -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}=" @@ -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 @@ -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}=" @@ -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