1 /* 2 * Copyright (c) 2017 Uber Technologies, Inc. 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 */ 22 23 package com.uber.nullaway.handlers.contract; 24 25 import static com.uber.nullaway.handlers.contract.ContractUtils.getAntecedent; 26 import static com.uber.nullaway.handlers.contract.ContractUtils.getConsequent; 27 28 import com.google.common.base.Preconditions; 29 import com.google.errorprone.VisitorState; 30 import com.google.errorprone.util.ASTHelpers; 31 import com.sun.source.tree.MethodTree; 32 import com.sun.source.tree.ReturnTree; 33 import com.sun.source.util.TreePath; 34 import com.sun.source.util.TreePathScanner; 35 import com.sun.tools.javac.code.Symbol; 36 import com.uber.nullaway.Config; 37 import com.uber.nullaway.ErrorMessage; 38 import com.uber.nullaway.NullAway; 39 import com.uber.nullaway.Nullness; 40 import com.uber.nullaway.handlers.BaseNoOpHandler; 41 42 /** 43 * This Handler parses the jetbrains @Contract annotation and tries to check if the contract is 44 * followed. 45 * 46 * <p>Currently, it supports the case when there is only one clause in the contract. The clause of 47 * the form in which all the elements of the antecedent are either of "_", "null" or "!null", and 48 * the consequent is "!null" is supported. The handler checks and warns under the conditions of the 49 * antecedent if the consequent is "!null" and there is a return statement with "nullable" or "null" 50 * expression. 51 */ 52 public class ContractCheckHandler extends BaseNoOpHandler { 53 54 private final Config config; 55 ContractCheckHandler(Config config)56 public ContractCheckHandler(Config config) { 57 this.config = config; 58 } 59 60 @Override onMatchMethod( NullAway analysis, MethodTree tree, VisitorState state, Symbol.MethodSymbol methodSymbol)61 public void onMatchMethod( 62 NullAway analysis, MethodTree tree, VisitorState state, Symbol.MethodSymbol methodSymbol) { 63 Symbol.MethodSymbol callee = ASTHelpers.getSymbol(tree); 64 Preconditions.checkNotNull(callee); 65 // Check to see if this method has an @Contract annotation 66 String contractString = ContractUtils.getContractString(callee, config); 67 if (contractString != null) { 68 // Found a contract, lets parse it. 69 String[] clauses = contractString.split(";"); 70 if (clauses.length != 1) { 71 return; 72 } 73 74 String clause = clauses[0]; 75 String[] antecedent = 76 getAntecedent(clause, tree, analysis, state, callee, tree.getParameters().size()); 77 String consequent = getConsequent(clause, tree, analysis, state, callee); 78 79 boolean supported = true; 80 81 for (int i = 0; i < antecedent.length; ++i) { 82 String valueConstraint = antecedent[i].trim(); 83 if (!(valueConstraint.equals("_") 84 || valueConstraint.equals("!null") 85 || valueConstraint.equals("null"))) { 86 supported = false; 87 } 88 } 89 90 if (!consequent.equals("!null")) { 91 supported = false; 92 } 93 94 if (!supported) { 95 return; 96 } 97 98 // we scan the method tree for the return nodes and check the contract 99 new TreePathScanner<Void, Void>() { 100 @Override 101 public Void visitReturn(ReturnTree returnTree, Void unused) { 102 103 final VisitorState returnState = state.withPath(getCurrentPath()); 104 final Nullness nullness = 105 analysis 106 .getNullnessAnalysis(returnState) 107 .getNullnessForContractDataflow( 108 new TreePath(returnState.getPath(), returnTree.getExpression()), 109 returnState.context); 110 111 if (nullness == Nullness.NULLABLE || nullness == Nullness.NULL) { 112 113 String errorMessage; 114 115 // used for error message 116 int nonNullAntecedentCount = 0; 117 int nonNullAntecedentPosition = -1; 118 119 for (int i = 0; i < antecedent.length; ++i) { 120 String valueConstraint = antecedent[i].trim(); 121 122 if (valueConstraint.equals("!null")) { 123 nonNullAntecedentCount += 1; 124 nonNullAntecedentPosition = i; 125 } 126 } 127 128 if (nonNullAntecedentCount == 1) { 129 130 errorMessage = 131 "Method " 132 + callee.name 133 + " has @Contract(" 134 + contractString 135 + "), but this appears to be violated, as a @Nullable value may be returned when parameter " 136 + tree.getParameters().get(nonNullAntecedentPosition).getName() 137 + " is non-null."; 138 } else { 139 errorMessage = 140 "Method " 141 + callee.name 142 + " has @Contract(" 143 + contractString 144 + "), but this appears to be violated, as a @Nullable value may be returned " 145 + "when the contract preconditions are true."; 146 } 147 148 returnState.reportMatch( 149 analysis 150 .getErrorBuilder() 151 .createErrorDescription( 152 new ErrorMessage( 153 ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, errorMessage), 154 returnTree, 155 analysis.buildDescription(returnTree), 156 returnState)); 157 } 158 return super.visitReturn(returnTree, unused); 159 } 160 }.scan(state.getPath(), null); 161 } 162 } 163 } 164