Skip to content

Commit 2a21081

Browse files
authored
fix: mark struct value-type variables for accurate type assertions (#98)
This commit inverts the struct marking strategy from marking pointers to marking values for better alignment with Go type assertion semantics. Previously, we marked struct pointers with markAsStructPointer; now we mark struct values with markAsStructValue. - analysis.go: Added visitCompositeLit to detect &var in composites for VarRef marking. Updated visitTypeAssertExpr to handle pointer expr types and track struct method implementations for interfaces. - assignment.go: Added $.markAsStructValue to struct value cloning. Handled pointer-to-pointer assignments with conditional .value based on varref status. Added special handling for pointer variable assignment to interfaces without .value for varref'd pointers. - compiler.go: Introduced insideAddressOf flag to track when inside & operator. - composite-lit.go: Conditionally apply $.markAsStructValue to named struct values only if not inside &. - expr-selector.go: Added $.markAsStructValue to cloned receivers in method binding. - expr.go: For pointer comparisons, add .value only if operands are varref'd. For unary &, set insideAddressOf flag during operand evaluation. - spec-struct.go: Export struct types only if not defined inside functions. - spec-value.go: Use WriteValueExpr for unary & initializers. Added $.markAsStructValue to struct clones in initializers. Introduced writeInitializerForInterface to handle pointer to interface assignments without .value. - spec.go: Added $.markAsStructValue to struct field initializers and clones in constructors. - stmt.go: Added destructuring support for type assertions in shadowed assignments. - type.ts: Inverted marking to use STRUCT_VALUE_MARKER on values. Updated matchesStructType to require value marker for value assertions. Updated matchesPointerType to match unmarked structs (pointers) or VarRefs for pointer assertions. Added special handling in typeAssert to return inner .value for pointer asserts on VarRefs. These changes ensure correct type assertion behavior distinguishing struct values from pointers, proper VarRef handling in composites, and consistent pointer semantics. (Commit message written by Grok 4) Fixes: #92 Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent 85fbd8e commit 2a21081

File tree

91 files changed

+1454
-702
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+1454
-702
lines changed

compiler/analysis.go

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,12 @@ func (v *analysisVisitor) Visit(node ast.Node) ast.Visitor {
589589

590590
case *ast.TypeAssertExpr:
591591
return v.visitTypeAssertExpr(n)
592+
593+
case *ast.CompositeLit:
594+
// Traverse into composite literal elements to detect &variable expressions
595+
// This is important for cases like: arr := []interface{}{value1, &value2}
596+
// where value2 needs to be marked as NeedsVarRef due to the &value2 usage
597+
return v.visitCompositeLit(n)
592598
}
593599

594600
// For all other nodes, continue traversal
@@ -869,10 +875,89 @@ func (v *analysisVisitor) visitIfStmt(n *ast.IfStmt) ast.Visitor {
869875
return v
870876
}
871877

872-
// visitTypeAssertExpr handles type assertion expression analysis
873-
func (v *analysisVisitor) visitTypeAssertExpr(n *ast.TypeAssertExpr) ast.Visitor {
874-
// Track interface implementations when we see type assertions
875-
v.trackTypeAssertion(n)
878+
// visitTypeAssertExpr handles type assertion analysis for interface method implementations
879+
func (v *analysisVisitor) visitTypeAssertExpr(typeAssert *ast.TypeAssertExpr) ast.Visitor {
880+
// Get the type being asserted to
881+
assertedType := v.pkg.TypesInfo.TypeOf(typeAssert.Type)
882+
if assertedType == nil {
883+
return v
884+
}
885+
886+
// Check if the asserted type is an interface
887+
interfaceType, isInterface := assertedType.Underlying().(*types.Interface)
888+
if !isInterface {
889+
return v
890+
}
891+
892+
// Get the type of the expression being asserted
893+
exprType := v.pkg.TypesInfo.TypeOf(typeAssert.X)
894+
if exprType == nil {
895+
return v
896+
}
897+
898+
// Handle pointer types by getting the element type
899+
if ptrType, isPtr := exprType.(*types.Pointer); isPtr {
900+
exprType = ptrType.Elem()
901+
}
902+
903+
// Check if the expression type is a named struct type
904+
namedType, isNamed := exprType.(*types.Named)
905+
if !isNamed {
906+
return v
907+
}
908+
909+
// For each method in the interface, check if the struct implements it
910+
for i := 0; i < interfaceType.NumExplicitMethods(); i++ {
911+
interfaceMethod := interfaceType.ExplicitMethod(i)
912+
913+
// Find the corresponding method in the struct type
914+
structMethod := v.findStructMethod(namedType, interfaceMethod.Name())
915+
if structMethod != nil {
916+
// Determine if this struct method is async using unified system
917+
isAsync := false
918+
if obj := structMethod; obj != nil {
919+
isAsync = v.analysis.IsAsyncFunc(obj)
920+
}
921+
922+
// Track this interface implementation
923+
v.analysis.trackInterfaceImplementation(interfaceType, namedType, structMethod, isAsync)
924+
}
925+
}
926+
return v
927+
}
928+
929+
// visitCompositeLit analyzes composite literals for address-of expressions
930+
// This is important for detecting cases like: arr := []interface{}{value1, &value2}
931+
// where value2 needs to be marked as NeedsVarRef due to the &value2 usage
932+
func (v *analysisVisitor) visitCompositeLit(compLit *ast.CompositeLit) ast.Visitor {
933+
// Analyze each element of the composite literal
934+
for _, elt := range compLit.Elts {
935+
// Handle both direct elements and key-value pairs
936+
var expr ast.Expr
937+
if kv, ok := elt.(*ast.KeyValueExpr); ok {
938+
// For key-value pairs, analyze the value expression
939+
expr = kv.Value
940+
} else {
941+
// For direct elements, analyze the element expression
942+
expr = elt
943+
}
944+
945+
// Check if this element is an address-of expression
946+
if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.AND {
947+
// Found &something in the composite literal
948+
if ident, ok := unaryExpr.X.(*ast.Ident); ok {
949+
// Found &variable - mark the variable as needing VarRef
950+
if obj := v.pkg.TypesInfo.ObjectOf(ident); obj != nil {
951+
// Record that this variable has its address taken
952+
usageInfo := v.getOrCreateUsageInfo(obj)
953+
usageInfo.Destinations = append(usageInfo.Destinations, AssignmentInfo{
954+
Object: nil, // No specific destination object for composite literals
955+
Type: AddressOfAssignment,
956+
})
957+
}
958+
}
959+
}
960+
}
876961
return v
877962
}
878963

compiler/assignment.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
5656

5757
// Handle the RHS expression (potentially adding .clone() for structs)
5858
if shouldApplyClone(c.pkg, rhs[0]) {
59+
// When cloning for value assignment, mark the result as struct value
60+
c.tsw.WriteLiterally("$.markAsStructValue(")
5961
if err := c.WriteValueExpr(rhs[0]); err != nil {
6062
return err
6163
}
62-
c.tsw.WriteLiterally(".clone()")
64+
c.tsw.WriteLiterally(".clone())")
6365
} else {
6466
if err := c.WriteValueExpr(rhs[0]); err != nil {
6567
return err
@@ -338,8 +340,37 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
338340
}
339341
}
340342

343+
// Check for pointer-to-pointer assignment
344+
if rhsIsIdent && rhsObj != nil && len(lhs) == 1 {
345+
lhsType := c.pkg.TypesInfo.TypeOf(lhs[0])
346+
rhsType := rhsObj.Type()
347+
348+
if lhsType != nil && rhsType != nil {
349+
// Check if both LHS and RHS are pointer types
350+
if _, lhsIsPtr := lhsType.(*types.Pointer); lhsIsPtr {
351+
if _, rhsIsPtr := rhsType.(*types.Pointer); rhsIsPtr {
352+
// This is pointer-to-pointer assignment
353+
// The key question: is the RHS variable itself varref'd?
354+
// - If RHS is varref'd (like pp1), use .value to get the actual pointer
355+
// - If RHS is not varref'd (like p1), use the variable directly
356+
357+
if c.analysis.NeedsVarRef(rhsObj) {
358+
// RHS variable is varref'd, so we need its .value to get the actual pointer
359+
c.WriteIdent(rhsIdent, true) // Add .value access
360+
} else {
361+
// RHS variable is not varref'd, so it directly holds the pointer
362+
c.WriteIdent(rhsIdent, false) // No .value access
363+
}
364+
continue
365+
}
366+
}
367+
}
368+
}
369+
341370
// Handle different cases for struct cloning
342371
if shouldApplyClone(c.pkg, r) {
372+
// When cloning for value assignment, mark the result as struct value
373+
c.tsw.WriteLiterally("$.markAsStructValue(")
343374
// For other expressions, we need to handle variable referenced access differently
344375
if _, isIdent := r.(*ast.Ident); isIdent {
345376
// For identifiers, WriteValueExpr already adds .value if needed
@@ -357,8 +388,39 @@ func (c *GoToTSCompiler) writeAssignmentCore(lhs, rhs []ast.Expr, tok token.Toke
357388
}
358389
}
359390

360-
c.tsw.WriteLiterally(".clone()") // Always add clone for struct values
391+
c.tsw.WriteLiterally(".clone())") // Always add clone for struct values
361392
} else {
393+
// Check if this is a pointer variable assignment to an interface type
394+
if rhsIsIdent && rhsObj != nil {
395+
// Check if LHS is interface type and RHS is a pointer variable
396+
if len(lhs) == 1 {
397+
lhsType := c.pkg.TypesInfo.TypeOf(lhs[0])
398+
rhsType := rhsObj.Type()
399+
400+
if lhsType != nil && rhsType != nil {
401+
// Check if LHS is interface and RHS is pointer
402+
if _, isInterface := lhsType.Underlying().(*types.Interface); isInterface {
403+
if ptrType, isPtr := rhsType.(*types.Pointer); isPtr {
404+
// This is pointer-to-interface assignment
405+
// For pointer variables that point to varrefed values, write without .value
406+
// We want to pass the VarRef object itself to the interface, not its .value
407+
if c.analysis.NeedsVarRefAccess(rhsObj) {
408+
// Write the pointer variable without .value access
409+
c.WriteIdent(rhsIdent, false)
410+
continue
411+
}
412+
413+
// Check if this is a struct pointer for the element type
414+
if _, isStruct := ptrType.Elem().Underlying().(*types.Struct); isStruct {
415+
// Struct pointer to interface - might need special handling
416+
// Continue to normal WriteValueExpr handling
417+
}
418+
}
419+
}
420+
}
421+
}
422+
}
423+
362424
// Non-struct case: write RHS normally
363425
if err := c.WriteValueExpr(r); err != nil { // RHS is a non-struct value
364426
return err

compiler/compiler.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,9 @@ type GoToTSCompiler struct {
704704
pkg *packages.Package
705705

706706
analysis *Analysis
707+
708+
// Context flags
709+
insideAddressOf bool // true when processing operand of & operator
707710
}
708711

709712
// It initializes the compiler with a `TSCodeWriter` for output,

compiler/composite-lit.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ func (c *GoToTSCompiler) WriteCompositeLit(exp *ast.CompositeLit) error {
212212
var structType *types.Struct
213213
isStructLiteral := false
214214
isAnonymousStruct := false
215+
needsValueMarkerClose := false // Track if we need to close $.markAsStructValue()
215216

216217
if namedType, ok := litType.(*types.Named); ok {
217218
if underlyingStruct, ok := namedType.Underlying().(*types.Struct); ok {
@@ -224,8 +225,14 @@ func (c *GoToTSCompiler) WriteCompositeLit(exp *ast.CompositeLit) error {
224225
return err
225226
}
226227
} else {
227-
// Named struct, use constructor
228-
c.tsw.WriteLiterally("new ")
228+
// Named struct value, use constructor
229+
if !c.insideAddressOf {
230+
// Only mark as struct value if not inside address-of operator
231+
c.tsw.WriteLiterally("$.markAsStructValue(new ")
232+
needsValueMarkerClose = true
233+
} else {
234+
c.tsw.WriteLiterally("new ")
235+
}
229236
c.WriteTypeExpr(exp.Type)
230237
}
231238
}
@@ -241,8 +248,14 @@ func (c *GoToTSCompiler) WriteCompositeLit(exp *ast.CompositeLit) error {
241248
return err
242249
}
243250
} else {
244-
// Type alias for struct, use constructor
245-
c.tsw.WriteLiterally("new ")
251+
// Type alias for struct value, use constructor
252+
if !c.insideAddressOf {
253+
// Only mark as struct value if not inside address-of operator
254+
c.tsw.WriteLiterally("$.markAsStructValue(new ")
255+
needsValueMarkerClose = true
256+
} else {
257+
c.tsw.WriteLiterally("new ")
258+
}
246259
c.WriteTypeExpr(exp.Type)
247260
}
248261
}
@@ -483,6 +496,10 @@ func (c *GoToTSCompiler) WriteCompositeLit(exp *ast.CompositeLit) error {
483496
c.tsw.WriteLiterally("}")
484497
} else {
485498
c.tsw.WriteLiterally("})")
499+
// Close markAsStructValue wrapper if we opened one
500+
if needsValueMarkerClose {
501+
c.tsw.WriteLiterally(")")
502+
}
486503
}
487504

488505
} else {

compiler/expr-selector.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,24 +239,24 @@ func (c *GoToTSCompiler) writeMethodValue(exp *ast.SelectorExpr, selection *type
239239
// The receiver should be a copy of the dereferenced value
240240
c.tsw.WriteLiterally(".value.")
241241
c.WriteIdent(exp.Sel, false)
242-
c.tsw.WriteLiterally(".bind(")
242+
c.tsw.WriteLiterally(".bind($.markAsStructValue(")
243243
if err := c.WriteValueExpr(exp.X); err != nil {
244244
return fmt.Errorf("failed to write method value receiver for binding: %w", err)
245245
}
246-
c.tsw.WriteLiterally("!.value.clone())")
246+
c.tsw.WriteLiterally("!.value.clone()))")
247247
} else if !isPointerReceiver && !baseIsPointer {
248248
// Value receiver method on value type: t.Mv
249249
// The receiver should be a copy of the value
250250
c.tsw.WriteLiterally(".")
251251
c.WriteIdent(exp.Sel, false)
252-
c.tsw.WriteLiterally(".bind(")
252+
c.tsw.WriteLiterally(".bind($.markAsStructValue(")
253253
if err := c.WriteValueExpr(exp.X); err != nil {
254254
return fmt.Errorf("failed to write method value receiver for binding: %w", err)
255255
}
256256
if baseIsPointer {
257257
c.tsw.WriteLiterally("!")
258258
}
259-
c.tsw.WriteLiterally(".clone())")
259+
c.tsw.WriteLiterally(".clone()))")
260260
} else {
261261
// Pointer receiver method on pointer type: pt.Mp
262262
// The receiver should be the pointer itself

compiler/expr.go

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,45 @@ func (c *GoToTSCompiler) WriteBinaryExpr(exp *ast.BinaryExpr) error {
382382
// Compare the varRef objects directly using === or !==
383383
if c.isPointerComparison(exp) {
384384
c.tsw.WriteLiterally("(") // Wrap comparison
385-
if err := c.WriteValueExpr(exp.X); err != nil {
386-
return fmt.Errorf("failed to write binary expression left operand: %w", err)
385+
386+
// For pointer comparisons, we need to handle variable varref status
387+
// If a variable is varref'd, we need its .value to get the actual pointer value
388+
389+
// Check if operands are varref'd variables
390+
var leftObj, rightObj types.Object
391+
leftIsVarRef := false
392+
rightIsVarRef := false
393+
394+
if leftIdent, ok := exp.X.(*ast.Ident); ok {
395+
leftObj = c.pkg.TypesInfo.ObjectOf(leftIdent)
396+
if leftObj != nil {
397+
leftIsVarRef = c.analysis.NeedsVarRef(leftObj)
398+
}
399+
}
400+
401+
if rightIdent, ok := exp.Y.(*ast.Ident); ok {
402+
rightObj = c.pkg.TypesInfo.ObjectOf(rightIdent)
403+
if rightObj != nil {
404+
rightIsVarRef = c.analysis.NeedsVarRef(rightObj)
405+
}
387406
}
407+
408+
// Write left operand
409+
if leftIdent, ok := exp.X.(*ast.Ident); ok {
410+
if leftIsVarRef {
411+
// Variable is varref'd, access its .value to get the pointer
412+
c.WriteIdent(leftIdent, true)
413+
} else {
414+
// Variable is not varref'd, use it directly
415+
c.WriteIdent(leftIdent, false)
416+
}
417+
} else {
418+
// For non-identifiers, use WriteValueExpr
419+
if err := c.WriteValueExpr(exp.X); err != nil {
420+
return fmt.Errorf("failed to write binary expression left operand: %w", err)
421+
}
422+
}
423+
388424
c.tsw.WriteLiterally(" ")
389425
// Use === for == and !== for !=
390426
tokStr := ""
@@ -398,9 +434,23 @@ func (c *GoToTSCompiler) WriteBinaryExpr(exp *ast.BinaryExpr) error {
398434
}
399435
c.tsw.WriteLiterally(tokStr)
400436
c.tsw.WriteLiterally(" ")
401-
if err := c.WriteValueExpr(exp.Y); err != nil {
402-
return fmt.Errorf("failed to write binary expression right operand: %w", err)
437+
438+
// Write right operand
439+
if rightIdent, ok := exp.Y.(*ast.Ident); ok {
440+
if rightIsVarRef {
441+
// Variable is varref'd, access its .value to get the pointer
442+
c.WriteIdent(rightIdent, true)
443+
} else {
444+
// Variable is not varref'd, use it directly
445+
c.WriteIdent(rightIdent, false)
446+
}
447+
} else {
448+
// For non-identifiers, use WriteValueExpr
449+
if err := c.WriteValueExpr(exp.Y); err != nil {
450+
return fmt.Errorf("failed to write binary expression right operand: %w", err)
451+
}
403452
}
453+
404454
c.tsw.WriteLiterally(")") // Close wrap
405455
return nil
406456
}
@@ -503,12 +553,21 @@ func (c *GoToTSCompiler) WriteUnaryExpr(exp *ast.UnaryExpr) error {
503553
}
504554
}
505555

556+
// Note: With inversion to markAsStructValue, we no longer mark &CompositeLit{}
557+
// since we now mark the CompositeLit{} (struct values) instead of pointers
558+
506559
// Otherwise (&unvarrefedVar, &CompositeLit{}, &FuncCall(), etc.),
507560
// the address-of operator in Go, when used to create a pointer,
508561
// translates to simply evaluating the operand in TypeScript.
509562
// The resulting value (e.g., a new object instance) acts as the "pointer".
510563
// VarRefing decisions are handled at the assignment site based on the LHS variable.
511-
if err := c.WriteValueExpr(exp.X); err != nil {
564+
565+
// Set context flag to prevent marking composite literals as struct values
566+
c.insideAddressOf = true
567+
err := c.WriteValueExpr(exp.X)
568+
c.insideAddressOf = false
569+
570+
if err != nil {
512571
return fmt.Errorf("failed to write &-operand: %w", err)
513572
}
514573

0 commit comments

Comments
 (0)