src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }