docs: protected boolean existsProperty(final PropertyExpression pexp, final boolean readMode, final ClassCodeVisitorSupport visitor) {
          super.visitPropertyExpression(pexp);
  
          String propertyName = pexp.getPropertyAsString();
          if (propertyName == null) return false;
  
          Expression objectExpression = pexp.getObjectExpression();
          final ClassNode objectExpressionType = getType(objectExpression);
  
          boolean staticOnlyAccess = isClassClassNodeWrappingConcreteType(objectExpressionType);
          if ("this".equals(propertyName) && staticOnlyAccess) {
              // Outer.this for any level of nesting
              ClassNode outerNode = objectExpressionType.getGenericsTypes()[0].getType();
              List<ClassNode> candidates = typeCheckingContext.getEnclosingClassNodes();
              ClassNode found = null;
              for (ClassNode current : candidates) {
                  if (!current.isStaticClass() && current instanceof InnerClassNode && outerNode.equals(current.getOuterClass())) {
                      found = current;
                      break;
                  }
              }
              if (found != null) {
                  storeType(pexp, outerNode);
                  return true;
              }
          }
  
          if (objectExpressionType.isArray() && "length".equals(pexp.getPropertyAsString())) {
              storeType(pexp, int_TYPE);
              if (visitor != null) {
                  PropertyNode node = new PropertyNode("length", Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, int_TYPE, objectExpressionType, null, null, null);
                  visitor.visitProperty(node);
              }
              return true;
          }
  
          boolean foundGetterOrSetter = false;
          List<Receiver<String>> receivers = new LinkedList<Receiver<String>>();
          List<Receiver<String>> owners = makeOwnerList(objectExpression);
          addReceivers(receivers, owners, pexp.isImplicitThis());
  
          String capName = capitalize(propertyName);
          boolean isAttributeExpression = pexp instanceof AttributeExpression;
          HashSet<ClassNode> handledNodes = new HashSet<ClassNode>();
          for (Receiver<String> receiver : receivers) {
              ClassNode testClass = receiver.getType();
              LinkedList<ClassNode> queue = new LinkedList<ClassNode>();
              queue.add(testClass);
              if (isPrimitiveType(testClass)) {
                  queue.add(getWrapper(testClass));
              }
              while (!queue.isEmpty()) {
                  ClassNode current = queue.removeFirst();
                  if (handledNodes.contains(current)) continue;
                  handledNodes.add(current);
                  Set<ClassNode> allInterfaces = current.getAllInterfaces();
                  for (ClassNode intf : allInterfaces) {
                      //TODO: apply right generics here!
                      queue.add(GenericsUtils.parameterizeType(current, intf));
                  }
  
                  // in case of a lookup on Class we look for instance methods on Class
                  // as well, since in case of a static property access we have the class
                  // itself in the list of receivers already;
                  boolean staticOnly;
                  if (isClassClassNodeWrappingConcreteType(current)) {
                      staticOnly = false;
                  } else {
                      staticOnly = staticOnlyAccess;
                  }
  
                  FieldNode field = current.getDeclaredField(propertyName);
                  field = allowStaticAccessToMember(field, staticOnly);
                  if (storeField(field, isAttributeExpression, pexp, current, visitor, receiver.getData(), !readMode))
                      return true;
  
                  boolean isThisExpression = objectExpression instanceof VariableExpression
                          && ((VariableExpression) objectExpression).isThisExpression()
                          && objectExpressionType.equals(current);
  
                  if (storeField(field, isThisExpression, pexp, receiver.getType(), visitor, receiver.getData(), !readMode))
                      return true;
  
                  MethodNode getter = findGetter(current, "get" + capName, pexp.isImplicitThis());
                  getter = allowStaticAccessToMember(getter, staticOnly);
                  if (getter == null) getter = findGetter(current, "is" + capName, pexp.isImplicitThis());
                  getter = allowStaticAccessToMember(getter, staticOnly);
                  final String setterName = "set" + capName;
                  List<MethodNode> setters = findSetters(current, setterName, false);
                  setters = allowStaticAccessToMember(setters, staticOnly);
  
                  // TODO: remove this visit
                  // need to visit even if we only look for a setters for compatibility
                  if (visitor != null && getter != null) visitor.visitMethod(getter);
  
                  PropertyNode propertyNode = current.getProperty(propertyName);
                  propertyNode = allowStaticAccessToMember(propertyNode, staticOnly);
                  //prefer explicit getter or setter over property if receiver is not 'this'
                  boolean checkGetterOrSetter = !isThisExpression || propertyNode == null;
  
                  if (readMode && checkGetterOrSetter) {
                      if (getter != null) {
                          ClassNode cn = inferReturnTypeGenerics(current, getter, ArgumentListExpression.EMPTY_ARGUMENTS);
                          storeInferredTypeForPropertyExpression(pexp, cn);
                          storeTargetMethod(pexp, getter);
                          pexp.removeNodeMetaData(StaticTypesMarker.READONLY_PROPERTY);
                          String delegationData = receiver.getData();
                          if (delegationData != null)
                              pexp.putNodeMetaData(StaticTypesMarker.IMPLICIT_RECEIVER, delegationData);
                          return true;
                      }
                  } else if (!readMode && checkGetterOrSetter) {
                      if (!setters.isEmpty()) {
                          if (visitor != null) {
                              if (field != null) {
                                  visitor.visitField(field);
                              } else {
                                  for (MethodNode setter : setters) {
                                      ClassNode setterType = setter.getParameters()[0].getOriginType();
                                      FieldNode virtual = new FieldNode(propertyName, 0, setterType, current, EmptyExpression.INSTANCE);
                                      visitor.visitField(virtual);
                                  }
                              }
                          }
                          //TODO: apply generics on parameter[0]?
  //                                storeType(pexp, setter.getParameters()[0].getType());
                          SetterInfo info = new SetterInfo(current, setterName, setters);
                          BinaryExpression enclosingBinaryExpression = typeCheckingContext.getEnclosingBinaryExpression();
                          if (enclosingBinaryExpression != null) {
                              putSetterInfo(enclosingBinaryExpression.getLeftExpression(), info);
                          }
                          String delegationData = receiver.getData();
                          if (delegationData != null) {
                              pexp.putNodeMetaData(StaticTypesMarker.IMPLICIT_RECEIVER, delegationData);
                          }
                          return true;
                      } else if (getter != null && propertyNode == null) {
                          pexp.putNodeMetaData(StaticTypesMarker.READONLY_PROPERTY, true);
                      }
                  }
                  foundGetterOrSetter = foundGetterOrSetter || !setters.isEmpty() || getter != null;
  
                  if (storeProperty(propertyNode, pexp, current, visitor, receiver.getData())) return true;
  
                  if (storeField(field, true, pexp, current, visitor, receiver.getData(), !readMode)) return true;
                  // if the property expression is an attribute expression (o.@attr), then
                  // we stop now, otherwise we must check the parent class
                  if (/*!isAttributeExpression && */current.getSuperClass() != null) {
                      queue.add(current.getUnresolvedSuperClass());
                  }
              }
              // GROOVY-5568, the property may be defined by DGM
              List<ClassNode> dgmReceivers = new ArrayList<ClassNode>(2);
              dgmReceivers.add(testClass);
              if (isPrimitiveType(testClass)) dgmReceivers.add(getWrapper(testClass));
              for (ClassNode dgmReceiver : dgmReceivers) {
                  List<MethodNode> methods = findDGMMethodsByNameAndArguments(getTransformLoader(), dgmReceiver, "get" + capName, ClassNode.EMPTY_ARRAY);
                  for (MethodNode m : findDGMMethodsByNameAndArguments(getTransformLoader(), dgmReceiver, "is" + capName, ClassNode.EMPTY_ARRAY)) {
                      if (Boolean_TYPE.equals(getWrapper(m.getReturnType()))) methods.add(m);
                  }
                  if (!methods.isEmpty()) {
                      List<MethodNode> methodNodes = chooseBestMethod(dgmReceiver, methods, ClassNode.EMPTY_ARRAY);
                      if (methodNodes.size() == 1) {
                          MethodNode getter = methodNodes.get(0);
                          if (visitor != null) {
                              visitor.visitMethod(getter);
                          }
                          ClassNode cn = inferReturnTypeGenerics(dgmReceiver, getter, ArgumentListExpression.EMPTY_ARGUMENTS);
                          storeInferredTypeForPropertyExpression(pexp, cn);
                          storeTargetMethod(pexp, getter);
                          return true;
                      }
                  }
              }
          }
  
          for (Receiver<String> receiver : receivers) {
              ClassNode testClass = receiver.getType();
              ClassNode propertyType = getTypeForMapPropertyExpression(testClass, objectExpressionType, pexp);
              if (propertyType == null)
                  propertyType = getTypeForListPropertyExpression(testClass, objectExpressionType, pexp);
              if (propertyType == null) propertyType = getTypeForSpreadExpression(testClass, objectExpressionType, pexp);
              if (propertyType == null) continue;
              if (visitor != null) {
                  // todo : type inference on maps and lists, if possible
                  PropertyNode node = new PropertyNode(propertyName, Opcodes.ACC_PUBLIC, propertyType, receiver.getType(), null, null, null);
                  node.setDeclaringClass(receiver.getType());
                  visitor.visitProperty(node);
              }
              storeType(pexp, propertyType);
              String delegationData = receiver.getData();
              if (delegationData != null) pexp.putNodeMetaData(StaticTypesMarker.IMPLICIT_RECEIVER, delegationData);
              return true;
          }
          return foundGetterOrSetter;
      }