Skip to content

Commit 5487bbc

Browse files
committed
fix: correctly import types from separate files
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent e180042 commit 5487bbc

30 files changed

Lines changed: 530 additions & 37 deletions

compiler/analysis.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ type PackageAnalysis struct {
125125
// FunctionCalls maps file names to the functions they call from other files
126126
// Key: filename (without .go extension), Value: map[sourceFile][]functionNames
127127
FunctionCalls map[string]map[string][]string
128+
129+
// TypeDefs maps file names to the types defined in that file
130+
// Key: filename (without .go extension), Value: list of type names
131+
TypeDefs map[string][]string
132+
133+
// TypeCalls maps file names to the types they reference from other files
134+
// Key: filename (without .go extension), Value: map[sourceFile][]typeNames
135+
TypeCalls map[string]map[string][]string
128136
}
129137

130138
// NewAnalysis creates a new Analysis instance.
@@ -146,6 +154,8 @@ func NewPackageAnalysis() *PackageAnalysis {
146154
return &PackageAnalysis{
147155
FunctionDefs: make(map[string][]string),
148156
FunctionCalls: make(map[string]map[string][]string),
157+
TypeDefs: make(map[string][]string),
158+
TypeCalls: make(map[string]map[string][]string),
149159
}
150160
}
151161

@@ -1036,18 +1046,30 @@ func AnalyzePackage(pkg *packages.Package) *PackageAnalysis {
10361046
baseFileName := strings.TrimSuffix(filepath.Base(fileName), ".go")
10371047

10381048
var functions []string
1049+
var types []string
10391050
for _, decl := range syntax.Decls {
10401051
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
10411052
// Only collect top-level functions (not methods)
10421053
if funcDecl.Recv == nil {
10431054
functions = append(functions, funcDecl.Name.Name)
10441055
}
10451056
}
1057+
if genDecl, ok := decl.(*ast.GenDecl); ok {
1058+
// Collect type declarations
1059+
for _, spec := range genDecl.Specs {
1060+
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
1061+
types = append(types, typeSpec.Name.Name)
1062+
}
1063+
}
1064+
}
10461065
}
10471066

10481067
if len(functions) > 0 {
10491068
analysis.FunctionDefs[baseFileName] = functions
10501069
}
1070+
if len(types) > 0 {
1071+
analysis.TypeDefs[baseFileName] = types
1072+
}
10511073
}
10521074

10531075
// Second pass: analyze function calls and determine which need imports
@@ -1111,6 +1133,71 @@ func AnalyzePackage(pkg *packages.Package) *PackageAnalysis {
11111133
}
11121134
}
11131135

1136+
// Third pass: analyze type references and determine which need imports
1137+
for i, syntax := range pkg.Syntax {
1138+
fileName := pkg.CompiledGoFiles[i]
1139+
baseFileName := strings.TrimSuffix(filepath.Base(fileName), ".go")
1140+
1141+
// Find all type references in this file
1142+
typeRefsFromOtherFiles := make(map[string][]string)
1143+
1144+
ast.Inspect(syntax, func(n ast.Node) bool {
1145+
// Look for type references in struct fields, function parameters, etc.
1146+
if ident, ok := n.(*ast.Ident); ok {
1147+
// Check if this identifier refers to a type
1148+
if obj := pkg.TypesInfo.Uses[ident]; obj != nil {
1149+
if _, ok := obj.(*types.TypeName); ok {
1150+
typeName := ident.Name
1151+
1152+
// Check if this type is defined in the current file
1153+
currentFileTypes := analysis.TypeDefs[baseFileName]
1154+
isDefinedInCurrentFile := false
1155+
for _, t := range currentFileTypes {
1156+
if t == typeName {
1157+
isDefinedInCurrentFile = true
1158+
break
1159+
}
1160+
}
1161+
1162+
// If not defined in current file, find which file defines it
1163+
if !isDefinedInCurrentFile {
1164+
for sourceFile, types := range analysis.TypeDefs {
1165+
if sourceFile == baseFileName {
1166+
continue // Skip current file
1167+
}
1168+
for _, t := range types {
1169+
if t == typeName {
1170+
// Found the type in another file
1171+
if typeRefsFromOtherFiles[sourceFile] == nil {
1172+
typeRefsFromOtherFiles[sourceFile] = []string{}
1173+
}
1174+
// Check if already added to avoid duplicates
1175+
found := false
1176+
for _, existing := range typeRefsFromOtherFiles[sourceFile] {
1177+
if existing == typeName {
1178+
found = true
1179+
break
1180+
}
1181+
}
1182+
if !found {
1183+
typeRefsFromOtherFiles[sourceFile] = append(typeRefsFromOtherFiles[sourceFile], typeName)
1184+
}
1185+
break
1186+
}
1187+
}
1188+
}
1189+
}
1190+
}
1191+
}
1192+
}
1193+
return true
1194+
})
1195+
1196+
if len(typeRefsFromOtherFiles) > 0 {
1197+
analysis.TypeCalls[baseFileName] = typeRefsFromOtherFiles
1198+
}
1199+
}
1200+
11141201
return analysis
11151202
}
11161203

compiler/compiler.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,61 @@ func (c *FileCompiler) Compile(ctx context.Context) error {
621621
}
622622
}
623623

624+
// Generate auto-imports for types from other files in the same package
625+
if typeImports := c.PackageAnalysis.TypeCalls[currentFileName]; typeImports != nil {
626+
// Sort source files for consistent import order
627+
var sourceFiles []string
628+
for sourceFile := range typeImports {
629+
sourceFiles = append(sourceFiles, sourceFile)
630+
}
631+
sort.Strings(sourceFiles)
632+
633+
for _, sourceFile := range sourceFiles {
634+
typeImports := typeImports[sourceFile]
635+
if len(typeImports) > 0 {
636+
// Filter out protobuf types - they should be imported from .pb.js files, not .gs.js files
637+
var nonProtobufTypes []string
638+
for _, typeName := range typeImports {
639+
// Check if this type is a protobuf type by looking for it in the package
640+
isProtobuf := false
641+
if typeObj := c.pkg.Types.Scope().Lookup(typeName); typeObj != nil {
642+
objType := typeObj.Type()
643+
if namedType, ok := objType.(*types.Named); ok {
644+
obj := namedType.Obj()
645+
if obj != nil && obj.Pkg() != nil {
646+
// Check if the type is defined in the current package and has a corresponding .pb.go file
647+
if obj.Pkg() == c.pkg.Types {
648+
// Check if there's a .pb.go file in the package that exports this type
649+
// For now, we'll use a simple heuristic: if the type name ends with "Msg"
650+
// and there's a .pb.go file in the package, assume it's a protobuf type
651+
if strings.HasSuffix(typeName, "Msg") {
652+
// Check if there are any .pb.go files in this package
653+
for _, fileName := range c.pkg.CompiledGoFiles {
654+
if strings.HasSuffix(fileName, ".pb.go") {
655+
isProtobuf = true
656+
break
657+
}
658+
}
659+
}
660+
}
661+
}
662+
}
663+
}
664+
if !isProtobuf {
665+
nonProtobufTypes = append(nonProtobufTypes, typeName)
666+
}
667+
}
668+
669+
if len(nonProtobufTypes) > 0 {
670+
// Sort types for consistent output
671+
sort.Strings(nonProtobufTypes)
672+
c.codeWriter.WriteLinef("import { %s } from \"./%s.gs.js\";",
673+
strings.Join(nonProtobufTypes, ", "), sourceFile)
674+
}
675+
}
676+
}
677+
}
678+
624679
c.codeWriter.WriteLine("") // Add a newline after imports
625680

626681
if err := goWriter.WriteDecls(f.Decls); err != nil {

compiler/spec-struct.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import (
2020
// - Wrapper methods for promoted fields and methods from embedded structs,
2121
// ensuring correct access and behavior.
2222
func (c *GoToTSCompiler) WriteStructTypeSpec(a *ast.TypeSpec, t *ast.StructType) error {
23-
// Add export for Go-exported structs
24-
if a.Name.IsExported() {
25-
c.tsw.WriteLiterally("export ")
26-
}
23+
// Always export types for cross-file imports within the same package
24+
// This allows unexported Go types to be imported by other files in the same package
25+
c.tsw.WriteLiterally("export ")
2726
c.tsw.WriteLiterally("class ")
2827
if err := c.WriteValueExpr(a.Name); err != nil {
2928
return err

compiler/spec.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func (c *GoToTSCompiler) WriteNamedTypeWithMethods(a *ast.TypeSpec) error {
196196
isInsideFunction = nodeInfo.IsInsideFunction
197197
}
198198

199-
if a.Name.IsExported() && !isInsideFunction {
199+
if !isInsideFunction {
200200
c.tsw.WriteLiterally("export ")
201201
}
202202

@@ -404,13 +404,13 @@ func (c *GoToTSCompiler) WriteTypeSpec(a *ast.TypeSpec) error {
404404
return c.WriteNamedTypeWithMethods(a)
405405
}
406406

407-
// type alias - add export for Go-exported types (but not if inside a function)
407+
// Always export types for cross-file imports within the same package (but not if inside a function)
408408
isInsideFunction := false
409409
if nodeInfo := c.analysis.NodeData[a]; nodeInfo != nil {
410410
isInsideFunction = nodeInfo.IsInsideFunction
411411
}
412412

413-
if a.Name.IsExported() && !isInsideFunction {
413+
if !isInsideFunction {
414414
c.tsw.WriteLiterally("export ")
415415
}
416416
c.tsw.WriteLiterally("type ")
@@ -438,7 +438,7 @@ func (c *GoToTSCompiler) WriteInterfaceTypeSpec(a *ast.TypeSpec, t *ast.Interfac
438438
isInsideFunction = nodeInfo.IsInsideFunction
439439
}
440440

441-
if a.Name.IsExported() && !isInsideFunction {
441+
if !isInsideFunction {
442442
c.tsw.WriteLiterally("export ")
443443
}
444444
c.tsw.WriteLiterally("type ")

compiler/stmt-range.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (c *GoToTSCompiler) writeMapRange(exp *ast.RangeStmt) error {
157157
if err := c.WriteValueExpr(exp.X); err != nil {
158158
return fmt.Errorf("failed to write range loop map expression: %w", err)
159159
}
160-
c.tsw.WriteLiterally(".entries()) {")
160+
c.tsw.WriteLiterally("?.entries() ?? []) {")
161161
c.tsw.Indent(1)
162162
c.tsw.WriteLine("")
163163

compiler/type.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,14 +373,15 @@ func (c *GoToTSCompiler) WriteArrayType(t *types.Array) {
373373
}
374374

375375
// WriteMapType translates a Go map type (map[K]V) to its TypeScript equivalent.
376-
// It generates Map<K_ts, V_ts>, where K_ts and V_ts are the translated key
376+
// It generates Map<K_ts, V_ts> | null, where K_ts and V_ts are the translated key
377377
// and element types respectively.
378+
// Maps are nilable in Go, so they are represented as nullable types in TypeScript.
378379
func (c *GoToTSCompiler) WriteMapType(t *types.Map) {
379380
c.tsw.WriteLiterally("Map<")
380381
c.WriteGoType(t.Key(), GoTypeContextGeneral)
381382
c.tsw.WriteLiterally(", ")
382383
c.WriteGoType(t.Elem(), GoTypeContextGeneral)
383-
c.tsw.WriteLiterally(">")
384+
c.tsw.WriteLiterally("> | null")
384385
}
385386

386387
// WriteChannelType translates a Go channel type (chan T) to its TypeScript equivalent.

compliance/WIP.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Type Missing Imports Issue
2+
3+
## Problem Description
4+
When Go types are referenced across different files in the same package, the generated TypeScript code does not properly import these types, causing TypeScript compilation errors.
5+
6+
## Test Case: `type_separate_files`
7+
- `memory.go` defines: `type file struct { name string; data []byte }`
8+
- `storage.go` references: `type storage struct { files map[string]*file; children map[string]map[string]*file }`
9+
- `type_separate_files.go` uses both types
10+
11+
## Issue Observed
12+
1. `memory.gs.ts` generates the `file` class but doesn't export it (because it's unexported in Go)
13+
2. `storage.gs.ts` references `file` type but doesn't import it from `memory.gs.ts`
14+
3. TypeScript compilation fails: `Cannot find name 'file'`
15+
16+
## Expected TypeScript Output
17+
- `memory.gs.ts` should export the `file` class (even though unexported in Go)
18+
- `storage.gs.ts` should import `{ file }` from `"./memory.gs.ts"`
19+
20+
## Analysis Completed
21+
22+
### Current Function Import System
23+
The compiler already has a system for cross-file function imports:
24+
- `PackageAnalysis.FunctionDefs` tracks which functions are defined in each file
25+
- `PackageAnalysis.FunctionCalls` tracks which functions each file calls from other files
26+
- `compiler.go` lines 602-622 generate import statements for functions
27+
28+
### Root Cause
29+
1. **Export Issue**: In `spec-struct.go` line 23, structs are only exported if `a.Name.IsExported()` is true. For unexported types like `file`, this is false.
30+
2. **Import Issue**: There's no equivalent to `FunctionCalls` for tracking type dependencies across files.
31+
32+
### Solution Approach
33+
1. **Always export types within same package**: Modify struct generation to export all types (even unexported ones) when generating TypeScript for same-package consumption
34+
2. **Add type dependency tracking**: Add `TypeDefs` and `TypeCalls` to `PackageAnalysis` similar to function tracking
35+
3. **Generate type imports**: Add import generation for types similar to function imports
36+
37+
## Implementation Plan
38+
1. Add `TypeDefs` and `TypeCalls` fields to `PackageAnalysis` struct
39+
2. Implement type dependency analysis in `AnalyzePackage` function
40+
3. Modify struct generation to always export types within same package
41+
4. Add type import generation in compiler.go
42+
5. Test with `type_separate_files` test case

compliance/compliance.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,13 +490,31 @@ func WriteTypeScriptRunner(t *testing.T, parentModulePath, testDir, tempDir stri
490490
}
491491
}
492492

493+
var mainFile string
493494
if len(goFiles) > 1 {
494-
t.Logf("warning: found multiple Go files in %s, using the first one for runner: %s", testDir, goFiles[0])
495+
// Search for a file containing "func main("
496+
for _, file := range goFiles {
497+
content, err := os.ReadFile(file)
498+
if err != nil {
499+
t.Logf("warning: failed to read file %s: %v", file, err)
500+
continue
501+
}
502+
if strings.Contains(string(content), "func main(") {
503+
mainFile = file
504+
break
505+
}
506+
}
507+
if mainFile == "" {
508+
t.Logf("warning: could not find any Go file containing 'func main(' in %s, using first file: %s", testDir, goFiles[0])
509+
mainFile = goFiles[0]
510+
}
511+
} else {
512+
mainFile = goFiles[0]
495513
}
496514

497515
// Determine tsFileName relative to the test's package root
498-
// e.g. if goFiles[0] is testDir/sub/main.go, relGoPath is sub/main.go
499-
relGoPath, _ := filepath.Rel(testDir, goFiles[0])
516+
// e.g. if mainFile is testDir/sub/main.go, relGoPath is sub/main.go
517+
relGoPath, _ := filepath.Rel(testDir, mainFile)
500518
tsFileRelPath := strings.TrimSuffix(relGoPath, ".go") + ".gs.ts"
501519

502520
testName := filepath.Base(testDir)

compliance/tests/buffer_value_field_error/buffer_value_field_error.gs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import * as $ from "@goscript/builtin/index.js";
55

6-
class buffer {
6+
export class buffer {
77
public get data(): $.Bytes {
88
return this._fields.data.value
99
}

compliance/tests/index_expr_type_assertion/index_expr_type_assertion.gs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export async function main(): Promise<void> {
1818
}
1919

2020
// Test type assertion assignment with map indexed LHS using regular assignment
21-
let m: Map<string, null | any> = $.makeMap<string, null | any>()
21+
let m: Map<string, null | any> | null = $.makeMap<string, null | any>()
2222
$.mapSet(m, "key2", 123)
23-
let mapResults: Map<string, null | any> = $.makeMap<string, null | any>()
23+
let mapResults: Map<string, null | any> | null = $.makeMap<string, null | any>()
2424
let ok2: boolean = false
2525
let _gs_ta_val_545_: number
2626
let _gs_ta_ok_545_: boolean

0 commit comments

Comments
 (0)