private void validateFieldAnnotations() {
    for (FieldNode fieldNode : annotatedClass.getFields()) {
      if (shouldFieldBeIgnored(fieldNode)) continue;

      AnnotationNode annotation = getAnnotation(fieldNode, DSL_FIELD_ANNOTATION);

      if (annotation == null) continue;

      if (ASTHelper.isListOrMap(fieldNode.getType())) return;

      if (annotation.getMember("members") != null) {
        addCompileError(
            String.format(
                "@Field.members is only valid for List or Map fields, but field %s is of type %s",
                fieldNode.getName(), fieldNode.getType().getName()),
            annotation);
      }
    }
  }
  private void validateFields(BlockStatement block) {
    Validation.Option mode =
        getEnumMemberValue(
            getAnnotation(annotatedClass, VALIDATION_ANNOTATION),
            "option",
            Validation.Option.class,
            Validation.Option.IGNORE_UNMARKED);
    for (FieldNode fieldNode : annotatedClass.getFields()) {
      if (shouldFieldBeIgnoredForValidation(fieldNode)) continue;

      ClosureExpression validationClosure =
          createGroovyTruthClosureExpression(block.getVariableScope());
      String message = null;

      AnnotationNode validateAnnotation = getAnnotation(fieldNode, VALIDATE_ANNOTATION);
      if (validateAnnotation != null) {
        message =
            getMemberStringValue(
                validateAnnotation, "message", "'" + fieldNode.getName() + "' must be set!");
        Expression member = validateAnnotation.getMember("value");
        if (member instanceof ClassExpression) {
          ClassNode memberType = member.getType();
          if (memberType.equals(ClassHelper.make(Validate.Ignore.class))) continue;
          else if (!memberType.equals(ClassHelper.make(Validate.GroovyTruth.class))) {
            addError(
                "value of Validate must be either Validate.GroovyTruth, Validate.Ignore or a closure.",
                fieldNode);
          }
        } else if (member instanceof ClosureExpression) {
          validationClosure = (ClosureExpression) member;
        }
      }

      if (validateAnnotation != null || mode == Validation.Option.VALIDATE_UNMARKED) {
        block.addStatement(
            new AssertStatement(
                new BooleanExpression(
                    callX(validationClosure, "call", args(varX(fieldNode.getName())))),
                message == null ? ConstantExpression.NULL : new ConstantExpression(message)));
      }
    }
  }
  @SuppressWarnings("unchecked")
  public <T extends Enum> T getEnumMemberValue(
      AnnotationNode node, String name, Class<T> type, T defaultValue) {
    if (node == null) return defaultValue;

    final PropertyExpression member = (PropertyExpression) node.getMember(name);
    if (member == null) return defaultValue;

    if (!type.equals(member.getObjectExpression().getType().getTypeClass())) return defaultValue;

    try {
      String value = member.getPropertyAsString();
      Method fromString = type.getMethod("valueOf", String.class);
      return (T) fromString.invoke(null, value);
    } catch (Exception e) {
      return defaultValue;
    }
  }
  @Override
  public void visit(ASTNode[] astNodes, SourceUnit source) {
    if (!(astNodes[0] instanceof AnnotationNode) || !(astNodes[1] instanceof AnnotatedNode)) {
      throw new RuntimeException("Internal error: wrong types: $node.class / $parent.class");
    }

    AnnotatedNode parent = (AnnotatedNode) astNodes[1];
    AnnotationNode node = (AnnotationNode) astNodes[0];
    if (!MY_TYPE.equals(node.getClassNode()) || !(parent instanceof ClassNode)) {
      return;
    }

    ClassNode classNode = (ClassNode) parent;
    if (classNode.isInterface() || Modifier.isAbstract(classNode.getModifiers())) {
      return;
    }

    boolean junit3Test = isJunit3Test(classNode);
    boolean spockTest = isSpockTest(classNode);
    boolean isJunit = classNode.getName().endsWith("Tests");

    if (!junit3Test && !spockTest && !isJunit) return;

    Expression value = node.getMember("value");
    ClassExpression ce;
    if (value instanceof ClassExpression) {
      ce = (ClassExpression) value;
      testFor(classNode, ce);
    } else {
      if (!junit3Test) {
        List<AnnotationNode> annotations = classNode.getAnnotations(MY_TYPE);
        if (annotations.size() > 0)
          return; // bail out, in this case it was already applied as a local transform
        // no explicit class specified try by convention
        String fileName = source.getName();
        String className = GrailsResourceUtils.getClassName(new FileSystemResource(fileName));
        if (className != null) {
          boolean isSpock = className.endsWith("Spec");
          String targetClassName = null;

          if (isJunit) {
            targetClassName = className.substring(0, className.indexOf("Tests"));
          } else if (isSpock) {
            targetClassName = className.substring(0, className.indexOf("Spec"));
          }

          if (targetClassName != null) {
            Resource targetResource =
                getResourceLocator().findResourceForClassName(targetClassName);
            if (targetResource != null) {
              try {
                if (GrailsResourceUtils.isDomainClass(targetResource.getURL())) {
                  testFor(
                      classNode,
                      new ClassExpression(
                          new ClassNode(targetClassName, 0, ClassHelper.OBJECT_TYPE)));
                } else {
                  for (String artefactType : artefactTypeToTestMap.keySet()) {
                    if (targetClassName.endsWith(artefactType)) {
                      testFor(
                          classNode,
                          new ClassExpression(
                              new ClassNode(targetClassName, 0, ClassHelper.OBJECT_TYPE)));
                      break;
                    }
                  }
                }
              } catch (IOException e) {
                // ignore
              }
            }
          }
        }
      }
    }
  }