263 lines
7.3 KiB
Go
263 lines
7.3 KiB
Go
|
package jen
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// NewFile Creates a new file, with the specified package name.
|
||
|
func NewFile(packageName string) *File {
|
||
|
return &File{
|
||
|
Group: &Group{
|
||
|
multi: true,
|
||
|
},
|
||
|
name: packageName,
|
||
|
imports: map[string]importdef{},
|
||
|
hints: map[string]importdef{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewFilePath creates a new file while specifying the package path - the
|
||
|
// package name is inferred from the path.
|
||
|
func NewFilePath(packagePath string) *File {
|
||
|
return &File{
|
||
|
Group: &Group{
|
||
|
multi: true,
|
||
|
},
|
||
|
name: guessAlias(packagePath),
|
||
|
path: packagePath,
|
||
|
imports: map[string]importdef{},
|
||
|
hints: map[string]importdef{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewFilePathName creates a new file with the specified package path and name.
|
||
|
func NewFilePathName(packagePath, packageName string) *File {
|
||
|
return &File{
|
||
|
Group: &Group{
|
||
|
multi: true,
|
||
|
},
|
||
|
name: packageName,
|
||
|
path: packagePath,
|
||
|
imports: map[string]importdef{},
|
||
|
hints: map[string]importdef{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// File represents a single source file. Package imports are managed
|
||
|
// automaticaly by File.
|
||
|
type File struct {
|
||
|
*Group
|
||
|
name string
|
||
|
path string
|
||
|
imports map[string]importdef
|
||
|
hints map[string]importdef
|
||
|
comments []string
|
||
|
headers []string
|
||
|
cgoPreamble []string
|
||
|
// If you're worried about generated package aliases conflicting with local variable names, you
|
||
|
// can set a prefix here. Package foo becomes {prefix}_foo.
|
||
|
PackagePrefix string
|
||
|
// CanonicalPath adds a canonical import path annotation to the package clause.
|
||
|
CanonicalPath string
|
||
|
}
|
||
|
|
||
|
// importdef is used to differentiate packages where we know the package name from packages where the
|
||
|
// import is aliased. If alias == false, then name is the actual package name, and the import will be
|
||
|
// rendered without an alias. If used == false, the import has not been used in code yet and should be
|
||
|
// excluded from the import block.
|
||
|
type importdef struct {
|
||
|
name string
|
||
|
alias bool
|
||
|
}
|
||
|
|
||
|
// HeaderComment adds a comment to the top of the file, above any package
|
||
|
// comments. A blank line is rendered below the header comments, ensuring
|
||
|
// header comments are not included in the package doc.
|
||
|
func (f *File) HeaderComment(comment string) {
|
||
|
f.headers = append(f.headers, comment)
|
||
|
}
|
||
|
|
||
|
// PackageComment adds a comment to the top of the file, above the package
|
||
|
// keyword.
|
||
|
func (f *File) PackageComment(comment string) {
|
||
|
f.comments = append(f.comments, comment)
|
||
|
}
|
||
|
|
||
|
// CgoPreamble adds a cgo preamble comment that is rendered directly before the "C" pseudo-package
|
||
|
// import.
|
||
|
func (f *File) CgoPreamble(comment string) {
|
||
|
f.cgoPreamble = append(f.cgoPreamble, comment)
|
||
|
}
|
||
|
|
||
|
// Anon adds an anonymous import.
|
||
|
func (f *File) Anon(paths ...string) {
|
||
|
for _, p := range paths {
|
||
|
f.imports[p] = importdef{name: "_", alias: true}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ImportName provides the package name for a path. If specified, the alias will be omitted from the
|
||
|
// import block. This is optional. If not specified, a sensible package name is used based on the path
|
||
|
// and this is added as an alias in the import block.
|
||
|
func (f *File) ImportName(path, name string) {
|
||
|
f.hints[path] = importdef{name: name, alias: false}
|
||
|
}
|
||
|
|
||
|
// ImportNames allows multiple names to be imported as a map. Use the [gennames](gennames) command to
|
||
|
// automatically generate a go file containing a map of a selection of package names.
|
||
|
func (f *File) ImportNames(names map[string]string) {
|
||
|
for path, name := range names {
|
||
|
f.hints[path] = importdef{name: name, alias: false}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ImportAlias provides the alias for a package path that should be used in the import block. A
|
||
|
// period can be used to force a dot-import.
|
||
|
func (f *File) ImportAlias(path, alias string) {
|
||
|
f.hints[path] = importdef{name: alias, alias: true}
|
||
|
}
|
||
|
|
||
|
func (f *File) isLocal(path string) bool {
|
||
|
return f.path == path
|
||
|
}
|
||
|
|
||
|
var reserved = []string{
|
||
|
/* keywords */
|
||
|
"break", "default", "func", "interface", "select", "case", "defer", "go", "map", "struct", "chan", "else", "goto", "package", "switch", "const", "fallthrough", "if", "range", "type", "continue", "for", "import", "return", "var",
|
||
|
/* predeclared */
|
||
|
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16", "int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", "true", "false", "iota", "nil", "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover",
|
||
|
/* common variables */
|
||
|
"err",
|
||
|
}
|
||
|
|
||
|
func isReservedWord(alias string) bool {
|
||
|
for _, name := range reserved {
|
||
|
if alias == name {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (f *File) isValidAlias(alias string) bool {
|
||
|
// multiple dot-imports are ok
|
||
|
if alias == "." {
|
||
|
return true
|
||
|
}
|
||
|
// the import alias is invalid if it's a reserved word
|
||
|
if isReservedWord(alias) {
|
||
|
return false
|
||
|
}
|
||
|
// the import alias is invalid if it's already been registered
|
||
|
for _, v := range f.imports {
|
||
|
if alias == v.name {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (f *File) isDotImport(path string) bool {
|
||
|
if id, ok := f.hints[path]; ok {
|
||
|
return id.name == "." && id.alias
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (f *File) register(path string) string {
|
||
|
if f.isLocal(path) {
|
||
|
// notest
|
||
|
// should never get here becasue in Qual the packageToken will be null,
|
||
|
// so render will never be called.
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// if the path has been registered previously, simply return the name
|
||
|
def := f.imports[path]
|
||
|
if def.name != "" && def.name != "_" {
|
||
|
return def.name
|
||
|
}
|
||
|
|
||
|
// special case for "C" pseudo-package
|
||
|
if path == "C" {
|
||
|
f.imports["C"] = importdef{name: "C", alias: false}
|
||
|
return "C"
|
||
|
}
|
||
|
|
||
|
var name string
|
||
|
var alias bool
|
||
|
|
||
|
if hint := f.hints[path]; hint.name != "" {
|
||
|
// look up the path in the list of provided package names and aliases by ImportName / ImportAlias
|
||
|
name = hint.name
|
||
|
alias = hint.alias
|
||
|
} else if standardLibraryHints[path] != "" {
|
||
|
// look up the path in the list of standard library packages
|
||
|
name = standardLibraryHints[path]
|
||
|
alias = false
|
||
|
} else {
|
||
|
// if a hint is not found for the package, guess the alias from the package path
|
||
|
name = guessAlias(path)
|
||
|
alias = true
|
||
|
}
|
||
|
|
||
|
// If the name is invalid or has been registered already, make it unique by appending a number
|
||
|
unique := name
|
||
|
i := 0
|
||
|
for !f.isValidAlias(unique) {
|
||
|
i++
|
||
|
unique = fmt.Sprintf("%s%d", name, i)
|
||
|
}
|
||
|
|
||
|
// If we've changed the name to make it unique, it should definitely be an alias
|
||
|
if unique != name {
|
||
|
alias = true
|
||
|
}
|
||
|
|
||
|
// Only add a prefix if the name is an alias
|
||
|
if f.PackagePrefix != "" && alias {
|
||
|
unique = f.PackagePrefix + "_" + unique
|
||
|
}
|
||
|
|
||
|
// Register the eventual name
|
||
|
f.imports[path] = importdef{name: unique, alias: alias}
|
||
|
|
||
|
return unique
|
||
|
}
|
||
|
|
||
|
// GoString renders the File for testing. Any error will cause a panic.
|
||
|
func (f *File) GoString() string {
|
||
|
buf := &bytes.Buffer{}
|
||
|
if err := f.Render(buf); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return buf.String()
|
||
|
}
|
||
|
|
||
|
func guessAlias(path string) string {
|
||
|
alias := path
|
||
|
|
||
|
if strings.HasSuffix(alias, "/") {
|
||
|
// training slashes are usually tolerated, so we can get rid of one if
|
||
|
// it exists
|
||
|
alias = alias[:len(alias)-1]
|
||
|
}
|
||
|
|
||
|
if strings.Contains(alias, "/") {
|
||
|
// if the path contains a "/", use the last part
|
||
|
alias = alias[strings.LastIndex(alias, "/")+1:]
|
||
|
}
|
||
|
|
||
|
// alias should be lower case
|
||
|
alias = strings.ToLower(alias)
|
||
|
|
||
|
// alias should now only contain alphanumerics
|
||
|
importsRegex := regexp.MustCompile(`[^a-z0-9]`)
|
||
|
alias = importsRegex.ReplaceAllString(alias, "")
|
||
|
|
||
|
return alias
|
||
|
}
|