go-poet
go-poet copied to clipboard
Importing from the same package as a generated file
In the following example, a file is generated inside of package main
while referencing a function, foo
, which is also in package main
.
package main
import (
"fmt"
"github.com/dpolansky/go-poet/poet"
)
func foo() {
}
func main() {
bar := poet.NewFuncSpec("bar").Statement("$T()", poet.TypeReferenceFromInstance(foo))
file := poet.NewFileSpec("main").CodeBlock(bar)
fmt.Println(file)
}
which currently produces
package main
import (
"main"
)
func bar() {
main.foo()
}
instead of the expected output
package main
func bar() {
foo()
}
We should be able to know that foo
is in the same package as the generated file and not import itself. In addition, since we only specify the base name of a package rather than the full path, we do not know the full package path of the generated file, making it ambiguous which packages do not need to be imported.
Here's how I'm thinking about it: there are two distinct places where the local package matters:
- When we build the
imports (...)
block inFileSpec.String()
, the local package should be omitted. - When we substitute
$T
in the template, it should become unqualified if we are writing to the local package.
Right now the API gives back a string and doesn't actually know what we are going to do with the generated code. I think we must get the ImportPath from the user when they create a FileSpec. This will let us change the "external imports" filter to skip the local package in addition to blank packages. The change can actually be made in a somewhat backwards compatible way by saying that the default package is path.Base(f.ImportPath)
and changing the package argument on NewFileSpec
to be the ImportPath
.
The type substitution is a little trickier. At a high level, I think we need to defer converting code to a string until we are somewhere with enough context to change TypeReference qualifiers based on ImportPath. Here's a proposal:
- Embed
Import
inTypeReference
, addingGetPackage()
andGetAlias()
. This is the most painful API break as every type implementing the interface will need to be updated. - Change
CodeBlock
's API fromString() string
toStatements []statement
. - Add
codeWriter.WriteCodeBlock
which iterates through the statements and writes them all. Use this as the primary means of writing to acodeWriter
(no morew.WriteCode()
). - Change
newCodeWriter
andtemplate()
take anImport
for the import path where they are writing - When
codeWriter.WriteStatement
runstemplate
, it provides itsImport
- When
template()
encounters$T
and callsgetQualifiedNameFromArg
, it checkscurrPkg == typeRef.GetPackage()
.- If
typeRef
is the local package, return the unqualified name for the type. - Else if
typeRef.GetAlias() != ""
, use the alias as the qualifier. - By default, use
path.Base(typeRef.GetPackage())
as the qualifier. - Note: I think this allows us to deprecate or get rid of the unqualified prefix stuff unless you think the escape hatch is useful.
- If
End-to-end when I use a type declared in my local package:
- My
FileSpec
knows it's local and skips the import - My
codeWriter
knows it's local and unqualifies the name - If I reuse the same
TypeReference
in aFileSpec
with a differentImportPath
, it will do the right thing and import the type. I think this solves #7 too
thoughts?
Delegating the conversion of CodeBlocks
to strings down to the CodeWriter
is a great idea. Combined with passing in the full import path, I think this solves the issue.
Could you elaborate more on how you think this approach could solve #7? My understanding is that we still don't know what the ImportPath
is for a generated type. With the changes to TypeReference
, the GetPackage()
method on a newly created spec will be blank. Are you suggesting that we use the wrapping idea combined with the full import path?