Skip to content

Commit 9b3d30a

Browse files
committed
Detect external packages
Signed-off-by: Tyler Laprade <[email protected]>
1 parent 7e7a60f commit 9b3d30a

File tree

8 files changed

+276
-11
lines changed

8 files changed

+276
-11
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,32 @@ go build -o go2rust ./go
1313
# Transpile a Go file
1414
./go2rust input.go > output.rs
1515

16+
# Transpile with external package handling
17+
./go2rust --external-packages=transpile input.go # Recursively transpile dependencies (default)
18+
./go2rust --external-packages=ffi input.go # Generate FFI bridge to Go libraries
19+
./go2rust --external-packages=none input.go # Error on external imports
20+
1621
# Run tests
1722
./test.sh
1823
```
1924

25+
### External Package Handling
26+
27+
Go2Rust provides three modes for handling external package imports:
28+
29+
1. **`transpile` (default)**: Recursively transpiles all dependencies to Rust
30+
- Pure Rust output with no Go runtime dependency
31+
- Currently in development
32+
33+
2. **`ffi`**: Generates FFI bridge to call Go libraries from Rust
34+
- Keeps Go packages as-is and generates bindings
35+
- Useful for packages with cgo or complex dependencies
36+
- Currently in development
37+
38+
3. **`none`**: Fails if external packages are imported
39+
- Useful for simple, self-contained programs
40+
- Ensures no external dependencies
41+
2042
## Example
2143

2244
**Input (Go):**

go/config.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
11
package main
22

3-
// This file is kept for potential future configuration needs
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// ExternalPackageMode defines how to handle external package imports
9+
type ExternalPackageMode int
10+
11+
const (
12+
ModeTranspile ExternalPackageMode = iota // Recursively transpile dependencies
13+
ModeFfi // Generate FFI bridge
14+
ModeNone // Error on external imports
15+
)
16+
17+
func (m ExternalPackageMode) String() string {
18+
switch m {
19+
case ModeTranspile:
20+
return "transpile"
21+
case ModeFfi:
22+
return "ffi"
23+
case ModeNone:
24+
return "none"
25+
default:
26+
return "unknown"
27+
}
28+
}
29+
30+
func ParseExternalPackageMode(s string) (ExternalPackageMode, error) {
31+
switch strings.ToLower(s) {
32+
case "transpile":
33+
return ModeTranspile, nil
34+
case "ffi":
35+
return ModeFfi, nil
36+
case "none":
37+
return ModeNone, nil
38+
default:
39+
return ModeTranspile, fmt.Errorf("invalid external package mode: %s (must be 'transpile', 'ffi', or 'none')", s)
40+
}
41+
}

go/main.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
11
package main
22

33
import (
4+
"flag"
45
"fmt"
56
"os"
67
"path/filepath"
78
"sort"
89
"strings"
910
)
1011

12+
var (
13+
externalPackagesFlag = flag.String("external-packages", "transpile", "How to handle external packages: transpile, ffi, or none")
14+
helpFlag = flag.Bool("help", false, "Show help message")
15+
)
16+
1117
func main() {
12-
if len(os.Args) < 2 {
13-
fmt.Fprintf(os.Stderr, "Usage: %s <go-file-or-directory>...\n", os.Args[0])
18+
flag.Parse()
19+
20+
if *helpFlag {
21+
showHelp()
22+
os.Exit(0)
23+
}
24+
25+
externalMode, err := ParseExternalPackageMode(*externalPackagesFlag)
26+
if err != nil {
27+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
28+
os.Exit(1)
29+
}
30+
31+
args := flag.Args()
32+
if len(args) == 0 {
33+
fmt.Fprintf(os.Stderr, "Usage: %s [options] <go-file-or-directory>...\n", os.Args[0])
34+
fmt.Fprintf(os.Stderr, "Run '%s -help' for more information\n", os.Args[0])
1435
os.Exit(1)
1536
}
1637

1738
var goFiles []string
18-
for _, arg := range os.Args[1:] {
39+
for _, arg := range args {
1940
files, err := collectGoFiles(arg)
2041
if err != nil {
2142
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", arg, err)
@@ -46,13 +67,32 @@ func main() {
4667
os.Exit(1)
4768
}
4869

49-
err := generator.Generate()
50-
if err != nil {
51-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
70+
// Set the external package mode
71+
generator.SetExternalPackageMode(externalMode)
72+
73+
genErr := generator.Generate()
74+
if genErr != nil {
75+
fmt.Fprintf(os.Stderr, "Error: %v\n", genErr)
5276
os.Exit(1)
5377
}
5478
}
5579

80+
func showHelp() {
81+
fmt.Printf("go2rust - Go to Rust transpiler\n\n")
82+
fmt.Printf("Usage: %s [options] <go-file-or-directory>...\n\n", os.Args[0])
83+
fmt.Printf("Options:\n")
84+
fmt.Printf(" -external-packages <mode> How to handle external packages (default: transpile)\n")
85+
fmt.Printf(" Modes:\n")
86+
fmt.Printf(" transpile - Recursively transpile all dependencies\n")
87+
fmt.Printf(" ffi - Generate FFI bridge to Go libraries\n")
88+
fmt.Printf(" none - Error on external imports\n")
89+
fmt.Printf(" -help Show this help message\n\n")
90+
fmt.Printf("Examples:\n")
91+
fmt.Printf(" %s main.go # Transpile with default settings\n", os.Args[0])
92+
fmt.Printf(" %s -external-packages=ffi ./cmd/myapp # Use FFI for external packages\n", os.Args[0])
93+
fmt.Printf(" %s -external-packages=none simple.go # Fail on external imports\n", os.Args[0])
94+
}
95+
5696
func collectGoFiles(path string) ([]string, error) {
5797
info, err := os.Stat(path)
5898
if err != nil {

go/project.go

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type ProjectGenerator struct {
1919
moduleNames []string
2020
typeInfo *TypeInfo
2121
projectImports *ImportTracker // Collect imports across all files
22+
externalMode ExternalPackageMode
23+
goImports map[string][]string // package path -> list of imports
2224
}
2325

2426
func NewProjectGenerator(goFiles []string) *ProjectGenerator {
@@ -29,10 +31,44 @@ func NewProjectGenerator(goFiles []string) *ProjectGenerator {
2931
goFiles: goFiles,
3032
projectPath: filepath.Dir(goFiles[0]),
3133
projectImports: NewImportTracker(),
34+
externalMode: ModeTranspile, // Default to transpile mode
35+
goImports: make(map[string][]string),
3236
}
3337
}
3438

39+
// SetExternalPackageMode sets how external packages should be handled
40+
func (pg *ProjectGenerator) SetExternalPackageMode(mode ExternalPackageMode) {
41+
pg.externalMode = mode
42+
}
43+
44+
// checkForExternalPackages scans for external package imports when mode is 'none'
45+
func (pg *ProjectGenerator) checkForExternalPackages() error {
46+
fileSet := token.NewFileSet()
47+
48+
for _, filename := range pg.goFiles {
49+
file, err := parser.ParseFile(fileSet, filename, nil, parser.ImportsOnly)
50+
if err != nil {
51+
continue // Skip files with parse errors
52+
}
53+
54+
for _, imp := range file.Imports {
55+
path := strings.Trim(imp.Path.Value, `"`)
56+
if !isStdlibPackage(path) {
57+
return fmt.Errorf("external package import not allowed with --external-packages=none: %s in %s", path, filename)
58+
}
59+
}
60+
}
61+
62+
return nil
63+
}
64+
3565
func (pg *ProjectGenerator) Generate() error {
66+
// Check for external packages first if mode is 'none'
67+
if pg.externalMode == ModeNone {
68+
if err := pg.checkForExternalPackages(); err != nil {
69+
return err
70+
}
71+
}
3672
fileSet := token.NewFileSet()
3773

3874
// Parse all files first for type checking
@@ -75,7 +111,15 @@ func (pg *ProjectGenerator) Generate() error {
75111
pg.isLibrary = pg.packageName != "main"
76112
}
77113

78-
rustCode, fileImports := Transpile(file, fileSet, pg.typeInfo)
114+
rustCode, fileImports, fileExternalPkgs := Transpile(file, fileSet, pg.typeInfo)
115+
116+
// Track external packages found
117+
for pkg := range fileExternalPkgs {
118+
if pg.goImports[filename] == nil {
119+
pg.goImports[filename] = []string{}
120+
}
121+
pg.goImports[filename] = append(pg.goImports[filename], pkg)
122+
}
79123

80124
// Merge file imports into project imports
81125
if fileImports != nil {
@@ -110,6 +154,33 @@ func (pg *ProjectGenerator) Generate() error {
110154
pg.moduleNames = append(pg.moduleNames, outputName)
111155
}
112156

157+
// Handle external packages based on mode
158+
if len(pg.goImports) > 0 && pg.hasExternalPackages() {
159+
switch pg.externalMode {
160+
case ModeTranspile:
161+
// TODO: Implement recursive transpilation
162+
fmt.Fprintf(os.Stderr, "Warning: Recursive transpilation of external packages not yet implemented\n")
163+
fmt.Fprintf(os.Stderr, "External packages found:\n")
164+
for _, imports := range pg.goImports {
165+
for _, pkg := range imports {
166+
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
167+
}
168+
}
169+
case ModeFfi:
170+
// TODO: Implement FFI bridge generation
171+
fmt.Fprintf(os.Stderr, "Warning: FFI bridge generation not yet implemented\n")
172+
fmt.Fprintf(os.Stderr, "External packages found:\n")
173+
for _, imports := range pg.goImports {
174+
for _, pkg := range imports {
175+
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
176+
}
177+
}
178+
case ModeNone:
179+
// This should have been caught earlier, but double-check
180+
return fmt.Errorf("external packages found but mode is 'none'")
181+
}
182+
}
183+
113184
// Second pass: generate main.rs or lib.rs with module declarations
114185
if pg.hasMain {
115186
err := pg.generateMainRs(fileSet, astFiles)
@@ -126,6 +197,16 @@ func (pg *ProjectGenerator) Generate() error {
126197
return pg.generateCargoToml()
127198
}
128199

200+
// hasExternalPackages checks if any external packages were found
201+
func (pg *ProjectGenerator) hasExternalPackages() bool {
202+
for _, imports := range pg.goImports {
203+
if len(imports) > 0 {
204+
return true
205+
}
206+
}
207+
return false
208+
}
209+
129210
func (pg *ProjectGenerator) hasMainFile() bool {
130211
for _, file := range pg.goFiles {
131212
if filepath.Base(file) == "main.go" {
@@ -162,7 +243,24 @@ func (pg *ProjectGenerator) generateMainRs(fileSet *token.FileSet, astFiles []*a
162243
mainRust.WriteString("\n")
163244
}
164245

165-
mainContent, mainImports := Transpile(file, fileSet, pg.typeInfo)
246+
mainContent, mainImports, mainExternalPkgs := Transpile(file, fileSet, pg.typeInfo)
247+
248+
// Track external packages from main
249+
mainPath := ""
250+
for _, fname := range pg.goFiles {
251+
if filepath.Base(fname) == "main.go" {
252+
mainPath = fname
253+
break
254+
}
255+
}
256+
if mainPath != "" {
257+
for pkg := range mainExternalPkgs {
258+
if pg.goImports[mainPath] == nil {
259+
pg.goImports[mainPath] = []string{}
260+
}
261+
pg.goImports[mainPath] = append(pg.goImports[mainPath], pkg)
262+
}
263+
}
166264

167265
// Merge main imports into project imports
168266
if mainImports != nil {

go/transpile.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ var typeDefinitions = make(map[string]string) // maps type name to underlying ty
3636
// typeAliases tracks which types are type aliases
3737
var typeAliases = make(map[string]bool)
3838

39+
// goPackageImports tracks imported Go packages for the current file
40+
// map[alias]packagePath (alias can be empty for default)
41+
var goPackageImports = make(map[string]string)
42+
43+
// externalPackages tracks external (non-stdlib) package imports
44+
var externalPackages = make(map[string]bool)
45+
3946
// structDefs tracks struct definitions and their fields
4047
type StructDef struct {
4148
Fields map[string]string // field name -> field type
@@ -55,6 +62,31 @@ type FieldAccessInfo struct {
5562
FieldName string // The actual field name (snake_case)
5663
}
5764

65+
// trackGoImport tracks a Go import statement
66+
func trackGoImport(packagePath string, nameIdent *ast.Ident) {
67+
// Determine the alias (if any)
68+
var alias string
69+
if nameIdent != nil {
70+
if nameIdent.Name == "_" {
71+
// Blank import - ignore for now
72+
return
73+
}
74+
alias = nameIdent.Name
75+
} else {
76+
// Default alias is the last component of the path
77+
parts := strings.Split(packagePath, "/")
78+
alias = parts[len(parts)-1]
79+
}
80+
81+
// Track the import
82+
goPackageImports[alias] = packagePath
83+
84+
// Check if it's an external package (not stdlib)
85+
if !isStdlibPackage(packagePath) {
86+
externalPackages[packagePath] = true
87+
}
88+
}
89+
5890
// resolveFieldAccess finds the path to access a field, considering embedded structs
5991
func resolveFieldAccess(structType string, fieldName string) FieldAccessInfo {
6092
// Check if it's a direct field
@@ -301,11 +333,15 @@ func implementsInterface(typeMethods []*ast.FuncDecl, iface *ast.InterfaceType)
301333
return true
302334
}
303335

304-
func Transpile(file *ast.File, fileSet *token.FileSet, typeInfo *TypeInfo) (string, *ImportTracker) {
336+
func Transpile(file *ast.File, fileSet *token.FileSet, typeInfo *TypeInfo) (string, *ImportTracker, map[string]bool) {
305337
// Create trackers
306338
imports := NewImportTracker()
307339
helpers := &HelperTracker{}
308340

341+
// Clear import tracking for this file
342+
goPackageImports = make(map[string]string)
343+
externalPackages = make(map[string]bool)
344+
309345
// Initialize the statement preprocessor
310346
statementPreprocessor = NewStatementPreprocessor(fileSet)
311347

@@ -348,6 +384,15 @@ func Transpile(file *ast.File, fileSet *token.FileSet, typeInfo *TypeInfo) (stri
348384
}
349385
case *ast.GenDecl:
350386
switch d.Tok {
387+
case token.IMPORT:
388+
// Track imports for external package handling
389+
for _, spec := range d.Specs {
390+
if importSpec, ok := spec.(*ast.ImportSpec); ok {
391+
path := strings.Trim(importSpec.Path.Value, `"`)
392+
// Track this import (will be handled based on external package mode)
393+
trackGoImport(path, importSpec.Name)
394+
}
395+
}
351396
case token.TYPE:
352397
for _, spec := range d.Specs {
353398
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
@@ -609,5 +654,5 @@ func Transpile(file *ast.File, fileSet *token.FileSet, typeInfo *TypeInfo) (stri
609654
}
610655
output.WriteString(body.String())
611656

612-
return output.String(), imports
657+
return output.String(), imports, externalPackages
613658
}

0 commit comments

Comments
 (0)