1   /*
2    * SonarQube Java
3    * Copyright (C) 2012-2019 SonarSource SA
4    * mailto:info AT sonarsource DOT com
5    *
6    * This program is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 3 of the License, or (at your option) any later version.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public License
17   * along with this program; if not, write to the Free Software Foundation,
18   * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19   */
20  package org.sonar.java.se.checks;
21  
22  import com.google.common.base.Preconditions;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Objects;
26  import javax.annotation.Nullable;
27  import org.sonar.check.Rule;
28  import org.sonar.java.matcher.MethodMatcher;
29  import org.sonar.java.matcher.MethodMatcherCollection;
30  import org.sonar.java.se.CheckerContext;
31  import org.sonar.java.se.ProgramState;
32  import org.sonar.java.se.constraint.BooleanConstraint;
33  import org.sonar.java.se.constraint.Constraint;
34  import org.sonar.java.se.constraint.ConstraintManager;
35  import org.sonar.java.se.constraint.ObjectConstraint;
36  import org.sonar.java.se.symbolicvalues.SymbolicValue;
37  import org.sonar.plugins.java.api.semantic.Symbol;
38  import org.sonar.plugins.java.api.tree.ExpressionTree;
39  import org.sonar.plugins.java.api.tree.IdentifierTree;
40  import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
41  import org.sonar.plugins.java.api.tree.MethodInvocationTree;
42  import org.sonar.plugins.java.api.tree.Tree;
43  
44  @Rule(key = "S3655")
45  public class OptionalGetBeforeIsPresentCheck extends SECheck {
46  
47    private static final ExceptionalYieldChecker EXCEPTIONAL_YIELD_CHECKER = new ExceptionalYieldChecker(
48      "\"NoSuchElementException\" will be thrown when invoking method \"%s()\" without verifying Optional parameter.");
49    private static final MethodMatcher OPTIONAL_GET = optionalMethod("get").withoutParameter();
50    private static final MethodMatcher OPTIONAL_ORELSE = optionalMethod("orElse").withAnyParameters();
51    private static final MethodMatcherCollection OPTIONAL_TEST_METHODS = MethodMatcherCollection.create(
52      optionalMethod("isPresent").withoutParameter(),
53      optionalMethod("isEmpty").withoutParameter());
54    private static final MethodMatcher OPTIONAL_EMPTY = optionalMethod("empty").withoutParameter();
55    private static final MethodMatcher OPTIONAL_OF = optionalMethod("of").withAnyParameters();
56    private static final MethodMatcher OPTIONAL_OF_NULLABLE = optionalMethod("ofNullable").withAnyParameters();
57    private static final MethodMatcher OPTIONAL_FILTER = optionalMethod("filter").withAnyParameters();
58  
59    private enum OptionalConstraint implements Constraint {
60      PRESENT, NOT_PRESENT;
61  
62      @Override
63      public boolean isValidWith(@Nullable Constraint constraint) {
64        return constraint == null || this == constraint;
65      }
66  
67      @Override
68      public boolean hasPreciseValue() {
69        return this == NOT_PRESENT;
70      }
71    }
72  
73    private static MethodMatcher optionalMethod(String methodName) {
74      return MethodMatcher.create().typeDefinition("java.util.Optional").name(methodName);
75    }
76  
77    @Override
78    public ProgramState checkPreStatement(CheckerContext context, Tree syntaxNode) {
79      PreStatementVisitor visitor = new PreStatementVisitor(this, context);
80      syntaxNode.accept(visitor);
81      return visitor.programState;
82    }
83  
84    @Override
85    public ProgramState checkPostStatement(CheckerContext context, Tree syntaxNode) {
86      List<ProgramState> programStates = setOptionalConstraint(context, syntaxNode);
87      Preconditions.checkState(programStates.size() == 1);
88      return programStates.get(0);
89    }
90  
91    private static List<ProgramState> setOptionalConstraint(CheckerContext context, Tree syntaxNode) {
92      ProgramState programState = context.getState();
93      if (!syntaxNode.is(Tree.Kind.METHOD_INVOCATION)) {
94        return Collections.singletonList(programState);
95      }
96      MethodInvocationTree mit = (MethodInvocationTree) syntaxNode;
97      SymbolicValue peekValue = programState.peekValue();
98      Objects.requireNonNull(peekValue);
99      if (OPTIONAL_EMPTY.matches(mit)) {
100       return peekValue.setConstraint(programState, OptionalConstraint.NOT_PRESENT);
101     }
102     if (OPTIONAL_OF.matches(mit)) {
103       return peekValue.setConstraint(programState, OptionalConstraint.PRESENT);
104     }
105     if (OPTIONAL_OF_NULLABLE.matches(mit)) {
106       ProgramState psPriorMethodInvocation = context.getNode().programState;
107       SymbolicValue paramSV = psPriorMethodInvocation.peekValue(0);
108       ObjectConstraint paramConstraint = psPriorMethodInvocation.getConstraint(paramSV, ObjectConstraint.class);
109       if (paramConstraint != null) {
110         // Optional.ofNullable(null) returns an empty Optional
111         return peekValue.setConstraint(programState, paramConstraint == ObjectConstraint.NULL ? OptionalConstraint.NOT_PRESENT : OptionalConstraint.PRESENT);
112       }
113     }
114     return Collections.singletonList(programState);
115   }
116 
117   private static class OptionalSymbolicValue extends SymbolicValue {
118     protected final SymbolicValue wrappedValue;
119 
120     private OptionalSymbolicValue(SymbolicValue wrappedValue) {
121       this.wrappedValue = wrappedValue;
122     }
123 
124     @Override
125     public boolean references(SymbolicValue other) {
126       return wrappedValue.equals(other) || wrappedValue.references(other);
127     }
128   }
129 
130   private static class FilteredOptionalSymbolicValue extends OptionalSymbolicValue {
131     private FilteredOptionalSymbolicValue(SymbolicValue wrappedValue) {
132       super(wrappedValue);
133     }
134 
135     @Override
136     public List<ProgramState> setConstraint(ProgramState programState, Constraint constraint) {
137       ProgramState ps = programState;
138       if (constraint == OptionalConstraint.PRESENT) {
139         List<ProgramState> programStates = wrappedValue.setConstraint(ps, constraint);
140         // programStates should always have size 1 here as a FilteredOptionalSymbolicValue is only created on top of SV having
141         // either a PRESENT or no constraint. SV having already NOT_PRESENT constraints do not create a new FilteredOptionalSymbolicValue
142         // since the filtering operation will have no effect.
143         Preconditions.checkState(programStates.size() == 1);
144         ps = programStates.get(0);
145       }
146       return super.setConstraint(ps, constraint);
147     }
148 
149   }
150 
151   /**
152    * Used to wrap symbolic value resulting from invocation of Optional test methods:
153    * - isPresent() (jdk 8)
154    * - isEmpty() (jdk 11)
155    */
156   private static class OptionalTestMethodSymbolicValue extends SymbolicValue {
157 
158     private final SymbolicValue optionalSV;
159     private final boolean isIsEmpty;
160 
161     public OptionalTestMethodSymbolicValue(SymbolicValue sv, Symbol testMethod) {
162       this.optionalSV = sv;
163       this.isIsEmpty = "isEmpty".equals(testMethod.name());
164     }
165 
166     /**
167      * Will be called only after calling Optional.isPresent() or Optional.isEmpty()
168      */
169     @Override
170     public List<ProgramState> setConstraint(ProgramState programState, BooleanConstraint booleanConstraint) {
171       OptionalConstraint optionalConstraint =  programState.getConstraint(optionalSV, OptionalConstraint.class);
172       if (isImpossibleState(booleanConstraint, optionalConstraint)) {
173         return Collections.emptyList();
174       }
175       if (optionalConstraint == OptionalConstraint.NOT_PRESENT || optionalConstraint == OptionalConstraint.PRESENT) {
176         return Collections.singletonList(programState);
177       }
178 
179       return optionalSV.setConstraint(programState, expectedOptionalConstraint(booleanConstraint));
180     }
181 
182     private boolean isImpossibleState(BooleanConstraint booleanConstraint, @Nullable OptionalConstraint optionalConstraint) {
183       return optionalConstraint == expectedOptionalConstraint(booleanConstraint.isTrue() ? BooleanConstraint.FALSE : BooleanConstraint.TRUE);
184     }
185 
186     private OptionalConstraint expectedOptionalConstraint(BooleanConstraint booleanConstraint) {
187       if (booleanConstraint.isTrue()) {
188         return isIsEmpty ? OptionalConstraint.NOT_PRESENT : OptionalConstraint.PRESENT;
189       }
190       return isIsEmpty ? OptionalConstraint.PRESENT : OptionalConstraint.NOT_PRESENT;
191     }
192 
193     @Override
194     public boolean references(SymbolicValue other) {
195       return optionalSV.equals(other) || optionalSV.references(other);
196     }
197   }
198 
199   private static class PreStatementVisitor extends CheckerTreeNodeVisitor {
200 
201     private final CheckerContext context;
202     private final ConstraintManager constraintManager;
203     private final SECheck check;
204 
205     private PreStatementVisitor(SECheck check, CheckerContext context) {
206       super(context.getState());
207       this.context = context;
208       this.constraintManager = context.getConstraintManager();
209       this.check = check;
210     }
211 
212     @Override
213     public void visitMethodInvocation(MethodInvocationTree tree) {
214       SymbolicValue peek = programState.peekValue();
215       if (OPTIONAL_TEST_METHODS.anyMatch(tree)) {
216         constraintManager.setValueFactory(() -> new OptionalTestMethodSymbolicValue(peek, tree.symbol()));
217       } else if (OPTIONAL_GET.matches(tree) && presenceHasNotBeenChecked(peek)) {
218         context.addExceptionalYield(peek, programState, "java.util.NoSuchElementException", check);
219         reportIssue(tree);
220         // continue exploration after reporting, assuming the optional is now present (killing any noise after the initial issue)
221         programState = programState.addConstraint(peek, OptionalConstraint.PRESENT);
222       } else if (OPTIONAL_FILTER.matches(tree)) {
223         // filter has one parameter, so optional is next item on stack
224         SymbolicValue optionalSV = programState.peekValue(1);
225 
226         if (programState.getConstraint(optionalSV, OptionalConstraint.class) == OptionalConstraint.NOT_PRESENT) {
227           // reuse the same optional - filtering a non-present optional is a no-op
228           constraintManager.setValueFactory(() -> optionalSV);
229         } else {
230           constraintManager.setValueFactory(() -> new FilteredOptionalSymbolicValue(optionalSV));
231         }
232       } else if (OPTIONAL_ORELSE.matches(tree)) {
233         ProgramState.Pop pop = programState.unstackValue(2);
234         SymbolicValue orElseValue = pop.values.get(0);
235         SymbolicValue optional = pop.values.get(1);
236         List<ProgramState> psEmpty = optional.setConstraint(pop.state.stackValue(orElseValue), OptionalConstraint.NOT_PRESENT);
237         SymbolicValue symbolicValue;
238         if(optional instanceof OptionalSymbolicValue) {
239           symbolicValue = ((OptionalSymbolicValue) optional).wrappedValue;
240         } else {
241           symbolicValue = constraintManager.createSymbolicValue(tree);
242         }
243         List<ProgramState> psPresent = optional.setConstraint(pop.state.stackValue(symbolicValue), OptionalConstraint.PRESENT);
244         psEmpty.forEach(context::addTransition);
245         psPresent.forEach(context::addTransition);
246         programState = null;
247       } else if (OPTIONAL_OF.matches(tree) || OPTIONAL_OF_NULLABLE.matches(tree)) {
248         constraintManager.setValueFactory(() -> new OptionalSymbolicValue(peek));
249       }
250     }
251 
252     private void reportIssue(MethodInvocationTree mit) {
253       String identifier = getIdentifierPart(mit.methodSelect());
254       String issueMsg = identifier.isEmpty() ? "Optional#" : (identifier + ".");
255       Tree reportTree = mit.methodSelect().is(Tree.Kind.MEMBER_SELECT) ? ((MemberSelectExpressionTree) mit.methodSelect()).expression() : mit;
256       context.reportIssue(reportTree, check, "Call \""+ issueMsg + "isPresent()\" before accessing the value.");
257     }
258 
259     private boolean presenceHasNotBeenChecked(SymbolicValue sv) {
260       return programState.getConstraint(sv, OptionalConstraint.class) != OptionalConstraint.PRESENT;
261     }
262 
263     private static String getIdentifierPart(ExpressionTree methodSelect) {
264       if (methodSelect.is(Tree.Kind.MEMBER_SELECT)) {
265         ExpressionTree expression = ((MemberSelectExpressionTree) methodSelect).expression();
266         if (expression.is(Tree.Kind.IDENTIFIER)) {
267           return ((IdentifierTree) expression).name();
268         }
269       }
270       return "";
271     }
272 
273   }
274 
275   @Override
276   public void checkEndOfExecutionPath(CheckerContext context, ConstraintManager constraintManager) {
277     EXCEPTIONAL_YIELD_CHECKER.reportOnExceptionalYield(context.getNode(), this);
278   }
279 }