|
28 | 28 | import com.google.common.base.Preconditions; |
29 | 29 | import com.google.errorprone.VisitorState; |
30 | 30 | import com.google.errorprone.util.ASTHelpers; |
| 31 | +import com.sun.source.tree.ClassTree; |
| 32 | +import com.sun.source.tree.ConditionalExpressionTree; |
| 33 | +import com.sun.source.tree.ExpressionTree; |
| 34 | +import com.sun.source.tree.LambdaExpressionTree; |
31 | 35 | import com.sun.source.tree.MethodTree; |
| 36 | +import com.sun.source.tree.ParenthesizedTree; |
32 | 37 | import com.sun.source.tree.ReturnTree; |
33 | 38 | import com.sun.source.util.TreePath; |
34 | 39 | import com.sun.source.util.TreePathScanner; |
|
37 | 42 | import com.uber.nullaway.ErrorMessage; |
38 | 43 | import com.uber.nullaway.NullAway; |
39 | 44 | import com.uber.nullaway.Nullness; |
| 45 | +import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis; |
40 | 46 | import com.uber.nullaway.handlers.Handler; |
41 | 47 | import com.uber.nullaway.handlers.MethodAnalysisContext; |
| 48 | +import java.util.ArrayList; |
| 49 | +import java.util.List; |
42 | 50 | import java.util.Set; |
43 | 51 | import org.jspecify.annotations.Nullable; |
44 | 52 |
|
@@ -136,68 +144,130 @@ public void onMatchMethod(MethodTree tree, MethodAnalysisContext methodAnalysisC |
136 | 144 |
|
137 | 145 | // we scan the method tree for the return nodes and check the contract |
138 | 146 | new TreePathScanner<@Nullable Void, @Nullable Void>() { |
139 | | - @Override |
140 | | - public @Nullable Void visitReturn(ReturnTree returnTree, @Nullable Void unused) { |
141 | | - |
142 | | - VisitorState returnState = state.withPath(getCurrentPath()); |
143 | | - Nullness nullness = |
144 | | - analysis |
145 | | - .getNullnessAnalysis(returnState) |
146 | | - .getNullnessForContractDataflow( |
147 | | - new TreePath(returnState.getPath(), returnTree.getExpression()), |
148 | | - returnState.context); |
149 | 147 |
|
150 | | - if (nullness == Nullness.NULLABLE || nullness == Nullness.NULL) { |
| 148 | + @Override |
| 149 | + public @Nullable Void visitLambdaExpression( |
| 150 | + LambdaExpressionTree node, @Nullable Void unused) { |
| 151 | + // do not scan into lambdas |
| 152 | + return null; |
| 153 | + } |
151 | 154 |
|
152 | | - String errorMessage; |
| 155 | + @Override |
| 156 | + public @Nullable Void visitClass(ClassTree node, @Nullable Void unused) { |
| 157 | + // do not scan into local/anonymous classes |
| 158 | + return null; |
| 159 | + } |
153 | 160 |
|
154 | | - // used for error message |
155 | | - int nonNullAntecedentCount = 0; |
156 | | - int nonNullAntecedentPosition = -1; |
| 161 | + @Override |
| 162 | + public @Nullable Void visitReturn(ReturnTree returnTree, @Nullable Void unused) { |
157 | 163 |
|
158 | | - for (int i = 0; i < antecedent.length; ++i) { |
159 | | - String valueConstraint = antecedent[i].trim(); |
| 164 | + ExpressionTree returnExpression = returnTree.getExpression(); |
| 165 | + if (returnExpression == null) { |
| 166 | + // this should only be possible with an invalid @Contract on a void-returning method |
| 167 | + return null; |
| 168 | + } |
| 169 | + TreePath returnExpressionPath = new TreePath(getCurrentPath(), returnExpression); |
| 170 | + AccessPathNullnessAnalysis nullnessAnalysis = analysis.getNullnessAnalysis(state); |
| 171 | + List<TreePath> allPossiblyReturnedExpressions = new ArrayList<>(); |
| 172 | + collectNestedReturnedExpressions(returnExpressionPath, allPossiblyReturnedExpressions); |
160 | 173 |
|
161 | | - if (valueConstraint.equals("!null")) { |
162 | | - nonNullAntecedentCount += 1; |
163 | | - nonNullAntecedentPosition = i; |
| 174 | + boolean contractViolated = false; |
| 175 | + for (TreePath expressionPath : allPossiblyReturnedExpressions) { |
| 176 | + Nullness nullness = |
| 177 | + nullnessAnalysis.getNullnessForContractDataflow(expressionPath, state.context); |
| 178 | + if (nullness == Nullness.NULLABLE || nullness == Nullness.NULL) { |
| 179 | + if (nullnessAnalysis.hasBottomAccessPathForContractDataflow( |
| 180 | + expressionPath, state.context)) { |
| 181 | + // if any access path is mapped to bottom, this branch is unreachable |
| 182 | + continue; |
164 | 183 | } |
| 184 | + contractViolated = true; |
| 185 | + break; |
165 | 186 | } |
| 187 | + } |
166 | 188 |
|
167 | | - if (nonNullAntecedentCount == 1) { |
168 | | - |
169 | | - errorMessage = |
170 | | - "Method " |
171 | | - + callee.name |
172 | | - + " has @Contract(" |
173 | | - + contractString |
174 | | - + "), but this appears to be violated, as a @Nullable value may be returned when parameter " |
175 | | - + tree.getParameters().get(nonNullAntecedentPosition).getName() |
176 | | - + " is non-null."; |
177 | | - } else { |
178 | | - errorMessage = |
179 | | - "Method " |
180 | | - + callee.name |
181 | | - + " has @Contract(" |
182 | | - + contractString |
183 | | - + "), but this appears to be violated, as a @Nullable value may be returned " |
184 | | - + "when the contract preconditions are true."; |
185 | | - } |
| 189 | + if (contractViolated) { |
| 190 | + String errorMessage = |
| 191 | + getErrorMessageForViolatedContract(antecedent, callee, contractString, tree); |
186 | 192 |
|
187 | | - returnState.reportMatch( |
| 193 | + state.reportMatch( |
188 | 194 | analysis |
189 | 195 | .getErrorBuilder() |
190 | 196 | .createErrorDescription( |
191 | 197 | new ErrorMessage( |
192 | 198 | ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, errorMessage), |
193 | 199 | returnTree, |
194 | 200 | analysis.buildDescription(returnTree), |
195 | | - returnState, |
| 201 | + state, |
196 | 202 | null)); |
197 | 203 | } |
198 | 204 | return super.visitReturn(returnTree, null); |
199 | 205 | } |
200 | 206 | }.scan(state.getPath(), null); |
201 | 207 | } |
202 | 208 | } |
| 209 | + |
| 210 | + private static String getErrorMessageForViolatedContract( |
| 211 | + String[] antecedent, Symbol.MethodSymbol callee, String contractString, MethodTree tree) { |
| 212 | + String errorMessage; |
| 213 | + |
| 214 | + // used for error message |
| 215 | + int nonNullAntecedentCount = 0; |
| 216 | + int nonNullAntecedentPosition = -1; |
| 217 | + |
| 218 | + for (int i = 0; i < antecedent.length; ++i) { |
| 219 | + String valueConstraint = antecedent[i].trim(); |
| 220 | + |
| 221 | + if (valueConstraint.equals("!null")) { |
| 222 | + nonNullAntecedentCount += 1; |
| 223 | + nonNullAntecedentPosition = i; |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + if (nonNullAntecedentCount == 1) { |
| 228 | + errorMessage = |
| 229 | + "Method " |
| 230 | + + callee.name |
| 231 | + + " has @Contract(" |
| 232 | + + contractString |
| 233 | + + "), but this appears to be violated, as a @Nullable value may be returned when parameter " |
| 234 | + + tree.getParameters().get(nonNullAntecedentPosition).getName() |
| 235 | + + " is non-null."; |
| 236 | + } else { |
| 237 | + errorMessage = |
| 238 | + "Method " |
| 239 | + + callee.name |
| 240 | + + " has @Contract(" |
| 241 | + + contractString |
| 242 | + + "), but this appears to be violated, as a @Nullable value may be returned " |
| 243 | + + "when the contract preconditions are true."; |
| 244 | + } |
| 245 | + return errorMessage; |
| 246 | + } |
| 247 | + |
| 248 | + /** |
| 249 | + * Collect {@code TreePath}s to all nested expressions that may be returned, recursing through |
| 250 | + * parenthesized expressions and conditional expressions. |
| 251 | + * |
| 252 | + * @param expressionPath the TreePath to an expression being returned |
| 253 | + * @param output output parameter list to collect nested returned expression TreePaths |
| 254 | + */ |
| 255 | + private static void collectNestedReturnedExpressions( |
| 256 | + TreePath expressionPath, List<TreePath> output) { |
| 257 | + ExpressionTree expression = (ExpressionTree) expressionPath.getLeaf(); |
| 258 | + while (expression instanceof ParenthesizedTree) { |
| 259 | + ExpressionTree nestedExpression = ((ParenthesizedTree) expression).getExpression(); |
| 260 | + expressionPath = new TreePath(expressionPath, nestedExpression); |
| 261 | + expression = nestedExpression; |
| 262 | + } |
| 263 | + if (expression instanceof ConditionalExpressionTree) { |
| 264 | + ConditionalExpressionTree conditionalExpression = (ConditionalExpressionTree) expression; |
| 265 | + TreePath truePath = new TreePath(expressionPath, conditionalExpression.getTrueExpression()); |
| 266 | + TreePath falsePath = new TreePath(expressionPath, conditionalExpression.getFalseExpression()); |
| 267 | + collectNestedReturnedExpressions(truePath, output); |
| 268 | + collectNestedReturnedExpressions(falsePath, output); |
| 269 | + return; |
| 270 | + } |
| 271 | + output.add(expressionPath); |
| 272 | + } |
203 | 273 | } |
0 commit comments