Skip to content

Commit 3f1094d

Browse files
committed
fix: interface embedding
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent c94e582 commit 3f1094d

File tree

24 files changed

+153
-92
lines changed

24 files changed

+153
-92
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ When updating design documents:
154154
When eliminating dead code if requested by the user:
155155

156156
1. Receive instructions from the user
157-
2. Run `golangci-lint run --no-config --enable-only=unused`
157+
2. Run `golangci-lint run --no-config --enable-only=unused` (exactly this command)
158158
3. Remove any unused code in `./compiler` ignoring ./compliance
159159
4. Any line which is unused in `./compliance` add a `//nolint:unused` comment at the end.
160160
5. Rerun the golangci-lint command to ensure we got everything.

compiler/analysis.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,21 +1254,26 @@ func (a *Analysis) addImportsForPromotedMethods(pkg *packages.Package) {
12541254

12551255
// Get the type of the embedded field
12561256
embeddedType := field.Type()
1257-
1257+
12581258
// Handle pointer to embedded type
12591259
if ptr, ok := embeddedType.(*types.Pointer); ok {
12601260
embeddedType = ptr.Elem()
12611261
}
12621262

1263-
// Get named type to access methods
1264-
embeddedNamed, ok := embeddedType.(*types.Named)
1265-
if !ok {
1266-
continue
1263+
// Use method set to get all promoted methods including pointer receiver methods
1264+
// This matches Go's behavior where embedding T promotes both T and *T methods
1265+
methodSetType := embeddedType
1266+
if _, isPtr := embeddedType.(*types.Pointer); !isPtr {
1267+
if _, isInterface := embeddedType.Underlying().(*types.Interface); !isInterface {
1268+
methodSetType = types.NewPointer(embeddedType)
1269+
}
12671270
}
1271+
embeddedMethodSet := types.NewMethodSet(methodSetType)
12681272

1269-
// Scan all methods of the embedded type
1270-
for j := 0; j < embeddedNamed.NumMethods(); j++ {
1271-
method := embeddedNamed.Method(j)
1273+
// Scan all methods in the method set
1274+
for j := 0; j < embeddedMethodSet.Len(); j++ {
1275+
selection := embeddedMethodSet.At(j)
1276+
method := selection.Obj()
12721277
sig, ok := method.Type().(*types.Signature)
12731278
if !ok {
12741279
continue
@@ -1319,6 +1324,15 @@ func (a *Analysis) collectPackageFromType(t types.Type, currentPkg *types.Packag
13191324
a.collectPackageFromType(typ.TypeArgs().At(i), currentPkg, packagesToAdd)
13201325
}
13211326
}
1327+
case *types.Interface:
1328+
// For interfaces, we need to check embedded interfaces and method signatures
1329+
for i := 0; i < typ.NumEmbeddeds(); i++ {
1330+
a.collectPackageFromType(typ.EmbeddedType(i), currentPkg, packagesToAdd)
1331+
}
1332+
for i := 0; i < typ.NumExplicitMethods(); i++ {
1333+
method := typ.ExplicitMethod(i)
1334+
a.collectPackageFromType(method.Type(), currentPkg, packagesToAdd)
1335+
}
13221336
case *types.Pointer:
13231337
a.collectPackageFromType(typ.Elem(), currentPkg, packagesToAdd)
13241338
case *types.Slice:

compiler/compiler.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,8 @@ func (c *FileCompiler) Compile(ctx context.Context) error {
678678

679679
// Write any imports that were added during analysis (e.g., for promoted methods)
680680
// but don't appear in the source AST
681+
// Note: c.Analysis.Imports is package-wide, so we need to filter to only imports
682+
// that are actually used in this specific file
681683
writtenImports := make(map[string]bool)
682684
// Track imports that will be written from the AST
683685
for _, imp := range f.Imports {
@@ -693,17 +695,47 @@ func (c *FileCompiler) Compile(ctx context.Context) error {
693695
}
694696
}
695697
}
696-
// Write imports from analysis that aren't in the AST
697-
var additionalImports []string
698-
for pkgName := range c.Analysis.Imports {
699-
if !writtenImports[pkgName] && pkgName != "$" {
700-
additionalImports = append(additionalImports, pkgName)
698+
// Write any imports that were added during analysis (e.g., for promoted methods)
699+
// Check if this file defines any structs with embedded fields
700+
fileHasEmbeddedStructs := false
701+
for _, decl := range f.Decls {
702+
genDecl, ok := decl.(*ast.GenDecl)
703+
if !ok || genDecl.Tok != token.TYPE {
704+
continue
705+
}
706+
for _, spec := range genDecl.Specs {
707+
typeSpec, ok := spec.(*ast.TypeSpec)
708+
if !ok {
709+
continue
710+
}
711+
structType, ok := typeSpec.Type.(*ast.StructType)
712+
if !ok {
713+
continue
714+
}
715+
// Check if any field is embedded
716+
for _, field := range structType.Fields.List {
717+
if len(field.Names) == 0 { // Embedded field
718+
fileHasEmbeddedStructs = true
719+
break
720+
}
721+
}
722+
if fileHasEmbeddedStructs {
723+
break
724+
}
725+
}
726+
if fileHasEmbeddedStructs {
727+
break
701728
}
702729
}
703-
sort.Strings(additionalImports)
704-
for _, pkgName := range additionalImports {
705-
fileImp := c.Analysis.Imports[pkgName]
706-
c.codeWriter.WriteImport(pkgName, fileImp.importPath+"/index.js")
730+
731+
// If this file has embedded structs, write imports from analysis that aren't in AST
732+
if fileHasEmbeddedStructs {
733+
for pkgName, fileImport := range c.Analysis.Imports {
734+
if !writtenImports[pkgName] {
735+
c.codeWriter.WriteLinef("import * as %s from \"%s/index.js\"", pkgName, fileImport.importPath)
736+
writtenImports[pkgName] = true
737+
}
738+
}
707739
}
708740

709741
c.codeWriter.WriteLine("") // Add a newline after imports

compiler/spec-struct.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -345,21 +345,24 @@ func (c *GoToTSCompiler) WriteStructTypeSpec(a *ast.TypeSpec, t *ast.StructType)
345345
}
346346
}
347347

348-
// Promoted methods
349-
// Use pointer to embedded type to get both value and pointer receiver methods
350-
// This matches Go's behavior where embedding T promotes both T and *T methods
351-
methodSetType := embeddedFieldType
352-
if _, isPtr := embeddedFieldType.(*types.Pointer); !isPtr {
353-
methodSetType = types.NewPointer(embeddedFieldType)
354-
}
355-
embeddedMethodSet := types.NewMethodSet(methodSetType)
356-
for k := range embeddedMethodSet.Len() {
357-
methodSelection := embeddedMethodSet.At(k)
358-
method := methodSelection.Obj().(*types.Func)
359-
methodName := method.Name()
360-
361-
// Skip if it's not a promoted method (indirect) or if it's shadowed by a direct method or an already processed promoted method
362-
if len(methodSelection.Index()) == 1 && !directMethods[methodName] && !seenPromotedFields[methodName] {
348+
// Promoted methods
349+
// Use pointer to embedded type to get both value and pointer receiver methods
350+
// This matches Go's behavior where embedding T promotes both T and *T methods
351+
// Exception: For interfaces, use the interface directly as pointer-to-interface has no methods
352+
methodSetType := embeddedFieldType
353+
if _, isPtr := embeddedFieldType.(*types.Pointer); !isPtr {
354+
if _, isInterface := embeddedFieldType.Underlying().(*types.Interface); !isInterface {
355+
methodSetType = types.NewPointer(embeddedFieldType)
356+
}
357+
}
358+
embeddedMethodSet := types.NewMethodSet(methodSetType)
359+
for k := range embeddedMethodSet.Len() {
360+
methodSelection := embeddedMethodSet.At(k)
361+
method := methodSelection.Obj().(*types.Func)
362+
methodName := method.Name()
363+
364+
// Skip if it's not a promoted method (indirect) or if it's shadowed by a direct method or an already processed promoted method
365+
if len(methodSelection.Index()) == 1 && !directMethods[methodName] && !seenPromotedFields[methodName] {
363366
// Check for conflict with outer struct's own fields
364367
conflictWithField := false
365368
for k_idx := 0; k_idx < underlyingStruct.NumFields(); k_idx++ {

compliance/deps/encoding/json/encode.gs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,10 @@ export class jsonError {
575575
return cloned
576576
}
577577

578+
public Error(): string {
579+
return this.error!.Error()
580+
}
581+
578582
// Register this type with the runtime type system
579583
static __typeInfo = $.registerStructType(
580584
'encoding/json.jsonError',

compliance/deps/go/scanner/errors.gs.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
import * as $ from "@goscript/builtin/index.js"
2-
import * as bytes from "bytes/index.js"
3-
import * as filepath from "path/filepath/index.js"
4-
import * as strconv from "strconv/index.js"
5-
import * as unicode from "unicode/index.js"
6-
import * as utf8 from "unicode/utf8/index.js"
72

83
import * as fmt from "@goscript/fmt/index.js"
94

compliance/deps/go/scanner/scanner.gs.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import * as $ from "@goscript/builtin/index.js"
2-
import * as io from "@goscript/io/index.js"
3-
import * as sort from "@goscript/sort/index.js"
42

53
import * as bytes from "@goscript/bytes/index.js"
64

compliance/deps/go/token/position.gs.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import * as $ from "@goscript/builtin/index.js"
22
import { key, tree } from "./tree.gs.js";
3-
import * as iter from "iter/index.js"
4-
import * as unicode from "unicode/index.js"
5-
import * as utf8 from "unicode/utf8/index.js"
63

74
import * as cmp from "@goscript/cmp/index.js"
85

compliance/deps/go/token/serialize.gs.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import * as $ from "@goscript/builtin/index.js"
22
import { File, FileSet, lineInfo } from "./position.gs.js";
3-
import * as atomic from "@goscript/sync/atomic/index.js"
4-
import * as cmp from "@goscript/cmp/index.js"
5-
import * as fmt from "@goscript/fmt/index.js"
6-
import * as iter from "iter/index.js"
7-
import * as strconv from "@goscript/strconv/index.js"
8-
import * as sync from "@goscript/sync/index.js"
9-
import * as unicode from "unicode/index.js"
10-
import * as utf8 from "unicode/utf8/index.js"
113

124
import * as slices from "@goscript/slices/index.js"
135

compliance/deps/go/token/token.gs.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
11
import * as $ from "@goscript/builtin/index.js"
2-
import * as atomic from "@goscript/sync/atomic/index.js"
3-
import * as cmp from "@goscript/cmp/index.js"
4-
import * as fmt from "@goscript/fmt/index.js"
5-
import * as iter from "iter/index.js"
6-
import * as slices from "@goscript/slices/index.js"
7-
import * as sync from "@goscript/sync/index.js"
82

93
import * as strconv from "@goscript/strconv/index.js"
104

0 commit comments

Comments
 (0)