Skip to content

Resolve embedded structs declared in another file of the same package - #63

Open
dschreij wants to merge 2 commits into
go-gorm:masterfrom
dschreij:fix/embedded-cross-file
Open

dschreij wants to merge 2 commits into
go-gorm:masterfrom
dschreij:fix/embedded-cross-file

Conversation

@dschreij

@dschreij dschreij commented Sep 5, 2026

Copy link
Copy Markdown

Problem

An anonymous embedded struct declared in another file of the same package loses all of its fields in the generated helper. go/parser resolves identifiers within one file, so the embedded identifier has no Obj when its declaration lives elsewhere, and the generator silently skipped it. The common case is a shared CommonFields struct (ID, CreatedAt, UpdatedAt, DeletedAt) in common.go, embedded by every model: none of the models' helpers had those four fields.

Fix

When an embedded identifier has no Obj and the package path is known, the generator loads the package and processes the declaration the way it already does for a struct from another package. loadNamedStructType now also returns the imports of the file declaring the struct, which are merged into the current file's imports, so the embedded fields' types resolve through the aliases their own file uses.

Test

TestEmbeddedStructDeclaredInAnotherFile generates over two files of one package: Base in one, a model embedding Base by value and another embedding *Base in the other. It asserts Base gets its own helper and both embeddings carry its fields.

The first commit adds a small test helper (generateFromSources, containsField) that writes ad hoc sources into a throwaway module and runs the generator over them. The other pull requests from this fork share that commit; git merges the identical addition without conflict, so they can land in any order.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 5, 2026 07:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new import-merging approach can still produce incorrect type resolution when import aliases differ between files, risking invalid generated output in real-world packages.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes internal/gen’s handling of anonymous embedded structs whose declarations live in a different file within the same Go package, ensuring their fields are included in generated helpers and that any needed imports from the declaring file are available during type resolution.

Changes:

  • Extend embedded-struct handling to load and process same-package declarations when go/parser can’t resolve Ident.Obj.
  • Update loadNamedStructType to also return the declaring file’s imports and reuse a shared newImport helper.
  • Add a focused regression test (plus a small test harness) covering cross-file value and pointer embeddings.
File summaries
File Description
internal/gen/utils.go loadNamedStructType now returns declaring-file imports; adds newImport/fileImports helpers.
internal/gen/generator.go Uses newImport; loads embedded struct declarations from same package when Obj is missing and merges declaring-file imports.
internal/gen/generator_support_test.go Adds helper to generate from ad-hoc sources and assert generated struct fields.
internal/gen/embedded_cross_file_test.go Regression test for cross-file embedded structs (value and pointer embeddings).
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/gen/generator.go Outdated
Comment on lines 786 to 789
// The embedded struct's field types use the aliases of the file declaring it, which may
// import packages this file does not; merge them so those types still resolve.
mergeImports(&p.Imports, imports)
return addEmbeddedFields(st, typeName, pkgName)
Comment on lines +19 to +23
for name, content := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
@dschreij
dschreij force-pushed the fix/embedded-cross-file branch from 1d37b4f to ab742cf Compare September 5, 2026 15:38
@dschreij

dschreij commented Sep 5, 2026

Copy link
Copy Markdown
Author

Addressed the review. The shared test helper now creates parent directories before writing, so a nested key like models/model.go works as its doc comment promises.

The alias finding was right, and it was reachable in both directions: merging into p.Imports deduped by path, so the declaring file's alias for a path this file already imports under another name was dropped, and where the two files bind the same alias to different paths, getFullImportPath returned whichever came first.

Rather than merge, the embedded struct's fields now resolve against a copy of the file whose Imports are the declaring file's followed by this file's. Neither file's aliases reach the other, and p.Imports is left alone. mergeImports is untouched and still used for config imports.

TestEmbeddedStructKeepsItsOwnFileAliases covers it: two files of one package bind m to different packages, and the embedded field's type is a driver.Valuer. Resolved through the wrong package it is not recognised as one, so the column came out as field.Struct[other.Money] instead of field.Field[money.Money]. The assertions read the embedding struct's own block, because Base also gets a helper of its own that is built from its own file and correct either way.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are review comments that should be addressed, including misplaced doc comments and a potentially expensive repeated package-load path introduced for common embeddings.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread internal/gen/generator.go
Comment on lines +780 to 792
// Helper function to load and process a struct type declared in another file or package
loadAndProcessExternalStruct := func(pkgName, typeName string) bool {
st, err := loadNamedStructType(p.goModDir, p.getFullImportPath(pkgName), typeName)
st, imports, err := loadNamedStructType(p.goModDir, p.getFullImportPath(pkgName), typeName)
if err != nil || st == nil {
return false
}
return addEmbeddedFields(st, typeName, pkgName)
// The embedded struct's field types are written in the aliases of the file declaring it,
// which this file need not share and may bind to another path. They resolve against that
// file's imports, on a copy, so neither file's aliases reach the other.
declaring := *p
declaring.Imports = append(append([]Import{}, imports...), p.Imports...)
return addEmbeddedFields(&declaring, st, typeName, pkgName)
}
Comment thread internal/gen/utils.go
Comment on lines 185 to 189
// mergeImports appends imports from src into dst if not already present (by Path)

// shouldSkipFile checks if a file contains the generated code header and should be skipped

func mergeImports(dst *[]Import, src []Import) {
@dschreij
dschreij force-pushed the fix/embedded-cross-file branch from ab742cf to 0038d12 Compare September 5, 2026 20:00
@dschreij

dschreij commented Sep 5, 2026

Copy link
Copy Markdown
Author

Addressed the second round. Both findings were right, and the first was mine.

The doc comments for mergeImports and shouldSkipFile had come adrift when I restored mergeImports after an earlier edit: both ended up stacked above mergeImports, separated by blank lines, leaving shouldSkipFile undocumented. gofmt has nothing to say about that, so it survived. Each is back immediately above its own function, in the original order.

On the repeated loads: loadNamedStructType is now memoised per module root, package and type name. A package where every model embeds one base struct loaded that package once per embedding, and each load shells out to the go command. The generator is single threaded today, but the map is behind a mutex so that stays true if it ever is not.

The shared test helper also reads the whole output tree rather than its top level, which matters here: this pull request's new test is the one that writes sources under subdirectories.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new memoization holds a global mutex across packages.Load, which can unnecessarily serialize and stall generation under concurrent use.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread internal/gen/utils.go
Comment on lines +133 to +146
func loadNamedStructType(modRoot, pkgPath, name string) (*ast.StructType, []Import, error) {
key := modRoot + "\x00" + pkgPath + "\x00" + name

loadedStructsMu.Lock()
defer loadedStructsMu.Unlock()

if cached, ok := loadedStructs[key]; ok {
return cached.st, cached.imports, cached.err
}
st, imports, err := loadNamedStructTypeUncached(modRoot, pkgPath, name)
loadedStructs[key] = loadedStruct{st: st, imports: imports, err: err}

return st, imports, err
}
@dschreij
dschreij force-pushed the fix/embedded-cross-file branch from 0038d12 to 3e691bc Compare September 5, 2026 20:45
@dschreij

dschreij commented Sep 5, 2026

Copy link
Copy Markdown
Author

Fair point, and fixed. The lock now guards the map only: it is taken to read, released across the load, and taken again to store. Two callers racing on one key both load, which costs a redundant load and no correctness, where holding it across packages.Load put every caller behind one shell-out to the go command.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes address the described resolution gap with a targeted implementation and add focused regression tests covering both cross-file embedding and import-alias conflicts.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants