docs: @SuppressWarnings("null")
      public static Method getMethod(EvaluationContext ctx, Object base, Object property,
              Class<?>[] paramTypes, Object[] paramValues)
              throws MethodNotFoundException {
          if (base == null || property == null) {
              throw new MethodNotFoundException(MessageFactory.get(
                      "error.method.notfound", base, property,
                      paramString(paramTypes)));
          }
  
          String methodName = (property instanceof String) ? (String) property
                  : property.toString();
  
          int paramCount;
          if (paramTypes == null) {
              paramCount = 0;
          } else {
              paramCount = paramTypes.length;
          }
  
          Method[] methods = base.getClass().getMethods();
          Map<Method,MatchResult> candidates = new HashMap<>();
  
          for (Method m : methods) {
              if (!m.getName().equals(methodName)) {
                  // Method name doesn't match
                  continue;
              }
  
              Class<?>[] mParamTypes = m.getParameterTypes();
              //149852 - This was removed due to a findBugs error we felt was worthy of a change.
              //m.getParameterTypes will ALWAYS return an array, it may be of length 0 though
              int mParamCount = mParamTypes.length;
  
              // Check the number of parameters
              if (!(paramCount == mParamCount ||
                      (m.isVarArgs() && paramCount >= mParamCount))) {
                  // Method has wrong number of parameters
                  continue;
              }
  
              // Check the parameters match
              int exactMatch = 0;
              int assignableMatch = 0;
              int coercibleMatch = 0;
              boolean noMatch = false;
              for (int i = 0; i < mParamCount; i++) {
                  // Can't be null
                  if (mParamTypes[i].equals(paramTypes[i])) {
                      exactMatch++;
                  } else if (i == (mParamCount - 1) && m.isVarArgs()) {
                      Class<?> varType = mParamTypes[i].getComponentType();
                      for (int j = i; j < paramCount; j++) {
                          if (isAssignableFrom(paramTypes[j], varType)) {
                              assignableMatch++;
                          } else {
                              if (paramValues == null) {
                                  noMatch = true;
                                  break;
                              } else {
                                  if (isCoercibleFrom(ctx, paramValues[j], varType)) {
                                      coercibleMatch++;
                                  } else {
                                      noMatch = true;
                                      break;
                                  }
                              }
                          }
                          // Don't treat a varArgs match as an exact match, it can
                          // lead to a varArgs method matching when the result
                          // should be ambiguous
                      }
                  } else if (isAssignableFrom(paramTypes[i], mParamTypes[i])) {
                      assignableMatch++;
                  } else {
                      if (paramValues == null) {
                          noMatch = true;
                          break;
                      } else {
                          if (isCoercibleFrom(ctx, paramValues[i], mParamTypes[i])) {
                              coercibleMatch++;
                          } else {
                              noMatch = true;
                              break;
                          }
                      }
                  }
              }
              if (noMatch) {
                  continue;
              }
  
              // If a method is found where every parameter matches exactly,
              // return it
              if (exactMatch == paramCount) {
                  return getMethod(base.getClass(), m);
              }
  
              candidates.put(m, new MatchResult(
                      exactMatch, assignableMatch, coercibleMatch, m.isBridge()));
          }
  
          // Look for the method that has the highest number of parameters where
          // the type matches exactly
          MatchResult bestMatch = new MatchResult(0, 0, 0, false);
          Method match = null;
          boolean multiple = false;
          for (Map.Entry<Method, MatchResult> entry : candidates.entrySet()) {
              int cmp = entry.getValue().compareTo(bestMatch);
              if (cmp > 0 || match == null) {
                  bestMatch = entry.getValue();
                  match = entry.getKey();
                  multiple = false;
              } else if (cmp == 0) {
                  multiple = true;
              }
          }
          if (multiple) {
              if (bestMatch.getExact() == paramCount - 1) {
                  // Only one parameter is not an exact match - try using the
                  // super class
                  match = resolveAmbiguousMethod(candidates.keySet(), paramTypes);
              } else {
                  match = null;
              }
  
              if (match == null) {
                  // If multiple methods have the same matching number of parameters
                  // the match is ambiguous so throw an exception
                  throw new MethodNotFoundException(MessageFactory.get(
                          "error.method.ambiguous", base, property,
                          paramString(paramTypes)));
                  }
          }
  
          // Handle case where no match at all was found
          if (match == null) {
              throw new MethodNotFoundException(MessageFactory.get(
                          "error.method.notfound", base, property,
                          paramString(paramTypes)));
          }
  
          return getMethod(base.getClass(), match);
      }