@@ -81,7 +81,9 @@ var ErrNoFunc = errors.New("no call stack information")
8181//
8282// It accepts the '+' and '#' flags for most of the verbs as follows.
8383//
84- // %+s path of source file relative to the compile time GOPATH
84+ // %+s path of source file relative to the compile time GOPATH,
85+ // or the module path joined to the path of source file relative
86+ // to module root
8587// %#s full path of source file
8688// %+n import path qualified function name
8789// %+k full package path
@@ -100,7 +102,7 @@ func (c Call) Format(s fmt.State, verb rune) {
100102 case s .Flag ('#' ):
101103 // done
102104 case s .Flag ('+' ):
103- file = file [ pkgIndex ( file , c .frame . Function ):]
105+ file = pkgFilePath ( & c .frame )
104106 default :
105107 const sep = "/"
106108 if i := strings .LastIndex (file , sep ); i != - 1 {
@@ -285,6 +287,80 @@ func pkgIndex(file, funcName string) int {
285287 return i + len (sep )
286288}
287289
290+ // pkgFilePath returns the frame's filepath relative to the compile-time GOPATH,
291+ // or its module path joined to its path relative to the module root.
292+ //
293+ // As of Go 1.11 there is no direct way to know the compile time GOPATH or
294+ // module paths at runtime, but we can piece together the desired information
295+ // from available information. We note that runtime.Frame.Function contains the
296+ // function name qualified by the package path, which includes the module path
297+ // but not the GOPATH. We can extract the package path from that and append the
298+ // last segments of the file path to arrive at the desired package qualified
299+ // file path. For example, given:
300+ //
301+ // GOPATH /home/user
302+ // import path pkg/sub
303+ // frame.File /home/user/src/pkg/sub/file.go
304+ // frame.Function pkg/sub.Type.Method
305+ // Desired return pkg/sub/file.go
306+ //
307+ // It appears that we simply need to trim ".Type.Method" from frame.Function and
308+ // append "/" + path.Base(file).
309+ //
310+ // But there are other wrinkles. Although it is idiomatic to do so, the internal
311+ // name of a package is not required to match the last segment of its import
312+ // path. In addition, the introduction of modules in Go 1.11 allows working
313+ // without a GOPATH. So we also must make these work right:
314+ //
315+ // GOPATH /home/user
316+ // import path pkg/go-sub
317+ // package name sub
318+ // frame.File /home/user/src/pkg/go-sub/file.go
319+ // frame.Function pkg/sub.Type.Method
320+ // Desired return pkg/go-sub/file.go
321+ //
322+ // Module path pkg/v2
323+ // import path pkg/v2/go-sub
324+ // package name sub
325+ // frame.File /home/user/cloned-pkg/go-sub/file.go
326+ // frame.Function pkg/v2/sub.Type.Method
327+ // Desired return pkg/v2/go-sub/file.go
328+ //
329+ // We can handle all of these situations by using the package path extracted
330+ // from frame.Function up to, but not including, the last segment as the prefix
331+ // and the last two segments of frame.File as the suffix of the returned path.
332+ // This preserves the existing behavior when working in a GOPATH without modules
333+ // and a semantically equivalent behavior when used in module aware project.
334+ func pkgFilePath (frame * runtime.Frame ) string {
335+ pre := pkgPrefix (frame .Function )
336+ post := pathSuffix (frame .File )
337+ if pre == "" {
338+ return post
339+ }
340+ return pre + "/" + post
341+ }
342+
343+ // pkgPrefix returns the import path of the function's package with the final
344+ // segment removed.
345+ func pkgPrefix (funcName string ) string {
346+ const pathSep = "/"
347+ end := strings .LastIndex (funcName , pathSep )
348+ if end == - 1 {
349+ return ""
350+ }
351+ return funcName [:end ]
352+ }
353+
354+ // pathSuffix returns the last two segments of path.
355+ func pathSuffix (path string ) string {
356+ const pathSep = "/"
357+ lastSep := strings .LastIndex (path , pathSep )
358+ if lastSep == - 1 {
359+ return path
360+ }
361+ return path [strings .LastIndex (path [:lastSep ], pathSep )+ 1 :]
362+ }
363+
288364var runtimePath string
289365
290366func init () {
0 commit comments