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 }