util.go (7921B)
1 // Copyright 2025 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package astutil 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/printer" 11 "go/token" 12 "strings" 13 14 "golang.org/x/tools/go/ast/inspector" 15 "honnef.co/go/tools/internal/xtools-internal/moreiters" 16 ) 17 18 // NodeContains reports whether the Pos/End range of node n encloses 19 // the given range. 20 // 21 // It is inclusive of both end points, to allow hovering (etc) when 22 // the cursor is immediately after a node. 23 // 24 // Like [NodeRange], it treats the range of an [ast.File] as the 25 // file's complete extent. 26 // 27 // Precondition: n must not be nil. 28 func NodeContains(n ast.Node, rng Range) bool { 29 return NodeRange(n).Contains(rng) 30 } 31 32 // NodeContainsPos reports whether the Pos/End range of node n encloses 33 // the given pos. 34 // 35 // Like [NodeRange], it treats the range of an [ast.File] as the 36 // file's complete extent. 37 func NodeContainsPos(n ast.Node, pos token.Pos) bool { 38 return NodeRange(n).ContainsPos(pos) 39 } 40 41 // EnclosingFile returns the syntax tree for the file enclosing c. 42 // 43 // TODO(adonovan): promote this to a method of Cursor. 44 func EnclosingFile(c inspector.Cursor) *ast.File { 45 c, _ = moreiters.First(c.Enclosing((*ast.File)(nil))) 46 return c.Node().(*ast.File) 47 } 48 49 // DocComment returns the doc comment for a node, if any. 50 func DocComment(n ast.Node) *ast.CommentGroup { 51 switch n := n.(type) { 52 case *ast.FuncDecl: 53 return n.Doc 54 case *ast.GenDecl: 55 return n.Doc 56 case *ast.ValueSpec: 57 return n.Doc 58 case *ast.TypeSpec: 59 return n.Doc 60 case *ast.File: 61 return n.Doc 62 case *ast.ImportSpec: 63 return n.Doc 64 case *ast.Field: 65 return n.Doc 66 } 67 return nil 68 } 69 70 // Format returns a string representation of the node n. 71 func Format(fset *token.FileSet, n ast.Node) string { 72 var buf strings.Builder 73 printer.Fprint(&buf, fset, n) // ignore errors 74 return buf.String() 75 } 76 77 // -- Range -- 78 79 // Range is a Pos interval. 80 // It implements [analysis.Range] and [ast.Node]. 81 type Range struct{ Start, EndPos token.Pos } 82 83 // RangeOf constructs a Range. 84 // 85 // RangeOf exists to pacify the "unkeyed literal" (composites) vet 86 // check. It would be nice if there were a way for a type to add 87 // itself to the allowlist. 88 func RangeOf(start, end token.Pos) Range { return Range{start, end} } 89 90 // NodeRange returns the extent of node n as a Range. 91 // 92 // For unfortunate historical reasons, the Pos/End extent of an 93 // ast.File runs from the start of its package declaration---excluding 94 // copyright comments, build tags, and package documentation---to the 95 // end of its last declaration, excluding any trailing comments. So, 96 // as a special case, if n is an [ast.File], NodeContains uses 97 // n.FileStart <= pos && pos <= n.FileEnd to report whether the 98 // position lies anywhere within the file. 99 func NodeRange(n ast.Node) Range { 100 if file, ok := n.(*ast.File); ok { 101 return Range{file.FileStart, file.FileEnd} // entire file 102 } 103 return Range{n.Pos(), n.End()} 104 } 105 106 func (r Range) Pos() token.Pos { return r.Start } 107 func (r Range) End() token.Pos { return r.EndPos } 108 109 // ContainsPos reports whether the range (inclusive of both end points) 110 // includes the specified position. 111 func (r Range) ContainsPos(pos token.Pos) bool { 112 return r.Contains(RangeOf(pos, pos)) 113 } 114 115 // Contains reports whether the range (inclusive of both end points) 116 // includes the specified range. 117 func (r Range) Contains(rng Range) bool { 118 return r.Start <= rng.Start && rng.EndPos <= r.EndPos 119 } 120 121 // IsValid reports whether the range is valid. 122 func (r Range) IsValid() bool { return r.Start.IsValid() && r.Start <= r.EndPos } 123 124 // -- 125 126 // Select returns the syntax nodes identified by a user's text 127 // selection. It returns three nodes: the innermost node that wholly 128 // encloses the selection; and the first and last nodes that are 129 // wholly enclosed by the selection. 130 // 131 // For example, given this selection: 132 // 133 // { f(); g(); /* comment */ } 134 // ~~~~~~~~~~~ 135 // 136 // Select returns the enclosing BlockStmt, the f() CallExpr, and the g() CallExpr. 137 // 138 // If the selection does not wholly enclose any nodes, Select returns an error 139 // and invalid start/end nodes, but it may return a valid enclosing node. 140 // 141 // Callers that require exactly one syntax tree (e.g. just f() or just 142 // g()) should check that the returned start and end nodes are 143 // identical. 144 // 145 // This function is intended to be called early in the handling of a 146 // user's request, since it is tolerant of sloppy selection including 147 // extraneous whitespace and comments. Use it in new code instead of 148 // PathEnclosingInterval. When the exact extent of a node is known, 149 // use [Cursor.FindByPos] instead. 150 // 151 // TODO(hxjiang): Consider refactoring the function signature. It is currently 152 // confusing that an error is returned even when a valid enclosing node is 153 // successfully found. Consider grouping all cursors into one struct. 154 func Select(curFile inspector.Cursor, start, end token.Pos) (_enclosing, _start, _end inspector.Cursor, _ error) { 155 curEnclosing, ok := curFile.FindByPos(start, end) 156 if !ok { 157 return noCursor, noCursor, noCursor, fmt.Errorf("invalid selection") 158 } 159 160 // Find the first and last node wholly within the (start, end) range. 161 // We'll narrow the effective selection to them, to exclude whitespace. 162 // (This matches the functionality of PathEnclosingInterval.) 163 var curStart, curEnd inspector.Cursor 164 rng := RangeOf(start, end) 165 for cur := range curEnclosing.Preorder() { 166 if rng.Contains(NodeRange(cur.Node())) { 167 // The start node has the least Pos. 168 if !curStart.Valid() { 169 curStart = cur 170 } 171 // The end node has the greatest End. 172 // End positions do not change monotonically, 173 // so we must compute the max. 174 if !curEnd.Valid() || 175 cur.Node().End() > curEnd.Node().End() { 176 curEnd = cur 177 } 178 } 179 } 180 if !curStart.Valid() { 181 // The selection is valid (inside curEnclosing) but contains no 182 // complete nodes. This happens for point selections (start == end), 183 // or selections covering only only spaces, comments, and punctuation 184 // tokens. 185 // Return the enclosing node so the caller can still use the context. 186 return curEnclosing, noCursor, noCursor, fmt.Errorf("invalid selection") 187 } 188 return curEnclosing, curStart, curEnd, nil 189 } 190 191 var noCursor inspector.Cursor 192 193 // MaybeParenthesize returns new, possibly wrapped in parens if needed 194 // to preserve operator precedence when it replaces old, whose parent 195 // is parentNode. 196 // 197 // (This would be more naturally written in terms of Cursor, but one of 198 // the callers--the inliner--does not have cursors handy.) 199 func MaybeParenthesize(parentNode ast.Node, old, new ast.Expr) ast.Expr { 200 if needsParens(parentNode, old, new) { 201 new = &ast.ParenExpr{X: new} 202 } 203 return new 204 } 205 206 func needsParens(parentNode ast.Node, old, new ast.Expr) bool { 207 // An expression beneath a non-expression 208 // has no precedence ambiguity. 209 parent, ok := parentNode.(ast.Expr) 210 if !ok { 211 return false 212 } 213 214 precedence := func(n ast.Node) int { 215 switch n := n.(type) { 216 case *ast.UnaryExpr, *ast.StarExpr: 217 return token.UnaryPrec 218 case *ast.BinaryExpr: 219 return n.Op.Precedence() 220 } 221 return -1 222 } 223 224 // Parens are not required if the new node 225 // is not unary or binary. 226 newprec := precedence(new) 227 if newprec < 0 { 228 return false 229 } 230 231 // Parens are required if parent and child are both 232 // unary or binary and the parent has higher precedence. 233 if precedence(parent) > newprec { 234 return true 235 } 236 237 // Was the old node the operand of a postfix operator? 238 // f().sel 239 // f()[i:j] 240 // f()[i] 241 // f().(T) 242 // f()(x) 243 switch parent := parent.(type) { 244 case *ast.SelectorExpr: 245 return parent.X == old 246 case *ast.IndexExpr: 247 return parent.X == old 248 case *ast.SliceExpr: 249 return parent.X == old 250 case *ast.TypeAssertExpr: 251 return parent.X == old 252 case *ast.CallExpr: 253 return parent.Fun == old 254 } 255 return false 256 } 257 258 func is[T any](n any) bool { 259 _, ok := n.(T) 260 return ok 261 }