diff --git a/ast.go b/ast.go index 21e939c2..41ba7c0e 100644 --- a/ast.go +++ b/ast.go @@ -913,8 +913,9 @@ finish: valArgs[i] = v.Val } ret = &internal.Elem{ - Val: &ast.CallExpr{Fun: fnVal, Args: valArgs, Ellipsis: token.Pos(flags & InstrFlagEllipsis)}, - Type: typ, + Val: &ast.CallExpr{Fun: fnVal, Args: valArgs, Ellipsis: token.Pos(flags & InstrFlagEllipsis)}, + Type: typ, + Flags: internal.ElemFlagTypeCast, } if len(args) == 1 { // TODO: const value may changed by type-convert ret.CVal = args[0].CVal diff --git a/builtin.go b/builtin.go index 3c91cbd4..b8a8917d 100644 --- a/builtin.go +++ b/builtin.go @@ -656,6 +656,7 @@ retry: pkg.NewParam(token.NoPos, "", typ), pkg.NewParam(token.NoPos, "", types.Typ[types.Bool])) } + // Channel receive operations can appear in statement context ret = &Element{Val: &ast.UnaryExpr{Op: token.ARROW, X: args[0].Val}, Type: typ} return } diff --git a/codebuild.go b/codebuild.go index b3f5c435..202b9b47 100644 --- a/codebuild.go +++ b/codebuild.go @@ -1270,6 +1270,10 @@ func (p *CodeBuilder) Index(nidx int, twoValue bool, src ...ast.Node) *CodeBuild elem := &internal.Elem{ Val: &ast.IndexExpr{X: args[0].Val, Index: args[1].Val}, Type: tyRet, Src: srcExpr, } + // Map index expressions have special error message format + if allowTwoValue { + elem.Flags = internal.ElemFlagMapIndex + } // TODO: check index type p.stk.Ret(2, elem) return p @@ -2559,13 +2563,14 @@ func (p *CodeBuilder) TypeAssert(typ types.Type, twoValue bool, src ...ast.Node) } pkg := p.pkg ret := &ast.TypeAssertExpr{X: arg.Val, Type: toType(pkg, typ)} + // Type assertions have comma-ok mode for error messages if twoValue { tyRet := types.NewTuple( pkg.NewParam(token.NoPos, "", typ), pkg.NewParam(token.NoPos, "", types.Typ[types.Bool])) - p.stk.Ret(1, &internal.Elem{Type: tyRet, Val: ret, Src: getSrc(src)}) + p.stk.Ret(1, &internal.Elem{Type: tyRet, Val: ret, Src: getSrc(src), Flags: internal.ElemFlagCommaOk}) } else { - p.stk.Ret(1, &internal.Elem{Type: typ, Val: ret, Src: getSrc(src)}) + p.stk.Ret(1, &internal.Elem{Type: typ, Val: ret, Src: getSrc(src), Flags: internal.ElemFlagCommaOk}) } return p } @@ -2824,6 +2829,186 @@ func (p *CodeBuilder) ResetStmt() { p.stk.SetLen(p.current.base) } +// expressionKindBuiltins contains builtin functions whose return values must be used. +// These functions cannot appear in statement context. +var expressionKindBuiltins = map[string]bool{ + "append": true, + "cap": true, + "complex": true, + "imag": true, + "len": true, + "make": true, + "max": true, + "min": true, + "new": true, + "real": true, +} + +// isValidStmtExpr checks if an expression can be used as a statement. +// According to Go spec, only function calls and receive operations can +// appear in statement context. Type conversions and certain builtin +// function calls (whose results must be used) are not valid. +func isValidStmtExpr(e *internal.Elem) bool { + // Check element flags first + switch e.Flags { + case internal.ElemFlagTypeCast, internal.ElemFlagMapIndex, internal.ElemFlagCommaOk: + return false + } + val := e.Val + // Unwrap parentheses: (foo()) and (<-ch) are valid + for { + if paren, ok := val.(*ast.ParenExpr); ok { + val = paren.X + } else { + break + } + } + switch v := val.(type) { + case *ast.CallExpr: + // Check if it's a builtin function whose result must be used + if ident, ok := v.Fun.(*ast.Ident); ok { + if expressionKindBuiltins[ident.Name] { + return false + } + } + return true + case *ast.UnaryExpr: + // Receive operation: <-ch + return v.Op == token.ARROW + } + return false +} + +// exprCode returns a string representation of an AST expression for error messages. +func exprCode(val ast.Expr) string { + switch v := val.(type) { + case *ast.Ident: + return v.Name + case *ast.BasicLit: + return v.Value + case *ast.CallExpr: + return exprCode(v.Fun) + "(...)" + case *ast.SelectorExpr: + return exprCode(v.X) + "." + v.Sel.Name + case *ast.IndexExpr: + xCode := exprCode(v.X) + idxCode := exprCode(v.Index) + return xCode + "[" + idxCode + "]" + case *ast.SliceExpr: + return exprCode(v.X) + "[...]" + case *ast.BinaryExpr: + return exprCode(v.X) + " " + v.Op.String() + " " + exprCode(v.Y) + case *ast.UnaryExpr: + return v.Op.String() + exprCode(v.X) + case *ast.TypeAssertExpr: + xCode := exprCode(v.X) + if v.Type == nil { + return xCode + ".(type)" + } + return xCode + ".(" + types.ExprString(v.Type) + ")" + case *ast.ParenExpr: + return "(" + exprCode(v.X) + ")" + case *ast.StarExpr: + return "*" + exprCode(v.X) + case *ast.CompositeLit: + return types.ExprString(v.Type) + "{...}" + } + return "expression" +} + +// isAddressable checks if an AST expression represents an addressable value. +// Addressable expressions include: identifiers (variables), index expressions +// on arrays/slices, selector expressions (field access), and pointer indirections. +func isAddressable(val ast.Expr) bool { + switch v := val.(type) { + case *ast.Ident: + return true + case *ast.IndexExpr: + // Array/slice/map indexing is addressable (for slice/array) + // Note: map indexing is not truly addressable but Go reports it as "variable" + return true + case *ast.SelectorExpr: + // Field access is addressable only if the receiver is addressable + return isAddressable(v.X) + case *ast.StarExpr: + // Pointer indirection is always addressable + return true + case *ast.ParenExpr: + return isAddressable(v.X) + } + return false +} + +// exprDescription returns a description of an expression for error messages. +// The format matches Go compiler's error messages. +// +// The description format follows Go compiler's operandModeString: +// - "map index expression of type X" for map index +// - "comma, ok expression of type X" for type assertions +// - "constant" or "constant V of type X" for constants +// - "variable of type X" for addressable expressions +// - "value of type X" for other values +func exprDescription(e *internal.Elem, code string) string { + typ := e.Type + if typ == nil { + return "expression" + } + // Check operand mode flags for special expressions + if e.Flags == internal.ElemFlagMapIndex { + return fmt.Sprintf("map index expression of type %v", typ) + } + if e.Flags == internal.ElemFlagCommaOk { + return fmt.Sprintf("comma, ok expression of type %v", typ) + } + if e.CVal != nil { + // Constant: format depends on whether it's untyped or typed + if basic, ok := typ.(*types.Basic); ok && (basic.Info()&types.IsUntyped) != 0 { + // Untyped constant: Go compiler shows value only if code differs from value + // e.g., "42" -> "untyped int constant" (no value) + // "1 + 2" -> "untyped int constant 3" (shows computed value) + // "y" (named const) -> "untyped int constant 20" (shows value) + if isLiteralCode(code, e.CVal, basic) { + return fmt.Sprintf("%v constant", typ) + } + return fmt.Sprintf("%v constant %v", typ, e.CVal) + } + // Typed constant: "constant V of type X" + return fmt.Sprintf("constant %v of type %v", e.CVal, typ) + } + // Check if it's addressable (variable-like), but not map index + if isAddressable(e.Val) && e.Flags != internal.ElemFlagMapIndex { + return fmt.Sprintf("variable of type %v", typ) + } + return fmt.Sprintf("value of type %v", typ) +} + +// isLiteralCode checks if the code string is a direct literal representation +// of the constant value. If so, Go compiler doesn't show the value in error messages. +func isLiteralCode(code string, cval constant.Value, typ *types.Basic) bool { + if code == "" { + return false + } + // For BasicLit, the code is the literal itself + // Check if code looks like a literal (not an expression or identifier) + switch typ.Kind() { + case types.UntypedInt: + // Check if code is a number literal (not an expression like "1 + 2") + return code == cval.String() + case types.UntypedFloat: + return code == cval.String() + case types.UntypedString: + return code == cval.String() + case types.UntypedBool: + return code == "true" || code == "false" + case types.UntypedRune: + // Rune literals like 'a' - Go always shows the numeric value + return false + case types.UntypedComplex: + return false + } + return false +} + // EndStmt func func (p *CodeBuilder) EndStmt() *CodeBuilder { n := p.stk.Len() - p.current.base @@ -2831,7 +3016,22 @@ func (p *CodeBuilder) EndStmt() *CodeBuilder { if n != 1 { panic("syntax error: unexpected newline, expecting := or = or comma") } - if e := p.stk.Pop(); p.noSkipConst || e.CVal == nil { // skip constant + e := p.stk.Pop() + // Check if the expression is valid as a statement (even for constants) + if !isValidStmtExpr(e) { + code, pos, end := p.loadExpr(e.Src) + if code == "" { + code = exprCode(e.Val) + } + // If position is not available from Src, try to get it from Val + if pos == token.NoPos && e.Val != nil { + pos = e.Val.Pos() + end = e.Val.End() + } + p.handleCodeErrorf(pos, end, "%s (%s) is not used", code, exprDescription(e, code)) + } + // Skip emitting pure constant expressions unless noSkipConst is set + if p.noSkipConst || e.CVal == nil { p.emitStmt(&ast.ExprStmt{X: e.Val}) } } diff --git a/codebuild_test.go b/codebuild_test.go index b0475f44..2106f929 100644 --- a/codebuild_test.go +++ b/codebuild_test.go @@ -14,6 +14,8 @@ package gogen import ( + "go/ast" + "go/constant" "go/token" "go/types" "testing" @@ -86,3 +88,291 @@ func TestFindMember(t *testing.T) { t.Fatalf("expected MemberMethod (1), got %v", kind) } } + +func TestIsValidStmtExprWithParenExpr(t *testing.T) { + // Test that parenthesized call expressions are valid: (foo()) + callExpr := &ast.CallExpr{Fun: ast.NewIdent("foo")} + parenCall := &ast.ParenExpr{X: callExpr} + elem := &Element{Val: parenCall} + if !isValidStmtExpr(elem) { + t.Error("(foo()) should be a valid statement expression") + } + + // Test nested parentheses: ((foo())) + nestedParen := &ast.ParenExpr{X: parenCall} + elem2 := &Element{Val: nestedParen} + if !isValidStmtExpr(elem2) { + t.Error("((foo())) should be a valid statement expression") + } + + // Test that parenthesized receive is valid: (<-ch) + recvExpr := &ast.UnaryExpr{Op: token.ARROW, X: ast.NewIdent("ch")} + parenRecv := &ast.ParenExpr{X: recvExpr} + elem3 := &Element{Val: parenRecv} + if !isValidStmtExpr(elem3) { + t.Error("(<-ch) should be a valid statement expression") + } + + // Test that parenthesized non-call/recv is still invalid: (a + b) + binExpr := &ast.BinaryExpr{X: ast.NewIdent("a"), Op: token.ADD, Y: ast.NewIdent("b")} + parenBin := &ast.ParenExpr{X: binExpr} + elem4 := &Element{Val: parenBin} + if isValidStmtExpr(elem4) { + t.Error("(a + b) should NOT be a valid statement expression") + } +} + +func TestExprCode(t *testing.T) { + tests := []struct { + name string + expr ast.Expr + expected string + }{ + { + name: "Ident", + expr: ast.NewIdent("foo"), + expected: "foo", + }, + { + name: "BasicLitInt", + expr: &ast.BasicLit{Kind: token.INT, Value: "42"}, + expected: "42", + }, + { + name: "CallExprIdent", + expr: &ast.CallExpr{Fun: ast.NewIdent("foo")}, + expected: "foo(...)", + }, + { + name: "CallExprSelector", + expr: &ast.CallExpr{ + Fun: &ast.SelectorExpr{X: ast.NewIdent("pkg"), Sel: ast.NewIdent("Func")}, + }, + expected: "pkg.Func(...)", + }, + { + name: "CallExprOther", + expr: &ast.CallExpr{ + Fun: &ast.IndexExpr{X: ast.NewIdent("funcs"), Index: ast.NewIdent("i")}, + }, + expected: "funcs[i](...)", + }, + { + name: "CallExprNestedSelector", + expr: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: ast.NewIdent("a"), + Sel: ast.NewIdent("b"), + }, + Sel: ast.NewIdent("c"), + }, + }, + expected: "a.b.c(...)", + }, + { + name: "SelectorExprIdent", + expr: &ast.SelectorExpr{X: ast.NewIdent("obj"), Sel: ast.NewIdent("Field")}, + expected: "obj.Field", + }, + { + name: "SelectorExprNested", + expr: &ast.SelectorExpr{ + X: &ast.CallExpr{Fun: ast.NewIdent("getObj")}, + Sel: ast.NewIdent("Field"), + }, + expected: "getObj(...).Field", + }, + { + name: "SelectorExprDeepNested", + expr: &ast.SelectorExpr{ + X: &ast.SelectorExpr{ + X: ast.NewIdent("a"), + Sel: ast.NewIdent("b"), + }, + Sel: ast.NewIdent("c"), + }, + expected: "a.b.c", + }, + { + name: "IndexExpr", + expr: &ast.IndexExpr{X: ast.NewIdent("arr"), Index: ast.NewIdent("i")}, + expected: "arr[i]", + }, + { + name: "SliceExprIdent", + expr: &ast.SliceExpr{X: ast.NewIdent("arr")}, + expected: "arr[...]", + }, + { + name: "SliceExprOther", + expr: &ast.SliceExpr{X: &ast.CallExpr{Fun: ast.NewIdent("getArr")}}, + expected: "getArr(...)[...]", + }, + { + name: "BinaryExpr", + expr: &ast.BinaryExpr{X: ast.NewIdent("a"), Op: token.ADD, Y: ast.NewIdent("b")}, + expected: "a + b", + }, + { + name: "UnaryExpr", + expr: &ast.UnaryExpr{Op: token.SUB, X: ast.NewIdent("x")}, + expected: "-x", + }, + { + name: "TypeAssertExprWithType", + expr: &ast.TypeAssertExpr{X: ast.NewIdent("x"), Type: ast.NewIdent("int")}, + expected: "x.(int)", + }, + { + name: "TypeAssertExprNilType", + expr: &ast.TypeAssertExpr{X: ast.NewIdent("x"), Type: nil}, + expected: "x.(type)", + }, + { + name: "ParenExpr", + expr: &ast.ParenExpr{X: ast.NewIdent("x")}, + expected: "(x)", + }, + { + name: "StarExpr", + expr: &ast.StarExpr{X: ast.NewIdent("ptr")}, + expected: "*ptr", + }, + { + name: "CompositeLit", + expr: &ast.CompositeLit{Type: ast.NewIdent("T")}, + expected: "T{...}", + }, + { + name: "Unknown", + expr: &ast.FuncLit{}, + expected: "expression", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := exprCode(tt.expr) + if got != tt.expected { + t.Errorf("exprCode() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestIsAddressableParenExpr(t *testing.T) { + // Test that parenthesized identifiers are addressable: (x) + parenIdent := &ast.ParenExpr{X: ast.NewIdent("x")} + if !isAddressable(parenIdent) { + t.Error("(x) should be addressable") + } + + // Test nested parentheses: ((x)) + nestedParen := &ast.ParenExpr{X: parenIdent} + if !isAddressable(nestedParen) { + t.Error("((x)) should be addressable") + } + + // Test that parenthesized non-addressable is not addressable: (a + b) + binExpr := &ast.BinaryExpr{X: ast.NewIdent("a"), Op: token.ADD, Y: ast.NewIdent("b")} + parenBin := &ast.ParenExpr{X: binExpr} + if isAddressable(parenBin) { + t.Error("(a + b) should NOT be addressable") + } +} + +func TestExprDescriptionNilType(t *testing.T) { + elem := &Element{Val: ast.NewIdent("x"), Type: nil} + got := exprDescription(elem, "x") + if got != "expression" { + t.Errorf("exprDescription() with nil type = %q, want %q", got, "expression") + } +} + +func TestIsLiteralCode(t *testing.T) { + tests := []struct { + name string + code string + cval constant.Value + typ *types.Basic + expected bool + }{ + { + name: "EmptyCode", + code: "", + cval: constant.MakeInt64(42), + typ: types.Typ[types.UntypedInt], + expected: false, + }, + { + name: "UntypedRune", + code: "'a'", + cval: constant.MakeInt64(97), + typ: types.Typ[types.UntypedRune], + expected: false, + }, + { + name: "TypedInt", + code: "42", + cval: constant.MakeInt64(42), + typ: types.Typ[types.Int], + expected: false, + }, + { + name: "BigInt", + code: "99999999999999999999999999999999999999", + cval: constant.MakeFromLiteral("99999999999999999999999999999999999999", token.INT, 0), + typ: types.Typ[types.UntypedInt], + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isLiteralCode(tt.code, tt.cval, tt.typ) + if got != tt.expected { + t.Errorf("isLiteralCode() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestExprDescriptionTypedConstant(t *testing.T) { + // Test typed constant: "constant V of type X" + elem := &Element{ + Val: ast.NewIdent("c"), + Type: types.Typ[types.Int], + CVal: constant.MakeInt64(42), + } + got := exprDescription(elem, "c") + expected := "constant 42 of type int" + if got != expected { + t.Errorf("exprDescription() = %q, want %q", got, expected) + } +} + +func TestUnusedExprWithoutSource(t *testing.T) { + // Test that unused expression error works without source position + // This triggers the exprCode fallback path + defer func() { + if e := recover(); e != nil { + err, ok := e.(*CodeError) + if !ok { + t.Fatalf("expected CodeError, got %T: %v", e, e) + } + // Verify the error message contains expected content + errMsg := err.Error() + if errMsg == "" { + t.Error("expected non-empty error message") + } + } else { + t.Fatal("expected error for unused expression") + } + }() + pkg := NewPackage("", "main", nil) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Int], "a"). + VarVal("a").EndStmt(). // No source() parameter + End() +} diff --git a/error_msg_test.go b/error_msg_test.go index 1a816d3f..b25a5f56 100644 --- a/error_msg_test.go +++ b/error_msg_test.go @@ -1580,3 +1580,274 @@ func TestErrorLit(t *testing.T) { End() }) } + +func TestErrUnusedExpr(t *testing.T) { + // Variable as statement - should error + codeErrorTest(t, `./foo.gop:2:2: a (variable of type int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Int], "a"). + VarVal("a", source("a", 2, 2)).EndStmt(). + End() + }) + + // Binary operation as statement - should error + codeErrorTest(t, `./foo.gop:2:2: a + 1 (value of type int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Int], "a"). + VarVal("a").Val(1).BinaryOp(token.ADD, source("a + 1", 2, 2)).EndStmt(). + End() + }) + + // Unary operation (not receive) as statement - should error + codeErrorTest(t, `./foo.gop:2:2: -a (value of type int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Int], "a"). + VarVal("a").UnaryOp(token.SUB, false, source("-a", 2, 2)).EndStmt(). + End() + }) + + // Index expression as statement - should error + codeErrorTest(t, `./foo.gop:2:2: arr[0] (variable of type int) is not used`, + func(pkg *gogen.Package) { + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tySlice, "arr"). + VarVal("arr").Val(0).Index(1, false, source("arr[0]", 2, 2)).EndStmt(). + End() + }) + + // Type conversion as statement - should error + codeErrorTest(t, `./foo.gop:2:2: int(x) (value of type int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Float64], "x"). + Typ(types.Typ[types.Int]).VarVal("x").CallWith(1, 0, source("int(x)", 2, 2)).EndStmt(). + End() + }) + + // len() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: len(s) (value of type int) is not used`, + func(pkg *gogen.Package) { + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tySlice, "s"). + Val(pkg.Builtin().Ref("len")).VarVal("s").CallWith(1, 0, source("len(s)", 2, 2)).EndStmt(). + End() + }) + + // cap() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: cap(s) (value of type int) is not used`, + func(pkg *gogen.Package) { + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tySlice, "s"). + Val(pkg.Builtin().Ref("cap")).VarVal("s").CallWith(1, 0, source("cap(s)", 2, 2)).EndStmt(). + End() + }) + + // new() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: new(int) (value of type *int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("new")).Typ(types.Typ[types.Int]).CallWith(1, 0, source("new(int)", 2, 2)).EndStmt(). + End() + }) + + // make() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: make([]int, 0) (value of type []int) is not used`, + func(pkg *gogen.Package) { + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("make")).Typ(tySlice).Val(0).CallWith(2, 0, source("make([]int, 0)", 2, 2)).EndStmt(). + End() + }) + + // append() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: append(...) (value of type []int) is not used`, + func(pkg *gogen.Package) { + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tySlice, "s"). + Val(pkg.Builtin().Ref("append")).VarVal("s").Val(1).CallWith(2, 0, source("append(...)", 2, 2)).EndStmt(). + End() + }) + + // complex() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: complex(...) (untyped complex constant (1 + 2i)) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("complex")).Val(1.0).Val(2.0).CallWith(2, 0, source("complex(...)", 2, 2)).EndStmt(). + End() + }) + + // real() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: real(c) (value of type float64) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Complex128], "c"). + Val(pkg.Builtin().Ref("real")).VarVal("c").CallWith(1, 0, source("real(c)", 2, 2)).EndStmt(). + End() + }) + + // imag() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: imag(c) (value of type float64) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Complex128], "c"). + Val(pkg.Builtin().Ref("imag")).VarVal("c").CallWith(1, 0, source("imag(c)", 2, 2)).EndStmt(). + End() + }) + + // Map index expression as statement - should error with "map index expression" + codeErrorTest(t, `./foo.gop:2:2: m[0] (map index expression of type int) is not used`, + func(pkg *gogen.Package) { + tyMap := types.NewMap(types.Typ[types.Int], types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyMap, "m"). + VarVal("m").Val(0).Index(1, false, source("m[0]", 2, 2)).EndStmt(). + End() + }) + + // Type assertion as statement - should error with "comma, ok expression" + codeErrorTest(t, `./foo.gop:2:2: x.(int) (comma, ok expression of type int) is not used`, + func(pkg *gogen.Package) { + tyInterface := types.NewInterfaceType(nil, nil).Complete() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyInterface, "x"). + VarVal("x").TypeAssert(types.Typ[types.Int], false, source("x.(int)", 2, 2)).EndStmt(). + End() + }) + + // min() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: min(...) (value of type int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("min")).Val(1).Val(2).CallWith(2, 0, source("min(...)", 2, 2)).EndStmt(). + End() + }) + + // max() as statement - should error + codeErrorTest(t, `./foo.gop:2:2: max(...) (value of type int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("max")).Val(1).Val(2).CallWith(2, 0, source("max(...)", 2, 2)).EndStmt(). + End() + }) + + // Constant literal as statement - should error (no value shown for literals) + codeErrorTest(t, `./foo.gop:2:2: 42 (untyped int constant) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(42, source("42", 2, 2)).EndStmt(). + End() + }) + + // String literal as statement - should error (no value shown for literals) + codeErrorTest(t, `./foo.gop:2:2: "hello" (untyped string constant) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val("hello", source(`"hello"`, 2, 2)).EndStmt(). + End() + }) + + // Field access as statement - should error + codeErrorTest(t, `./foo.gop:2:2: t.F (variable of type int) is not used`, + func(pkg *gogen.Package) { + fields := []*types.Var{types.NewField(token.NoPos, pkg.Types, "F", types.Typ[types.Int], false)} + tyStruct := types.NewStruct(fields, nil) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyStruct, "t"). + VarVal("t").MemberVal("F", source("t.F", 2, 2)).EndStmt(). + End() + }) + + // Nested field access (a.b.c) as statement - should error + codeErrorTest(t, `./foo.gop:2:2: a.B.C (variable of type int) is not used`, + func(pkg *gogen.Package) { + // type B struct { C int } + fieldsB := []*types.Var{types.NewField(token.NoPos, pkg.Types, "C", types.Typ[types.Int], false)} + tyB := types.NewStruct(fieldsB, nil) + // type A struct { B B } + fieldsA := []*types.Var{types.NewField(token.NoPos, pkg.Types, "B", tyB, false)} + tyA := types.NewStruct(fieldsA, nil) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyA, "a"). + VarVal("a").MemberVal("B").MemberVal("C", source("a.B.C", 2, 2)).EndStmt(). + End() + }) + + // Function result nested field access (getA().B.C) as statement - should error + codeErrorTest(t, `./foo.gop:2:2: getA().B.C (value of type int) is not used`, + func(pkg *gogen.Package) { + // type B struct { C int } + fieldsB := []*types.Var{types.NewField(token.NoPos, pkg.Types, "C", types.Typ[types.Int], false)} + tyB := types.NewStruct(fieldsB, nil) + // type A struct { B B } + fieldsA := []*types.Var{types.NewField(token.NoPos, pkg.Types, "B", tyB, false)} + tyA := types.NewStruct(fieldsA, nil) + // func getA() A { return A{} } + pkg.NewFunc(nil, "getA", nil, gogen.NewTuple(types.NewVar(token.NoPos, pkg.Types, "", tyA)), false).BodyStart(pkg). + StructLit(tyA, 0, false).Return(1). + End() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Types.Scope().Lookup("getA")).Call(0).MemberVal("B").MemberVal("C", source("getA().B.C", 2, 2)).EndStmt(). + End() + }) + + // Pointer dereference as statement - should error + // Note: ElemRef returns a refType internally, so we use Star() instead + codeErrorTest(t, `./foo.gop:2:2: *p (variable of type int) is not used`, + func(pkg *gogen.Package) { + tyPtr := types.NewPointer(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyPtr, "p"). + VarVal("p").Star(source("*p", 2, 2)).EndStmt(). + End() + }) + + // Address-of as statement - should error + codeErrorTest(t, `./foo.gop:2:2: &x (value of type *int) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(types.Typ[types.Int], "x"). + VarVal("x").UnaryOp(token.AND, false, source("&x", 2, 2)).EndStmt(). + End() + }) + + // Slice expression as statement - should error + codeErrorTest(t, `./foo.gop:2:2: arr[...] (value of type []int) is not used`, + func(pkg *gogen.Package) { + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tySlice, "arr"). + VarVal("arr").Val(1).Val(2).Slice(false, source("arr[...]", 2, 2)).EndStmt(). + End() + }) + + // Boolean literal as statement - should error (no value shown for literals) + codeErrorTest(t, `./foo.gop:2:2: true (untyped bool constant) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(true, source("true", 2, 2)).EndStmt(). + End() + }) + + // Float literal as statement - should error (no value shown for literals) + codeErrorTest(t, `./foo.gop:2:2: 3.14 (untyped float constant) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(3.14, source("3.14", 2, 2)).EndStmt(). + End() + }) + + // Binary expression as computed value - should show result + codeErrorTest(t, `./foo.gop:2:2: 1 + 2 (untyped int constant 3) is not used`, + func(pkg *gogen.Package) { + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(1).Val(2).BinaryOp(token.ADD, source("1 + 2", 2, 2)).EndStmt(). + End() + }) +} diff --git a/internal/stack.go b/internal/stack.go index 26b32b99..a32df422 100644 --- a/internal/stack.go +++ b/internal/stack.go @@ -23,11 +23,32 @@ import ( const defaultStkSize = 64 +// ElemFlags represents flags for stack elements. +// These flags describe the operand mode for error messages, +// similar to Go compiler's operandMode. +type ElemFlags uint32 + +const ( + // ElemFlagDefault is the default flag (no special meaning). + ElemFlagDefault ElemFlags = iota + + // ElemFlagTypeCast indicates a type conversion expression. + ElemFlagTypeCast + + // ElemFlagMapIndex indicates a map index expression. + // Map index expressions have special error message format. + ElemFlagMapIndex + + // ElemFlagCommaOk indicates a comma-ok expression (type assertion, channel receive). + ElemFlagCommaOk +) + type Elem struct { - Val ast.Expr - Type types.Type - CVal constant.Value - Src ast.Node + Val ast.Expr + Type types.Type + CVal constant.Value + Src ast.Node + Flags ElemFlags } // A Stack represents a FILO container. diff --git a/package_test.go b/package_test.go index 48a46934..01fc0bfa 100644 --- a/package_test.go +++ b/package_test.go @@ -3377,14 +3377,14 @@ func TestBinaryOpSHL(t *testing.T) { pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). NewVar(types.Typ[types.Int32], "a"). NewVar(types.Typ[types.Int], "b"). - VarVal("b").Val(1).VarVal("a").BinaryOp(token.SHL).BinaryOp(token.AND).EndStmt(). + VarRef(nil).VarVal("b").Val(1).VarVal("a").BinaryOp(token.SHL).BinaryOp(token.AND).Assign(1).EndStmt(). End() domTest(t, pkg, `package main func main() { var a int32 var b int - b & (1 << a) + _ = b & (1 << a) } `) } @@ -4356,4 +4356,185 @@ func main() { `) } +func TestValidStmtExpr(t *testing.T) { + // Function call as statement - should pass + pkg := newMainPackage() + fmt := pkg.Import("fmt") + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(fmt.Ref("Println")).Val("hello").Call(1).EndStmt(). + End() + domTest(t, pkg, `package main + +import "fmt" + +func main() { + fmt.Println("hello") +} +`) +} + +func TestValidStmtExprRecv(t *testing.T) { + // Receive operation as statement - should pass + pkg := newMainPackage() + tyChan := types.NewChan(types.RecvOnly, types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyChan, "ch"). + VarVal("ch").UnaryOp(token.ARROW).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + var ch <-chan int + <-ch +} +`) +} + +func TestValidStmtExprClose(t *testing.T) { + // close() as statement - should pass (no return value) + pkg := newMainPackage() + tyChan := types.NewChan(types.SendOnly, types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyChan, "ch"). + Val(pkg.Builtin().Ref("close")).VarVal("ch").Call(1).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + var ch chan<- int + close(ch) +} +`) +} + +func TestValidStmtExprDelete(t *testing.T) { + // delete() as statement - should pass (no return value) + pkg := newMainPackage() + tyMap := types.NewMap(types.Typ[types.String], types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyMap, "m"). + Val(pkg.Builtin().Ref("delete")).VarVal("m").Val("key").Call(2).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + var m map[string]int + delete(m, "key") +} +`) +} + +func TestValidStmtExprCopy(t *testing.T) { + // copy() as statement - should pass (return value can be ignored) + pkg := newMainPackage() + tySlice := types.NewSlice(types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tySlice, "dst"). + NewVar(tySlice, "src"). + Val(pkg.Builtin().Ref("copy")).VarVal("dst").VarVal("src").Call(2).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + var dst []int + var src []int + copy(dst, src) +} +`) +} + +func TestValidStmtExprRecover(t *testing.T) { + // recover() as statement - should pass (return value can be ignored) + pkg := newMainPackage() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("recover")).Call(0).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + recover() +} +`) +} + +func TestValidStmtExprPanic(t *testing.T) { + // panic() as statement - should pass (no return) + pkg := newMainPackage() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("panic")).Val("error").Call(1).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + panic("error") +} +`) +} + +func TestValidStmtExprPrint(t *testing.T) { + // print() as statement - should pass + pkg := newMainPackage() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("print")).Val("hello").Call(1).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + print("hello") +} +`) +} + +func TestValidStmtExprPrintln(t *testing.T) { + // println() as statement - should pass + pkg := newMainPackage() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Builtin().Ref("println")).Val("hello").Call(1).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + println("hello") +} +`) +} + +func TestValidStmtExprClear(t *testing.T) { + // clear() as statement - should pass (Go 1.21+) + pkg := newMainPackage() + tyMap := types.NewMap(types.Typ[types.Int], types.Typ[types.Int]) + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + NewVar(tyMap, "m"). + Val(pkg.Builtin().Ref("clear")).VarVal("m").Call(1).EndStmt(). + End() + domTest(t, pkg, `package main + +func main() { + var m map[int]int + clear(m) +} +`) +} + +func TestValidStmtExprUserFunc(t *testing.T) { + // User-defined function with return value as statement - should pass + pkg := newMainPackage() + retType := types.NewTuple(types.NewParam(token.NoPos, pkg.Types, "", types.Typ[types.Int])) + pkg.NewFunc(nil, "foo", nil, retType, false).BodyStart(pkg). + Val(1).Return(1). + End() + pkg.NewFunc(nil, "main", nil, nil, false).BodyStart(pkg). + Val(pkg.Ref("foo")).Call(0).EndStmt(). + End() + domTest(t, pkg, `package main + +func foo() int { + return 1 +} +func main() { + foo() +} +`) +} + // ----------------------------------------------------------------------------