golang.org/x/tools/gopls@v0.15.3/internal/golang/invertifcondition.go (about)

     1  // Copyright 2023 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 golang
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/token"
    11  	"go/types"
    12  	"strings"
    13  
    14  	"golang.org/x/tools/go/analysis"
    15  	"golang.org/x/tools/go/ast/astutil"
    16  	"golang.org/x/tools/gopls/internal/util/safetoken"
    17  )
    18  
    19  // invertIfCondition is a singleFileFixFunc that inverts an if/else statement
    20  func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
    21  	ifStatement, _, err := CanInvertIfCondition(file, start, end)
    22  	if err != nil {
    23  		return nil, nil, err
    24  	}
    25  
    26  	var replaceElse analysis.TextEdit
    27  
    28  	endsWithReturn, err := endsWithReturn(ifStatement.Else)
    29  	if err != nil {
    30  		return nil, nil, err
    31  	}
    32  
    33  	if endsWithReturn {
    34  		// Replace the whole else part with an empty line and an unindented
    35  		// version of the original if body
    36  		sourcePos := safetoken.StartPosition(fset, ifStatement.Pos())
    37  
    38  		indent := sourcePos.Column - 1
    39  		if indent < 0 {
    40  			indent = 0
    41  		}
    42  
    43  		standaloneBodyText := ifBodyToStandaloneCode(fset, ifStatement.Body, src)
    44  		replaceElse = analysis.TextEdit{
    45  			Pos:     ifStatement.Body.Rbrace + 1, // 1 == len("}")
    46  			End:     ifStatement.End(),
    47  			NewText: []byte("\n\n" + strings.Repeat("\t", indent) + standaloneBodyText),
    48  		}
    49  	} else {
    50  		// Replace the else body text with the if body text
    51  		bodyStart := safetoken.StartPosition(fset, ifStatement.Body.Lbrace)
    52  		bodyEnd := safetoken.EndPosition(fset, ifStatement.Body.Rbrace+1) // 1 == len("}")
    53  		bodyText := src[bodyStart.Offset:bodyEnd.Offset]
    54  		replaceElse = analysis.TextEdit{
    55  			Pos:     ifStatement.Else.Pos(),
    56  			End:     ifStatement.Else.End(),
    57  			NewText: bodyText,
    58  		}
    59  	}
    60  
    61  	// Replace the if text with the else text
    62  	elsePosInSource := safetoken.StartPosition(fset, ifStatement.Else.Pos())
    63  	elseEndInSource := safetoken.EndPosition(fset, ifStatement.Else.End())
    64  	elseText := src[elsePosInSource.Offset:elseEndInSource.Offset]
    65  	replaceBodyWithElse := analysis.TextEdit{
    66  		Pos:     ifStatement.Body.Pos(),
    67  		End:     ifStatement.Body.End(),
    68  		NewText: elseText,
    69  	}
    70  
    71  	// Replace the if condition with its inverse
    72  	inverseCondition, err := invertCondition(fset, ifStatement.Cond, src)
    73  	if err != nil {
    74  		return nil, nil, err
    75  	}
    76  	replaceConditionWithInverse := analysis.TextEdit{
    77  		Pos:     ifStatement.Cond.Pos(),
    78  		End:     ifStatement.Cond.End(),
    79  		NewText: inverseCondition,
    80  	}
    81  
    82  	// Return a SuggestedFix with just that TextEdit in there
    83  	return fset, &analysis.SuggestedFix{
    84  		TextEdits: []analysis.TextEdit{
    85  			replaceConditionWithInverse,
    86  			replaceBodyWithElse,
    87  			replaceElse,
    88  		},
    89  	}, nil
    90  }
    91  
    92  func endsWithReturn(elseBranch ast.Stmt) (bool, error) {
    93  	elseBlock, isBlockStatement := elseBranch.(*ast.BlockStmt)
    94  	if !isBlockStatement {
    95  		return false, fmt.Errorf("unable to figure out whether this ends with return: %T", elseBranch)
    96  	}
    97  
    98  	if len(elseBlock.List) == 0 {
    99  		// Empty blocks don't end in returns
   100  		return false, nil
   101  	}
   102  
   103  	lastStatement := elseBlock.List[len(elseBlock.List)-1]
   104  
   105  	_, lastStatementIsReturn := lastStatement.(*ast.ReturnStmt)
   106  	return lastStatementIsReturn, nil
   107  }
   108  
   109  // Turn { fmt.Println("Hello") } into just fmt.Println("Hello"), with one less
   110  // level of indentation.
   111  //
   112  // The first line of the result will not be indented, but all of the following
   113  // lines will.
   114  func ifBodyToStandaloneCode(fset *token.FileSet, ifBody *ast.BlockStmt, src []byte) string {
   115  	// Get the whole body (without the surrounding braces) as a string
   116  	bodyStart := safetoken.StartPosition(fset, ifBody.Lbrace+1) // 1 == len("}")
   117  	bodyEnd := safetoken.EndPosition(fset, ifBody.Rbrace)
   118  	bodyWithoutBraces := string(src[bodyStart.Offset:bodyEnd.Offset])
   119  	bodyWithoutBraces = strings.TrimSpace(bodyWithoutBraces)
   120  
   121  	// Unindent
   122  	bodyWithoutBraces = strings.ReplaceAll(bodyWithoutBraces, "\n\t", "\n")
   123  
   124  	return bodyWithoutBraces
   125  }
   126  
   127  func invertCondition(fset *token.FileSet, cond ast.Expr, src []byte) ([]byte, error) {
   128  	condStart := safetoken.StartPosition(fset, cond.Pos())
   129  	condEnd := safetoken.EndPosition(fset, cond.End())
   130  	oldText := string(src[condStart.Offset:condEnd.Offset])
   131  
   132  	switch expr := cond.(type) {
   133  	case *ast.Ident, *ast.ParenExpr, *ast.CallExpr, *ast.StarExpr, *ast.IndexExpr, *ast.IndexListExpr, *ast.SelectorExpr:
   134  		newText := "!" + oldText
   135  		if oldText == "true" {
   136  			newText = "false"
   137  		} else if oldText == "false" {
   138  			newText = "true"
   139  		}
   140  
   141  		return []byte(newText), nil
   142  
   143  	case *ast.UnaryExpr:
   144  		if expr.Op != token.NOT {
   145  			// This should never happen
   146  			return dumbInvert(fset, cond, src), nil
   147  		}
   148  
   149  		inverse := expr.X
   150  		if p, isParen := inverse.(*ast.ParenExpr); isParen {
   151  			// We got !(x), remove the parentheses with the ! so we get just "x"
   152  			inverse = p.X
   153  
   154  			start := safetoken.StartPosition(fset, inverse.Pos())
   155  			end := safetoken.EndPosition(fset, inverse.End())
   156  			if start.Line != end.Line {
   157  				// The expression is multi-line, so we can't remove the parentheses
   158  				inverse = expr.X
   159  			}
   160  		}
   161  
   162  		start := safetoken.StartPosition(fset, inverse.Pos())
   163  		end := safetoken.EndPosition(fset, inverse.End())
   164  		textWithoutNot := src[start.Offset:end.Offset]
   165  
   166  		return textWithoutNot, nil
   167  
   168  	case *ast.BinaryExpr:
   169  		// These inversions are unsound for floating point NaN, but that's ok.
   170  		negations := map[token.Token]string{
   171  			token.EQL: "!=",
   172  			token.LSS: ">=",
   173  			token.GTR: "<=",
   174  			token.NEQ: "==",
   175  			token.LEQ: ">",
   176  			token.GEQ: "<",
   177  		}
   178  
   179  		negation, negationFound := negations[expr.Op]
   180  		if !negationFound {
   181  			return invertAndOr(fset, expr, src)
   182  		}
   183  
   184  		xPosInSource := safetoken.StartPosition(fset, expr.X.Pos())
   185  		opPosInSource := safetoken.StartPosition(fset, expr.OpPos)
   186  		yPosInSource := safetoken.StartPosition(fset, expr.Y.Pos())
   187  
   188  		textBeforeOp := string(src[xPosInSource.Offset:opPosInSource.Offset])
   189  
   190  		oldOpWithTrailingWhitespace := string(src[opPosInSource.Offset:yPosInSource.Offset])
   191  		newOpWithTrailingWhitespace := negation + oldOpWithTrailingWhitespace[len(expr.Op.String()):]
   192  
   193  		textAfterOp := string(src[yPosInSource.Offset:condEnd.Offset])
   194  
   195  		return []byte(textBeforeOp + newOpWithTrailingWhitespace + textAfterOp), nil
   196  	}
   197  
   198  	return dumbInvert(fset, cond, src), nil
   199  }
   200  
   201  // dumbInvert is a fallback, inverting cond into !(cond).
   202  func dumbInvert(fset *token.FileSet, expr ast.Expr, src []byte) []byte {
   203  	start := safetoken.StartPosition(fset, expr.Pos())
   204  	end := safetoken.EndPosition(fset, expr.End())
   205  	text := string(src[start.Offset:end.Offset])
   206  	return []byte("!(" + text + ")")
   207  }
   208  
   209  func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte, error) {
   210  	if expr.Op != token.LAND && expr.Op != token.LOR {
   211  		// Neither AND nor OR, don't know how to invert this
   212  		return dumbInvert(fset, expr, src), nil
   213  	}
   214  
   215  	oppositeOp := "&&"
   216  	if expr.Op == token.LAND {
   217  		oppositeOp = "||"
   218  	}
   219  
   220  	xEndInSource := safetoken.EndPosition(fset, expr.X.End())
   221  	opPosInSource := safetoken.StartPosition(fset, expr.OpPos)
   222  	whitespaceAfterBefore := src[xEndInSource.Offset:opPosInSource.Offset]
   223  
   224  	invertedBefore, err := invertCondition(fset, expr.X, src)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  
   229  	invertedAfter, err := invertCondition(fset, expr.Y, src)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	yPosInSource := safetoken.StartPosition(fset, expr.Y.Pos())
   235  
   236  	oldOpWithTrailingWhitespace := string(src[opPosInSource.Offset:yPosInSource.Offset])
   237  	newOpWithTrailingWhitespace := oppositeOp + oldOpWithTrailingWhitespace[len(expr.Op.String()):]
   238  
   239  	return []byte(string(invertedBefore) + string(whitespaceAfterBefore) + newOpWithTrailingWhitespace + string(invertedAfter)), nil
   240  }
   241  
   242  // CanInvertIfCondition reports whether we can do invert-if-condition on the
   243  // code in the given range
   244  func CanInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) {
   245  	path, _ := astutil.PathEnclosingInterval(file, start, end)
   246  	for _, node := range path {
   247  		stmt, isIfStatement := node.(*ast.IfStmt)
   248  		if !isIfStatement {
   249  			continue
   250  		}
   251  
   252  		if stmt.Else == nil {
   253  			// Can't invert conditions without else clauses
   254  			return nil, false, fmt.Errorf("else clause required")
   255  		}
   256  
   257  		if _, hasElseIf := stmt.Else.(*ast.IfStmt); hasElseIf {
   258  			// Can't invert conditions with else-if clauses, unclear what that
   259  			// would look like
   260  			return nil, false, fmt.Errorf("else-if not supported")
   261  		}
   262  
   263  		return stmt, true, nil
   264  	}
   265  
   266  	return nil, false, fmt.Errorf("not an if statement")
   267  }