github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/compiler/analysis/info.go (about)

     1  package analysis
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"go/types"
     8  	"strings"
     9  
    10  	"github.com/gopherjs/gopherjs/compiler/astutil"
    11  	"github.com/gopherjs/gopherjs/compiler/typesutil"
    12  )
    13  
    14  type continueStmt struct {
    15  	forStmt      *ast.ForStmt
    16  	analyzeStack astPath
    17  }
    18  
    19  func newContinueStmt(forStmt *ast.ForStmt, stack astPath) continueStmt {
    20  	cs := continueStmt{
    21  		forStmt:      forStmt,
    22  		analyzeStack: stack.copy(),
    23  	}
    24  	return cs
    25  }
    26  
    27  // astPath is a list of AST nodes where each previous node is a parent of the
    28  // next node.
    29  type astPath []ast.Node
    30  
    31  func (src astPath) copy() astPath {
    32  	dst := make(astPath, len(src))
    33  	copy(dst, src)
    34  	return dst
    35  }
    36  
    37  func (ap astPath) String() string {
    38  	s := &strings.Builder{}
    39  	s.WriteString("[")
    40  	for i, n := range ap {
    41  		if i > 0 {
    42  			s.WriteString(", ")
    43  		}
    44  		fmt.Fprintf(s, "%T(%p)", n, n)
    45  	}
    46  	s.WriteString("]")
    47  	return s.String()
    48  }
    49  
    50  type Info struct {
    51  	*types.Info
    52  	Pkg           *types.Package
    53  	HasPointer    map[*types.Var]bool
    54  	FuncDeclInfos map[*types.Func]*FuncInfo
    55  	FuncLitInfos  map[*ast.FuncLit]*FuncInfo
    56  	InitFuncInfo  *FuncInfo // Context for package variable initialization.
    57  
    58  	isImportedBlocking func(*types.Func) bool // For functions from other packages.
    59  	allInfos           []*FuncInfo
    60  }
    61  
    62  func (info *Info) newFuncInfo(n ast.Node) *FuncInfo {
    63  	funcInfo := &FuncInfo{
    64  		pkgInfo:            info,
    65  		Flattened:          make(map[ast.Node]bool),
    66  		Blocking:           make(map[ast.Node]bool),
    67  		GotoLabel:          make(map[*types.Label]bool),
    68  		localNamedCallees:  make(map[*types.Func][]astPath),
    69  		literalFuncCallees: make(map[*ast.FuncLit][]astPath),
    70  	}
    71  
    72  	// Register the function in the appropriate map.
    73  	switch n := n.(type) {
    74  	case *ast.FuncDecl:
    75  		if n.Body == nil {
    76  			// Function body comes from elsewhere (for example, from a go:linkname
    77  			// directive), conservatively assume that it may be blocking.
    78  			// TODO(nevkontakte): It is possible to improve accuracy of this detection.
    79  			// Since GopherJS supports inly "import-style" go:linkname, at this stage
    80  			// the compiler already determined whether the implementation function is
    81  			// blocking, and we could check that.
    82  			funcInfo.Blocking[n] = true
    83  		}
    84  		info.FuncDeclInfos[info.Defs[n.Name].(*types.Func)] = funcInfo
    85  	case *ast.FuncLit:
    86  		info.FuncLitInfos[n] = funcInfo
    87  	}
    88  
    89  	// And add it to the list of all functions.
    90  	info.allInfos = append(info.allInfos, funcInfo)
    91  
    92  	return funcInfo
    93  }
    94  
    95  func (info *Info) IsBlocking(fun *types.Func) bool {
    96  	if funInfo := info.FuncDeclInfos[fun]; funInfo != nil {
    97  		return len(funInfo.Blocking) > 0
    98  	}
    99  	panic(fmt.Errorf(`info did not have function declaration for %s`, fun.FullName()))
   100  }
   101  
   102  func AnalyzePkg(files []*ast.File, fileSet *token.FileSet, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool) *Info {
   103  	info := &Info{
   104  		Info:               typesInfo,
   105  		Pkg:                typesPkg,
   106  		HasPointer:         make(map[*types.Var]bool),
   107  		isImportedBlocking: isBlocking,
   108  		FuncDeclInfos:      make(map[*types.Func]*FuncInfo),
   109  		FuncLitInfos:       make(map[*ast.FuncLit]*FuncInfo),
   110  	}
   111  	info.InitFuncInfo = info.newFuncInfo(nil)
   112  
   113  	// Traverse the full AST of the package and collect information about existing
   114  	// functions.
   115  	for _, file := range files {
   116  		ast.Walk(info.InitFuncInfo, file)
   117  	}
   118  
   119  	for _, funcInfo := range info.allInfos {
   120  		if !funcInfo.HasDefer {
   121  			continue
   122  		}
   123  		// Conservatively assume that if a function has a deferred call, it might be
   124  		// blocking, and therefore all return statements need to be treated as
   125  		// blocking.
   126  		// TODO(nevkontakte): This could be improved by detecting whether a deferred
   127  		// call is actually blocking. Doing so might reduce generated code size a
   128  		// bit.
   129  		for _, returnStmt := range funcInfo.returnStmts {
   130  			funcInfo.markBlocking(returnStmt)
   131  		}
   132  	}
   133  
   134  	// Propagate information about blocking calls to the caller functions.
   135  	// For each function we check all other functions it may call and if any of
   136  	// them are blocking, we mark the caller blocking as well. The process is
   137  	// repeated until no new blocking functions is detected.
   138  	for {
   139  		done := true
   140  		for _, caller := range info.allInfos {
   141  			// Check calls to named functions and function-typed variables.
   142  			for callee, callSites := range caller.localNamedCallees {
   143  				if info.IsBlocking(callee) {
   144  					for _, callSite := range callSites {
   145  						caller.markBlocking(callSite)
   146  					}
   147  					delete(caller.localNamedCallees, callee)
   148  					done = false
   149  				}
   150  			}
   151  
   152  			// Check direct calls to function literals.
   153  			for callee, callSites := range caller.literalFuncCallees {
   154  				if len(info.FuncLitInfos[callee].Blocking) > 0 {
   155  					for _, callSite := range callSites {
   156  						caller.markBlocking(callSite)
   157  					}
   158  					delete(caller.literalFuncCallees, callee)
   159  					done = false
   160  				}
   161  			}
   162  		}
   163  		if done {
   164  			break
   165  		}
   166  	}
   167  
   168  	// After all function blocking information was propagated, mark flow control
   169  	// statements as blocking whenever they may lead to a blocking function call.
   170  	for _, funcInfo := range info.allInfos {
   171  		for _, continueStmt := range funcInfo.continueStmts {
   172  			if funcInfo.Blocking[continueStmt.forStmt.Post] {
   173  				// If a for-loop post-expression is blocking, the continue statement
   174  				// that leads to it must be treated as blocking.
   175  				funcInfo.markBlocking(continueStmt.analyzeStack)
   176  			}
   177  		}
   178  	}
   179  
   180  	return info
   181  }
   182  
   183  type FuncInfo struct {
   184  	HasDefer bool
   185  	// Nodes are "flattened" into a switch-case statement when we need to be able
   186  	// to jump into an arbitrary position in the code with a GOTO statement, or
   187  	// resume a goroutine after a blocking call unblocks.
   188  	Flattened map[ast.Node]bool
   189  	// Blocking indicates that either the AST node itself or its descendant may
   190  	// block goroutine execution (for example, a channel operation).
   191  	Blocking map[ast.Node]bool
   192  	// GotoLavel indicates a label referenced by a goto statement, rather than a
   193  	// named loop.
   194  	GotoLabel map[*types.Label]bool
   195  	// List of continue statements in the function.
   196  	continueStmts []continueStmt
   197  	// List of return statements in the function.
   198  	returnStmts []astPath
   199  	// List of other named functions from the current package this function calls.
   200  	// If any of them are blocking, this function will become blocking too.
   201  	localNamedCallees map[*types.Func][]astPath
   202  	// List of function literals directly called from this function (for example:
   203  	// `func() { /* do stuff */ }()`). This is distinct from function literals
   204  	// assigned to named variables (for example: `doStuff := func() {};
   205  	// doStuff()`), which are handled by localNamedCallees. If any of them are
   206  	// identified as blocking, this function will become blocking too.
   207  	literalFuncCallees map[*ast.FuncLit][]astPath
   208  
   209  	pkgInfo      *Info // Function's parent package.
   210  	visitorStack astPath
   211  }
   212  
   213  func (fi *FuncInfo) Visit(node ast.Node) ast.Visitor {
   214  	if node == nil {
   215  		if len(fi.visitorStack) != 0 {
   216  			fi.visitorStack = fi.visitorStack[:len(fi.visitorStack)-1]
   217  		}
   218  		return nil
   219  	}
   220  	fi.visitorStack = append(fi.visitorStack, node)
   221  
   222  	switch n := node.(type) {
   223  	case *ast.FuncDecl, *ast.FuncLit:
   224  		// Analyze the function in its own context.
   225  		return fi.pkgInfo.newFuncInfo(n)
   226  	case *ast.BranchStmt:
   227  		switch n.Tok {
   228  		case token.GOTO:
   229  			// Emulating GOTO in JavaScript requires the code to be flattened into a
   230  			// switch-statement.
   231  			fi.markFlattened(fi.visitorStack)
   232  			fi.GotoLabel[fi.pkgInfo.Uses[n.Label].(*types.Label)] = true
   233  		case token.CONTINUE:
   234  			loopStmt := astutil.FindLoopStmt(fi.visitorStack, n, fi.pkgInfo.Info)
   235  			if forStmt, ok := (loopStmt).(*ast.ForStmt); ok {
   236  				// In `for x; y; z { ... }` loops `z` may be potentially blocking
   237  				// and therefore continue expression that triggers it would have to
   238  				// be treated as blocking.
   239  				fi.continueStmts = append(fi.continueStmts, newContinueStmt(forStmt, fi.visitorStack))
   240  			}
   241  		}
   242  		return fi
   243  	case *ast.CallExpr:
   244  		return fi.visitCallExpr(n)
   245  	case *ast.SendStmt:
   246  		// Sending into a channel is blocking.
   247  		fi.markBlocking(fi.visitorStack)
   248  		return fi
   249  	case *ast.UnaryExpr:
   250  		switch n.Op {
   251  		case token.AND:
   252  			if id, ok := astutil.RemoveParens(n.X).(*ast.Ident); ok {
   253  				fi.pkgInfo.HasPointer[fi.pkgInfo.Uses[id].(*types.Var)] = true
   254  			}
   255  		case token.ARROW:
   256  			// Receiving from a channel is blocking.
   257  			fi.markBlocking(fi.visitorStack)
   258  		}
   259  		return fi
   260  	case *ast.RangeStmt:
   261  		if _, ok := fi.pkgInfo.TypeOf(n.X).Underlying().(*types.Chan); ok {
   262  			// for-range loop over a channel is blocking.
   263  			fi.markBlocking(fi.visitorStack)
   264  		}
   265  		return fi
   266  	case *ast.SelectStmt:
   267  		for _, s := range n.Body.List {
   268  			if s.(*ast.CommClause).Comm == nil { // default clause
   269  				return fi
   270  			}
   271  		}
   272  		// Select statements without a default case are blocking.
   273  		fi.markBlocking(fi.visitorStack)
   274  		return fi
   275  	case *ast.CommClause:
   276  		// FIXME(nevkontakte): Does this need to be manually spelled out? Presumably
   277  		// ast.Walk would visit all those nodes anyway, and we are not creating any
   278  		// new contexts here.
   279  		// https://github.com/gopherjs/gopherjs/issues/230 seems to be relevant?
   280  		switch comm := n.Comm.(type) {
   281  		case *ast.SendStmt:
   282  			ast.Walk(fi, comm.Chan)
   283  			ast.Walk(fi, comm.Value)
   284  		case *ast.ExprStmt:
   285  			ast.Walk(fi, comm.X.(*ast.UnaryExpr).X)
   286  		case *ast.AssignStmt:
   287  			ast.Walk(fi, comm.Rhs[0].(*ast.UnaryExpr).X)
   288  		}
   289  		for _, s := range n.Body {
   290  			ast.Walk(fi, s)
   291  		}
   292  		return nil // The subtree was manually checked, no need to visit it again.
   293  	case *ast.GoStmt:
   294  		// Unlike a regular call, the function in a go statement doesn't block the
   295  		// caller goroutine, but the expression that determines the function and its
   296  		// arguments still need to be checked.
   297  		ast.Walk(fi, n.Call.Fun)
   298  		for _, arg := range n.Call.Args {
   299  			ast.Walk(fi, arg)
   300  		}
   301  		return nil // The subtree was manually checked, no need to visit it again.
   302  	case *ast.DeferStmt:
   303  		fi.HasDefer = true
   304  		if funcLit, ok := n.Call.Fun.(*ast.FuncLit); ok {
   305  			ast.Walk(fi, funcLit.Body)
   306  		}
   307  		return fi
   308  	case *ast.ReturnStmt:
   309  		// Capture all return statements in the function. They could become blocking
   310  		// if the function has a blocking deferred call.
   311  		fi.returnStmts = append(fi.returnStmts, fi.visitorStack.copy())
   312  		return fi
   313  	default:
   314  		return fi
   315  	}
   316  	// Deliberately no return here to make sure that each of the cases above is
   317  	// self-sufficient and explicitly decides in which context the its AST subtree
   318  	// needs to be analyzed.
   319  }
   320  
   321  func (fi *FuncInfo) visitCallExpr(n *ast.CallExpr) ast.Visitor {
   322  	switch f := astutil.RemoveParens(n.Fun).(type) {
   323  	case *ast.Ident:
   324  		fi.callToNamedFunc(fi.pkgInfo.Uses[f])
   325  	case *ast.SelectorExpr:
   326  		if sel := fi.pkgInfo.Selections[f]; sel != nil && typesutil.IsJsObject(sel.Recv()) {
   327  			// js.Object methods are known to be non-blocking, but we still must
   328  			// check its arguments.
   329  		} else {
   330  			fi.callToNamedFunc(fi.pkgInfo.Uses[f.Sel])
   331  		}
   332  	case *ast.FuncLit:
   333  		// Collect info about the function literal itself.
   334  		ast.Walk(fi, n.Fun)
   335  
   336  		// Check all argument expressions.
   337  		for _, arg := range n.Args {
   338  			ast.Walk(fi, arg)
   339  		}
   340  		// Register literal function call site in case it is identified as blocking.
   341  		fi.literalFuncCallees[f] = append(fi.literalFuncCallees[f], fi.visitorStack.copy())
   342  		return nil // No need to walk under this CallExpr, we already did it manually.
   343  	default:
   344  		if astutil.IsTypeExpr(f, fi.pkgInfo.Info) {
   345  			// This is a type conversion, not a call. Type assertion itself is not
   346  			// blocking, but we will visit the input expression.
   347  		} else {
   348  			// The function is returned by a non-trivial expression. We have to be
   349  			// conservative and assume that function might be blocking.
   350  			fi.markBlocking(fi.visitorStack)
   351  		}
   352  	}
   353  
   354  	return fi
   355  }
   356  
   357  func (fi *FuncInfo) callToNamedFunc(callee types.Object) {
   358  	switch o := callee.(type) {
   359  	case *types.Func:
   360  		o = o.Origin()
   361  		if recv := o.Type().(*types.Signature).Recv(); recv != nil {
   362  			if _, ok := recv.Type().Underlying().(*types.Interface); ok {
   363  				// Conservatively assume that an interface implementation may be blocking.
   364  				fi.markBlocking(fi.visitorStack)
   365  				return
   366  			}
   367  		}
   368  		if o.Pkg() != fi.pkgInfo.Pkg {
   369  			if fi.pkgInfo.isImportedBlocking(o) {
   370  				fi.markBlocking(fi.visitorStack)
   371  			}
   372  			return
   373  		}
   374  		// We probably don't know yet whether the callee function is blocking.
   375  		// Record the calls site for the later stage.
   376  		fi.localNamedCallees[o] = append(fi.localNamedCallees[o], fi.visitorStack.copy())
   377  	case *types.Var:
   378  		// Conservatively assume that a function in a variable might be blocking.
   379  		fi.markBlocking(fi.visitorStack)
   380  	}
   381  }
   382  
   383  func (fi *FuncInfo) markBlocking(stack astPath) {
   384  	for _, n := range stack {
   385  		fi.Blocking[n] = true
   386  		fi.Flattened[n] = true
   387  	}
   388  }
   389  
   390  func (fi *FuncInfo) markFlattened(stack astPath) {
   391  	for _, n := range stack {
   392  		fi.Flattened[n] = true
   393  	}
   394  }