44 "fmt"
55 "os"
66 "path/filepath"
7+ "reflect"
78 "runtime"
89 "strings"
910)
@@ -66,16 +67,14 @@ func normalizeFileForPackageMatch(file string) string {
6667// When successful it returns the file name and line number, empty string and
6768// 0 otherwise.
6869func computeErrorLocation () (file string , line int ) {
69- skipFunc := func (pc uintptr , file string ) bool {
70+ shouldSkip := func (file , name string ) bool {
7071 if strings .HasSuffix (file , "_test.go" ) { // Be nice with tests
7172 return false
7273 }
73- file = filepath .ToSlash (file )
74- fn := runtime .FuncForPC (pc )
75- var name string
76- if fn != nil {
77- name = fn .Name ()
74+ if isGoaSourceFile (file ) {
75+ return true
7876 }
77+ file = filepath .ToSlash (file )
7978 normalized := normalizeFileForPackageMatch (file )
8079 for _ , pkg := range Context .dslPackages {
8180 if strings .Contains (file , pkg ) || strings .Contains (normalized , pkg ) || strings .Contains (name , pkg ) {
@@ -84,24 +83,97 @@ func computeErrorLocation() (file string, line int) {
8483 }
8584 return false
8685 }
87- depth := 3
88- pc , file , line , _ := runtime .Caller (depth )
89- for skipFunc (pc , file ) {
90- depth ++
91- pc , file , line , _ = runtime .Caller (depth )
86+
87+ // Start scanning just above computeErrorLocation itself. This is robust to
88+ // inlining and avoids hardcoding assumptions about the exact call depth.
89+ const skip = 2
90+ pcs := make ([]uintptr , 32 )
91+ n := runtime .Callers (skip , pcs )
92+ frames := runtime .CallersFrames (pcs [:n ])
93+ for {
94+ frame , more := frames .Next ()
95+ if frame .File == "" || frame .Line == 0 {
96+ if ! more {
97+ break
98+ }
99+ continue
100+ }
101+ if ! shouldSkip (frame .File , frame .Function ) {
102+ return relativeToWorkdir (frame .File ), frame .Line
103+ }
104+ if ! more {
105+ break
106+ }
107+ }
108+ return "" , 0
109+ }
110+
111+ // isGoaSourceFile reports whether file points into the Goa module sources.
112+ //
113+ // This is used to robustly skip internal Goa frames even when the runtime
114+ // reports a call location (file:line) inside an inlined Goa function but the
115+ // corresponding frame Function name does not include an import path.
116+ func isGoaSourceFile (file string ) bool {
117+ _ , thisFile , _ , ok := runtime .Caller (0 )
118+ if ! ok {
119+ return false
120+ }
121+ root := filepath .Dir (filepath .Dir (thisFile ))
122+ rel , err := filepath .Rel (root , file )
123+ if err != nil {
124+ return false
125+ }
126+ return rel == "." || (rel != ".." && ! strings .HasPrefix (rel , ".." + string (filepath .Separator )))
127+ }
128+
129+ // validationErrorLocation returns the location of the DSL that declared the
130+ // given expression when available.
131+ //
132+ // The location is derived from the expression DSL function pointer and is used
133+ // to annotate validation errors (i.e. errors returned by Validate()).
134+ func validationErrorLocation (expr Expression ) (file string , line int , ok bool ) {
135+ source , ok := expr .(Source )
136+ if ! ok {
137+ return "" , 0 , false
138+ }
139+ fn := source .DSL ()
140+ if fn == nil {
141+ return "" , 0 , false
92142 }
143+ return dslFuncLocation (fn )
144+ }
145+
146+ // dslFuncLocation returns the file and line where the given DSL function is
147+ // declared.
148+ //
149+ // The returned file is relative to the current working directory when possible.
150+ func dslFuncLocation (fn func ()) (file string , line int , ok bool ) {
151+ pc := reflect .ValueOf (fn ).Pointer ()
152+ f := runtime .FuncForPC (pc )
153+ if f == nil {
154+ return "" , 0 , false
155+ }
156+ file , line = f .FileLine (pc )
157+ if file == "" || line == 0 {
158+ return "" , 0 , false
159+ }
160+ return relativeToWorkdir (file ), line , true
161+ }
162+
163+ // relativeToWorkdir returns file relative to the current working directory when
164+ // possible, otherwise it returns file unchanged.
165+ func relativeToWorkdir (file string ) string {
93166 wd , err := os .Getwd ()
94167 if err != nil {
95- return file , line
168+ return file
96169 }
97170 wd , err = filepath .Abs (wd )
98171 if err != nil {
99- return file , line
172+ return file
100173 }
101- f , err := filepath .Rel (wd , file )
174+ rel , err := filepath .Rel (wd , file )
102175 if err != nil {
103- return file , line
176+ return file
104177 }
105- file = f
106- return file , line
178+ return rel
107179}
0 commit comments