mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
cmd/compile: add explanations to escape-analysis JSON/LSP logging
For 1.15.
From the test:
{"range":{"start":{"line":7,"character":13},"end":{...},"severity":3,"code":"leaks","source":"go compiler","message":"parameter z leaks to ~r2 with derefs=0","relatedInformation":[
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":13},"end":{...}},"message":"escflow: flow: y = z:"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":13},"end":{...}},"message":"escflow: from y = \u003cN\u003e (assign-pair)"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":13},"end":{...}},"message":"escflow: flow: ~r1 = y:"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":4,"character":11},"end":{...}},"message":"inlineLoc"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":13},"end":{...}},"message":"escflow: from y.b (dot of pointer)"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":4,"character":11},"end":{...}},"message":"inlineLoc"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":13},"end":{...}},"message":"escflow: from \u0026y.b (address-of)"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":4,"character":9},"end":...}},"message":"inlineLoc"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":13},"end":{...}},"message":"escflow: from ~r1 = \u003cN\u003e (assign-pair)"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":3},"end":...}},"message":"escflow: flow: ~r2 = ~r1:"},
{"location":{"uri":"file://T/file.go","range":{"start":{"line":9,"character":3},"end":...}},"message":"escflow: from return (*int)(~r1) (return)"}]}
Change-Id: Idf02438801f63e487c35a928cf5a0b6d3cc48674
Reviewed-on: https://go-review.googlesource.com/c/go/+/206658
Run-TryBot: David Chase <drchase@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
This commit is contained in:
parent
fced302aa1
commit
06314b620d
3 changed files with 140 additions and 42 deletions
|
|
@ -7,6 +7,7 @@ package gc
|
||||||
import (
|
import (
|
||||||
"cmd/compile/internal/logopt"
|
"cmd/compile/internal/logopt"
|
||||||
"cmd/compile/internal/types"
|
"cmd/compile/internal/types"
|
||||||
|
"cmd/internal/src"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -945,7 +946,7 @@ func (k EscHole) note(where *Node, why string) EscHole {
|
||||||
if where == nil || why == "" {
|
if where == nil || why == "" {
|
||||||
Fatalf("note: missing where/why")
|
Fatalf("note: missing where/why")
|
||||||
}
|
}
|
||||||
if Debug['m'] >= 2 {
|
if Debug['m'] >= 2 || logopt.Enabled() {
|
||||||
k.notes = &EscNote{
|
k.notes = &EscNote{
|
||||||
next: k.notes,
|
next: k.notes,
|
||||||
where: where,
|
where: where,
|
||||||
|
|
@ -1092,10 +1093,16 @@ func (e *Escape) flow(k EscHole, src *EscLocation) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if dst.escapes && k.derefs < 0 { // dst = &src
|
if dst.escapes && k.derefs < 0 { // dst = &src
|
||||||
if Debug['m'] >= 2 {
|
if Debug['m'] >= 2 || logopt.Enabled() {
|
||||||
pos := linestr(src.n.Pos)
|
pos := linestr(src.n.Pos)
|
||||||
|
if Debug['m'] >= 2 {
|
||||||
fmt.Printf("%s: %v escapes to heap:\n", pos, src.n)
|
fmt.Printf("%s: %v escapes to heap:\n", pos, src.n)
|
||||||
e.explainFlow(pos, dst, src, k.derefs, k.notes)
|
}
|
||||||
|
explanation := e.explainFlow(pos, dst, src, k.derefs, k.notes, []*logopt.LoggedOpt{})
|
||||||
|
if logopt.Enabled() {
|
||||||
|
logopt.LogOpt(src.n.Pos, "escapes", "escape", e.curfn.funcname(), fmt.Sprintf("%v escapes to heap", src.n), explanation)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
src.escapes = true
|
src.escapes = true
|
||||||
return
|
return
|
||||||
|
|
@ -1187,9 +1194,15 @@ func (e *Escape) walkOne(root *EscLocation, walkgen uint32, enqueue func(*EscLoc
|
||||||
// that value flow for tagging the function
|
// that value flow for tagging the function
|
||||||
// later.
|
// later.
|
||||||
if l.isName(PPARAM) {
|
if l.isName(PPARAM) {
|
||||||
if Debug['m'] >= 2 && !l.escapes {
|
if (logopt.Enabled() || Debug['m'] >= 2) && !l.escapes {
|
||||||
|
if Debug['m'] >= 2 {
|
||||||
fmt.Printf("%s: parameter %v leaks to %s with derefs=%d:\n", linestr(l.n.Pos), l.n, e.explainLoc(root), base)
|
fmt.Printf("%s: parameter %v leaks to %s with derefs=%d:\n", linestr(l.n.Pos), l.n, e.explainLoc(root), base)
|
||||||
e.explainPath(root, l)
|
}
|
||||||
|
explanation := e.explainPath(root, l)
|
||||||
|
if logopt.Enabled() {
|
||||||
|
logopt.LogOpt(l.n.Pos, "leak", "escape", e.curfn.funcname(),
|
||||||
|
fmt.Sprintf("parameter %v leaks to %s with derefs=%d", l.n, e.explainLoc(root), base), explanation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
l.leakTo(root, base)
|
l.leakTo(root, base)
|
||||||
}
|
}
|
||||||
|
|
@ -1198,9 +1211,14 @@ func (e *Escape) walkOne(root *EscLocation, walkgen uint32, enqueue func(*EscLoc
|
||||||
// outlives it, then l needs to be heap
|
// outlives it, then l needs to be heap
|
||||||
// allocated.
|
// allocated.
|
||||||
if addressOf && !l.escapes {
|
if addressOf && !l.escapes {
|
||||||
|
if logopt.Enabled() || Debug['m'] >= 2 {
|
||||||
if Debug['m'] >= 2 {
|
if Debug['m'] >= 2 {
|
||||||
fmt.Printf("%s: %v escapes to heap:\n", linestr(l.n.Pos), l.n)
|
fmt.Printf("%s: %v escapes to heap:\n", linestr(l.n.Pos), l.n)
|
||||||
e.explainPath(root, l)
|
}
|
||||||
|
explanation := e.explainPath(root, l)
|
||||||
|
if logopt.Enabled() {
|
||||||
|
logopt.LogOpt(l.n.Pos, "escape", "escape", e.curfn.funcname(), fmt.Sprintf("%v escapes to heap", l.n), explanation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
l.escapes = true
|
l.escapes = true
|
||||||
enqueue(l)
|
enqueue(l)
|
||||||
|
|
@ -1225,43 +1243,67 @@ func (e *Escape) walkOne(root *EscLocation, walkgen uint32, enqueue func(*EscLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
// explainPath prints an explanation of how src flows to the walk root.
|
// explainPath prints an explanation of how src flows to the walk root.
|
||||||
func (e *Escape) explainPath(root, src *EscLocation) {
|
func (e *Escape) explainPath(root, src *EscLocation) []*logopt.LoggedOpt {
|
||||||
visited := make(map[*EscLocation]bool)
|
visited := make(map[*EscLocation]bool)
|
||||||
|
|
||||||
pos := linestr(src.n.Pos)
|
pos := linestr(src.n.Pos)
|
||||||
|
var explanation []*logopt.LoggedOpt
|
||||||
for {
|
for {
|
||||||
// Prevent infinite loop.
|
// Prevent infinite loop.
|
||||||
if visited[src] {
|
if visited[src] {
|
||||||
|
if Debug['m'] >= 2 {
|
||||||
fmt.Printf("%s: warning: truncated explanation due to assignment cycle; see golang.org/issue/35518\n", pos)
|
fmt.Printf("%s: warning: truncated explanation due to assignment cycle; see golang.org/issue/35518\n", pos)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
visited[src] = true
|
visited[src] = true
|
||||||
|
|
||||||
dst := src.dst
|
dst := src.dst
|
||||||
edge := &dst.edges[src.dstEdgeIdx]
|
edge := &dst.edges[src.dstEdgeIdx]
|
||||||
if edge.src != src {
|
if edge.src != src {
|
||||||
Fatalf("path inconsistency: %v != %v", edge.src, src)
|
Fatalf("path inconsistency: %v != %v", edge.src, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.explainFlow(pos, dst, src, edge.derefs, edge.notes)
|
explanation = e.explainFlow(pos, dst, src, edge.derefs, edge.notes, explanation)
|
||||||
|
|
||||||
if dst == root {
|
if dst == root {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
src = dst
|
src = dst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return explanation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Escape) explainFlow(pos string, dst, src *EscLocation, derefs int, notes *EscNote) {
|
func (e *Escape) explainFlow(pos string, dst, srcloc *EscLocation, derefs int, notes *EscNote, explanation []*logopt.LoggedOpt) []*logopt.LoggedOpt {
|
||||||
ops := "&"
|
ops := "&"
|
||||||
if derefs >= 0 {
|
if derefs >= 0 {
|
||||||
ops = strings.Repeat("*", derefs)
|
ops = strings.Repeat("*", derefs)
|
||||||
}
|
}
|
||||||
|
print := Debug['m'] >= 2
|
||||||
|
|
||||||
|
flow := fmt.Sprintf(" flow: %s = %s%v:", e.explainLoc(dst), ops, e.explainLoc(srcloc))
|
||||||
|
if print {
|
||||||
|
fmt.Printf("%s:%s\n", pos, flow)
|
||||||
|
}
|
||||||
|
if logopt.Enabled() {
|
||||||
|
var epos src.XPos
|
||||||
|
if notes != nil {
|
||||||
|
epos = notes.where.Pos
|
||||||
|
} else if srcloc != nil && srcloc.n != nil {
|
||||||
|
epos = srcloc.n.Pos
|
||||||
|
}
|
||||||
|
explanation = append(explanation, logopt.NewLoggedOpt(epos, "escflow", "escape", e.curfn.funcname(), flow))
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("%s: flow: %s = %s%v:\n", pos, e.explainLoc(dst), ops, e.explainLoc(src))
|
|
||||||
for note := notes; note != nil; note = note.next {
|
for note := notes; note != nil; note = note.next {
|
||||||
|
if print {
|
||||||
fmt.Printf("%s: from %v (%v) at %s\n", pos, note.where, note.why, linestr(note.where.Pos))
|
fmt.Printf("%s: from %v (%v) at %s\n", pos, note.where, note.why, linestr(note.where.Pos))
|
||||||
}
|
}
|
||||||
|
if logopt.Enabled() {
|
||||||
|
explanation = append(explanation, logopt.NewLoggedOpt(note.where.Pos, "escflow", "escape", e.curfn.funcname(),
|
||||||
|
fmt.Sprintf(" from %v (%v)", note.where, note.why)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return explanation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Escape) explainLoc(l *EscLocation) string {
|
func (e *Escape) explainLoc(l *EscLocation) string {
|
||||||
|
|
|
||||||
|
|
@ -294,18 +294,23 @@ func checkLogPath(flag, destination string) {
|
||||||
dest = destination
|
dest = destination
|
||||||
}
|
}
|
||||||
|
|
||||||
var loggedOpts []LoggedOpt
|
var loggedOpts []*LoggedOpt
|
||||||
var mu = sync.Mutex{} // mu protects loggedOpts.
|
var mu = sync.Mutex{} // mu protects loggedOpts.
|
||||||
|
|
||||||
|
func NewLoggedOpt(pos src.XPos, what, pass, fname string, args ...interface{}) *LoggedOpt {
|
||||||
|
pass = strings.Replace(pass, " ", "_", -1)
|
||||||
|
return &LoggedOpt{pos, pass, fname, what, args}
|
||||||
|
}
|
||||||
|
|
||||||
func LogOpt(pos src.XPos, what, pass, fname string, args ...interface{}) {
|
func LogOpt(pos src.XPos, what, pass, fname string, args ...interface{}) {
|
||||||
if Format == None {
|
if Format == None {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pass = strings.Replace(pass, " ", "_", -1)
|
lo := NewLoggedOpt(pos, what, pass, fname, args...)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
// Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
|
// Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
|
||||||
loggedOpts = append(loggedOpts, LoggedOpt{pos, pass, fname, what, args})
|
loggedOpts = append(loggedOpts, lo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Enabled() bool {
|
func Enabled() bool {
|
||||||
|
|
@ -321,7 +326,7 @@ func Enabled() bool {
|
||||||
// byPos sorts diagnostics by source position.
|
// byPos sorts diagnostics by source position.
|
||||||
type byPos struct {
|
type byPos struct {
|
||||||
ctxt *obj.Link
|
ctxt *obj.Link
|
||||||
a []LoggedOpt
|
a []*LoggedOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x byPos) Len() int { return len(x.a) }
|
func (x byPos) Len() int { return len(x.a) }
|
||||||
|
|
@ -402,15 +407,9 @@ func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
|
||||||
// For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory.
|
// For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory.
|
||||||
currentFile := ""
|
currentFile := ""
|
||||||
for _, x := range loggedOpts {
|
for _, x := range loggedOpts {
|
||||||
posTmp = ctxt.AllPos(x.pos, posTmp)
|
posTmp, p0 := x.parsePos(ctxt, posTmp)
|
||||||
// Reverse posTmp to put outermost first.
|
|
||||||
l := len(posTmp)
|
|
||||||
for i := 0; i < l/2; i++ {
|
|
||||||
posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
p0 := posTmp[0]
|
|
||||||
p0f := uprootedPath(p0.Filename())
|
p0f := uprootedPath(p0.Filename())
|
||||||
|
|
||||||
if currentFile != p0f {
|
if currentFile != p0f {
|
||||||
if w != nil {
|
if w != nil {
|
||||||
w.Close()
|
w.Close()
|
||||||
|
|
@ -429,16 +428,27 @@ func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
|
||||||
|
|
||||||
diagnostic.Code = x.what
|
diagnostic.Code = x.what
|
||||||
diagnostic.Message = target
|
diagnostic.Message = target
|
||||||
diagnostic.Range = Range{Start: Position{p0.Line(), p0.Col()},
|
diagnostic.Range = newPointRange(p0)
|
||||||
End: Position{p0.Line(), p0.Col()}}
|
|
||||||
diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]
|
diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]
|
||||||
|
|
||||||
for i := 1; i < l; i++ {
|
appendInlinedPos(posTmp, &diagnostic)
|
||||||
p := posTmp[i]
|
|
||||||
loc := Location{URI: uriIfy(uprootedPath(p.Filename())),
|
// Diagnostic explanation is stored in RelatedInformation after inlining info
|
||||||
Range: Range{Start: Position{p.Line(), p.Col()},
|
if len(x.target) > 1 {
|
||||||
End: Position{p.Line(), p.Col()}}}
|
switch y := x.target[1].(type) {
|
||||||
diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
|
case []*LoggedOpt:
|
||||||
|
for _, z := range y {
|
||||||
|
posTmp, p0 := z.parsePos(ctxt, posTmp)
|
||||||
|
loc := newLocation(p0)
|
||||||
|
msg := z.what
|
||||||
|
if len(z.target) > 0 {
|
||||||
|
msg = msg + ": " + fmt.Sprint(z.target[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
|
||||||
|
appendInlinedPos(posTmp, &diagnostic)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder.Encode(diagnostic)
|
encoder.Encode(diagnostic)
|
||||||
|
|
@ -448,3 +458,33 @@ func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newPointRange(p src.Pos) Range {
|
||||||
|
return Range{Start: Position{p.Line(), p.Col()},
|
||||||
|
End: Position{p.Line(), p.Col()}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocation(p src.Pos) Location {
|
||||||
|
loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)}
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendInlinedPos extracts inlining information from posTmp and append it to diagnostic
|
||||||
|
func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) {
|
||||||
|
for i := 1; i < len(posTmp); i++ {
|
||||||
|
p := posTmp[i]
|
||||||
|
loc := newLocation(p)
|
||||||
|
diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) {
|
||||||
|
posTmp = ctxt.AllPos(x.pos, posTmp)
|
||||||
|
// Reverse posTmp to put outermost first.
|
||||||
|
l := len(posTmp)
|
||||||
|
for i := 0; i < l/2; i++ {
|
||||||
|
posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
|
||||||
|
}
|
||||||
|
p0 := posTmp[0]
|
||||||
|
return posTmp, p0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,11 @@ func n() int {
|
||||||
`
|
`
|
||||||
|
|
||||||
func want(t *testing.T, out string, desired string) {
|
func want(t *testing.T, out string, desired string) {
|
||||||
if !strings.Contains(out, desired) {
|
// On Windows, Unicode escapes in the JSON output end up "normalized" elsewhere to /u....,
|
||||||
t.Errorf("did not see phrase %s in \n%s", desired, out)
|
// so "normalize" what we're looking for to match that.
|
||||||
|
s := strings.ReplaceAll(desired, string(os.PathSeparator), "/")
|
||||||
|
if !strings.Contains(out, s) {
|
||||||
|
t.Errorf("did not see phrase %s in \n%s", s, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,7 +181,20 @@ func s15a8(x *[15]int64) [15]int64 {
|
||||||
want(t, slogged, `{"range":{"start":{"line":11,"character":6},"end":{"line":11,"character":6}},"severity":3,"code":"isInBounds","source":"go compiler","message":""}`)
|
want(t, slogged, `{"range":{"start":{"line":11,"character":6},"end":{"line":11,"character":6}},"severity":3,"code":"isInBounds","source":"go compiler","message":""}`)
|
||||||
want(t, slogged, `{"range":{"start":{"line":7,"character":6},"end":{"line":7,"character":6}},"severity":3,"code":"canInlineFunction","source":"go compiler","message":"cost: 35"}`)
|
want(t, slogged, `{"range":{"start":{"line":7,"character":6},"end":{"line":7,"character":6}},"severity":3,"code":"canInlineFunction","source":"go compiler","message":"cost: 35"}`)
|
||||||
want(t, slogged, `{"range":{"start":{"line":21,"character":21},"end":{"line":21,"character":21}},"severity":3,"code":"cannotInlineCall","source":"go compiler","message":"foo cannot be inlined (escaping closure variable)"}`)
|
want(t, slogged, `{"range":{"start":{"line":21,"character":21},"end":{"line":21,"character":21}},"severity":3,"code":"cannotInlineCall","source":"go compiler","message":"foo cannot be inlined (escaping closure variable)"}`)
|
||||||
|
// escape analysis explanation
|
||||||
|
want(t, slogged, `{"range":{"start":{"line":7,"character":13},"end":{"line":7,"character":13}},"severity":3,"code":"leak","source":"go compiler","message":"parameter z leaks to ~r2 with derefs=0",`+
|
||||||
|
`"relatedInformation":[`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: y = z:"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y = \u003cN\u003e (assign-pair)"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: ~r1 = y:"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y.b (dot of pointer)"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from \u0026y.b (address-of)"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":9},"end":{"line":4,"character":9}}},"message":"inlineLoc"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from ~r1 = \u003cN\u003e (assign-pair)"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: flow: ~r2 = ~r1:"},`+
|
||||||
|
`{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: from return (*int)(~r1) (return)"}]}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue