All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jangaroo.jooc.backend.JsCodeGenerator Maven / Gradle / Ivy

There is a newer version: 4.1.17
Show newest version
package net.jangaroo.jooc.backend;

import net.jangaroo.jooc.CodeGenerator;
import net.jangaroo.jooc.CompilationUnitResolver;
import net.jangaroo.jooc.Debug;
import net.jangaroo.jooc.JooSymbol;
import net.jangaroo.jooc.Jooc;
import net.jangaroo.jooc.JsWriter;
import net.jangaroo.jooc.SyntacticKeywords;
import net.jangaroo.jooc.ast.*;
import net.jangaroo.jooc.config.DebugMode;
import net.jangaroo.jooc.config.JoocConfiguration;
import net.jangaroo.jooc.json.JsonArray;
import net.jangaroo.jooc.json.JsonObject;
import net.jangaroo.jooc.model.MethodType;
import net.jangaroo.jooc.mxml.MxmlUtils;
import net.jangaroo.jooc.sym;
import net.jangaroo.jooc.types.ExpressionType;
import net.jangaroo.jooc.util.MessageFormat;
import net.jangaroo.properties.PropcHelper;
import net.jangaroo.utils.AS3Type;
import net.jangaroo.utils.CompilerUtils;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

/**
 * A visitor of the AST that generates executable JavaScript code on
 * a {@link net.jangaroo.jooc.JsWriter}.
 */
public class JsCodeGenerator extends CodeGeneratorBase {
  private static final JooSymbol SYM_VAR = new JooSymbol(sym.VAR, "var"); // NOSONAR introducing a constant for "var" would obscure the generated output
  private static final JooSymbol SYM_SEMICOLON = new JooSymbol(sym.SEMICOLON, ";");
  private static final JooSymbol SYM_LBRACE = new JooSymbol(sym.LBRACE, "{");
  private static final JooSymbol SYM_RBRACE = new JooSymbol(sym.RBRACE, "}");
  public static final Set PRIMITIVES = new HashSet(4);
  public static final List ANNOTATIONS_TO_TRIGGER_AT_RUNTIME = Arrays.asList("SWF", "ExtConfig"); // TODO: inject / make configurable
  public static final String DEFAULT_ANNOTATION_PARAMETER_NAME = "";
  public static final String INIT_STATICS = "__initStatics__";

  static {
    PRIMITIVES.add("Boolean");
    PRIMITIVES.add("String");
    PRIMITIVES.add("Number");
    PRIMITIVES.add("int");
    PRIMITIVES.add("uint");
    PRIMITIVES.add("Object");
    PRIMITIVES.add("RegExp");
    PRIMITIVES.add("Date");
    PRIMITIVES.add("Array");
    PRIMITIVES.add("Error");
    PRIMITIVES.add("Vector");
    PRIMITIVES.add("Class");
    PRIMITIVES.add("Function");
    PRIMITIVES.add("XML");
  }

  public static boolean generatesCode(IdeDeclaration primaryDeclaration) {
    // only generate JavaScript if [Native] / [Mixin] annotation and 'native' modifier on primary compilationUnit are not present:
    return primaryDeclaration.getAnnotation(Jooc.NATIVE_ANNOTATION_NAME) == null
            && !primaryDeclaration.isNative()
            && primaryDeclaration.getAnnotation(Jooc.MIXIN_ANNOTATION_NAME) == null;
  }

  private final JsModuleResolver jsModuleResolver;
  private boolean expressionMode = false;
  private Map imports = new HashMap();
  private ClassDefinitionBuilder primaryClassDefinitionBuilder = new ClassDefinitionBuilder();
  private int staticCodeCounter = 0;
  private ClassDefinitionBuilder secondaryClassDefinitionBuilder;
  private CompilationUnit compilationUnit;
  private String factory;
  private final MessageFormat $NAME_EQUALS_ARGUMENTS_SLICE_$INDEX =
    new MessageFormat("{0}=Array.prototype.slice.call(arguments{1,choice,0#|0<,{1}});");

  private void generateToArrayCode(String paramName, int paramIndex) throws IOException {
    if (!FunctionExpr.ARGUMENTS.equals(paramName)) {
      out.writeToken("var");
    }
    out.writeToken($NAME_EQUALS_ARGUMENTS_SLICE_$INDEX.format(paramName, paramIndex));
  }

  private final CodeGenerator ARGUMENT_TO_ARRAY_CODE_GENERATOR = new CodeGenerator() {
    @Override
    public void generate(JsWriter out, boolean first) throws IOException {
      generateToArrayCode(FunctionExpr.ARGUMENTS, 0);
    }
  };

  public JsCodeGenerator(JsWriter out, CompilationUnitResolver compilationUnitModelResolver, JsModuleResolver jsModuleResolver) {
    super(out, compilationUnitModelResolver);
    this.jsModuleResolver = jsModuleResolver;
  }

  private Map membersOrStaticMembers(Declaration memberDeclaration) {
    ClassDefinitionBuilder classDefinitionBuilder = getClassDefinitionBuilder(memberDeclaration);
    return memberDeclaration.isStatic() ? classDefinitionBuilder.staticMembers : classDefinitionBuilder.members;
  }

  private ClassDefinitionBuilder getClassDefinitionBuilder(Declaration memberDeclaration) {
    return memberDeclaration.getClassDeclaration() == null || memberDeclaration.getClassDeclaration().isPrimaryDeclaration() ? primaryClassDefinitionBuilder : secondaryClassDefinitionBuilder;
  }

  String compilationUnitAccessCode(IdeDeclaration primaryDeclaration) {
    CompilationUnit otherUnit = primaryDeclaration.getCompilationUnit();
    if (otherUnit == compilationUnit) {
      return primaryDeclaration.getName();
    } else {
      primaryDeclaration = compilationUnit.mapMixinInterface(otherUnit).getPrimaryDeclaration();
      String primaryDeclarationName = imports.get(primaryDeclaration.getQualifiedNameStr());
      Debug.assertTrue(primaryDeclarationName != null, "QName not found in imports: " + primaryDeclaration.getQualifiedNameStr());
      return primaryDeclarationName;
    }
  }

  @Override
  public void visitDotExpr(DotExpr dotExpr) throws IOException {
    IdeDeclaration memberDeclaration = null;
    Expr arg = dotExpr.getArg();
    JooSymbol symDot = dotExpr.getOp();
    Ide ide = dotExpr.getIde();

    if (arg instanceof IdeExpr) {
      arg = ((IdeExpr)arg).getNormalizedExpr();
    }
    ExpressionType type = arg.getType();
    if (type != null) {
      memberDeclaration = type.resolvePropertyDeclaration(ide.getName());
    }

    String memberName = ide.getName();

    if (memberDeclaration != null && memberDeclaration.isPrivateStatic()) {
      if (arg instanceof IdeExpr && arg.getSymbol().isVirtual()) {
        // suppress synthesized class
        out.writeSymbolWhitespace(arg.getSymbol());
      } else {
        // comment out explicit reference to class for private static access:
        out.beginComment();
        arg.visit(this);
        out.writeSymbol(symDot);
        out.endComment();
      }
      // add "$static" suffix to private static members:
      memberName += "$static";
      if (memberDeclaration instanceof PropertyDeclaration ||
              memberDeclaration instanceof FunctionDeclaration && ((FunctionDeclaration) memberDeclaration).isGetterOrSetter()) {
        // it can only be a read access, because lhs is already handled in visiAssignmentOpExpr():
        memberName = "get$" + memberName + "()";
      }
      writeSymbolReplacement(ide.getIde(), memberName);
      return;
    }

    if (memberDeclaration != null) {
      Annotation nativeAnnotation = memberDeclaration.getAnnotation(Jooc.NATIVE_ANNOTATION_NAME);
      if (nativeAnnotation != null) {
        Object nativeMemberName = nativeAnnotation.getPropertiesByName().get(null);
        if (nativeMemberName instanceof String) {
          memberName = (String) nativeMemberName;
        }
      }
      if (memberDeclaration.isPrivate()) {
        memberName = memberName + "$" + ide.getScope().getClassDeclaration().getQualifiedNameHash();
      }
    }

    String separatorToken = ".";
    String closingToken = "";

    if (ide.isBound()) {
      // found access to a method without applying it immediately: bind!
      out.writeToken("AS3.bind(");
      separatorToken = ",";
      memberName = CompilerUtils.quote(memberName);
      closingToken = ")";
    } else if (memberDeclaration != null && !type.isConfigType() && !ide.isAssignmentLHS()) {
      TypedIdeDeclaration getter = findMemberWithBindableAnnotation(ide, MethodType.GET, memberDeclaration.getClassDeclaration());
      if (getter != null) {
        // found usage of an [Bindable]-annotated get function: call it via AS3.getBindable()!
        Expr normalizedArg = arg instanceof IdeExpr ? ((IdeExpr) arg).getNormalizedExpr() : arg;
        out.writeSymbolWhitespace(normalizedArg.getSymbol());
        out.writeToken("AS3.getBindable(");
        memberName = CompilerUtils.quote(getter.getName());
        separatorToken = ",";
        closingToken = ")";
        String bindableEvent = getBindableEventName(getter);
        if (bindableEvent != null && !"DUMMY".equals(bindableEvent)) {
          closingToken = "," + CompilerUtils.quote(bindableEvent) + closingToken;
        }
      }
    } else if (memberName.startsWith("@")) {
      // escape @... property names:
      separatorToken = "[";
      memberName = CompilerUtils.quote(memberName);
      closingToken = "]";
    }

    arg.visit(this);
    writeSymbolReplacement(symDot, separatorToken);
    writeSymbolReplacement(ide.getIde(), memberName);
    if (!closingToken.isEmpty()) {
      out.writeToken(closingToken);
    }
  }

  @Override
  public void visitPrefixOpExpr(PrefixOpExpr prefixOpExpr) throws IOException {
    if (prefixOpExpr instanceof Spread) {
      Expr expr = prefixOpExpr.getArg();
      if (expr instanceof ApplyExpr && ((ApplyExpr) expr).isTypeCheckObjectLiteralFunctionCall()) {
        expr = ((ApplyExpr) expr).getArgs().getExpr().getTail().getHead();
      }
      assert expr instanceof ObjectLiteral;
      // "flatten" spread inner objects:
      visitIfNotNull(((ObjectLiteral) expr).getFields());
    } else {
      super.visitPrefixOpExpr(prefixOpExpr);
    }
  }

  @Override
  public void visitTypeRelation(TypeRelation typeRelation) throws IOException {
    out.beginCommentWriteSymbol(typeRelation.getSymRelation());
    typeRelation.getType().getIde().visit(this);
    out.endComment();
  }

  @Override
  public void visitExtends(Extends anExtends) throws IOException {
    out.writeSymbol(anExtends.getSymExtends());
    out.writeSymbol(anExtends.getSuperClass().getIde());
  }

  @Override
  public void visitCompilationUnit(CompilationUnit compilationUnit) throws IOException {
    IdeDeclaration primaryDeclaration = compilationUnit.getPrimaryDeclaration();
    boolean isClassDeclaration = primaryDeclaration instanceof ClassDeclaration;
    String[] requires = collectDependencies(compilationUnit, isClassDeclaration ? Boolean.TRUE : null);
    String[] uses = isClassDeclaration ? collectDependencies(compilationUnit, false) : new String[0];
    PackageDeclaration packageDeclaration = compilationUnit.getPackageDeclaration();
    this.compilationUnit = compilationUnit;

    if (isClassDeclaration && ((ClassDeclaration) primaryDeclaration).isPropertiesClass()) {
      renderPropertiesClass((ClassDeclaration) primaryDeclaration, uses);
      return;
    }

    out.beginComment();
    packageDeclaration.visit(this);
    out.writeSymbol(compilationUnit.getLBrace());
    visitAll(compilationUnit.getDirectives());
    out.endComment();
    primaryDeclaration.visit(this);
    out.beginComment();
    out.writeSymbol(compilationUnit.getRBrace());
    out.write("\n");
    out.write("\n============================================== Jangaroo part ==============================================");
    out.endComment();
    JsonObject classDefinition = createClassDefinition(primaryDeclaration, primaryClassDefinitionBuilder);
    if (requires.length > 0) {
      classDefinition.set("requires", new JsonArray(requires));
    }
    if (uses.length > 0) {
      classDefinition.set("uses", new JsonArray(uses));
    }
    out.write("\n    return " + classDefinition.toString(2, 4) + ";\n}");
    out.write(");\n");
  }

  private void renderPropertiesClass(ClassDeclaration classDeclaration, String[] uses) throws IOException {
    for (Annotation annotation : classDeclaration.getAnnotations()) {
      annotation.visit(this);
    }
    out.writeSymbolWhitespace(classDeclaration.getSymModifiers()[0]); // only use public's white-space
    out.write("Ext.define(");
    String propertiesClassName = classDeclaration.getTargetQualifiedNameStr();
    out.write(CompilerUtils.quote(propertiesClassName));
    out.write(", {");
    boolean isPropertiesSubclass = classDeclaration.isPropertiesSubclass();
    boolean startWithComma = false;
    if (isPropertiesSubclass) {
      out.write("\n  override: " + CompilerUtils.quote(PropcHelper.computeBaseClassName(propertiesClassName)));
      startWithComma = true;
    } else {
      String alternateClassName = getAlternateClassName(classDeclaration);
      if (alternateClassName != null) {
        out.write("\n  alternateClassName: " + CompilerUtils.quote(alternateClassName));
        startWithComma = true;
      }
    }
    // for some reason, References end up in 'uses', not in 'requires':
    if (uses.length > 0) {
      if (startWithComma) {
        out.write(",");
      } else {
        startWithComma = true;
      }
      out.write("\n  requires: " + new JsonArray((Object[]) uses).toString(2, 2));
    }
    FunctionDeclaration constructorDeclaration = classDeclaration.getConstructor();
    renderPropertiesClassValues(getPropertiesClassAssignments(constructorDeclaration, true, false), true, !isPropertiesSubclass, startWithComma);
    List assignmentsWithReferences = getPropertiesClassAssignments(constructorDeclaration, false, true);
    if (!isPropertiesSubclass || !assignmentsWithReferences.isEmpty()) {
      out.write("\n}, function() {");
      if (!assignmentsWithReferences.isEmpty()) {
        out.write("\n  Ext.apply(this.prototype, {");
        renderPropertiesClassValues(assignmentsWithReferences, true, !isPropertiesSubclass, false);
        out.write("\n  });");
      }
      if (!isPropertiesSubclass) {
        out.write("\n  this." + PROPERTY_CLASS_INSTANCE + " = new this();");
      }
    }
    out.write("\n});");
  }

  private JsonObject createClassDefinition(IdeDeclaration declaration, ClassDefinitionBuilder classDefinitionBuilder) throws IOException {
    JsonObject classDefinition;
    if (declaration instanceof ClassDeclaration && ((ClassDeclaration) declaration).isInterface()) {
      classDefinition = createInterfaceDefinition((ClassDeclaration) declaration);
    } else {
      if (declaration instanceof ClassDeclaration) {
        classDefinition = createClassDefinition((ClassDeclaration) declaration);
      } else {
        classDefinition = new JsonObject();
        boolean isLazy = declaration.getAnnotation(Jooc.LAZY_ANNOTATION_NAME) != null;
        classDefinition.set(isLazy ? "__lazyFactory__" : "__factory__", JsonObject.code(factory));
      }
      fillClassDefinition(classDefinition, classDefinitionBuilder);
    }
    return classDefinition;
  }

  private JsonObject createInterfaceDefinition(ClassDeclaration interfaceDeclaration) {
    JsonObject interfaceDefinition = new JsonObject();
    addOptImplements(interfaceDeclaration, interfaceDefinition);
    return interfaceDefinition;
  }

  private void addOptImplements(ClassDeclaration classDeclaration, JsonObject classDefinition) {
    Implements optImplements = classDeclaration.getOptImplements();
    if (optImplements != null) {
      List superInterfaces = new ArrayList();
      CommaSeparatedList superTypes = optImplements.getSuperTypes();
      while (superTypes != null) {
        IdeDeclaration superInterface = superTypes.getHead().getDeclaration(false);
        if (superInterface == null) {
          System.err.println("ignoring unresolvable interface " + superTypes.getHead().getQualifiedNameStr());
        } else {
          CompilationUnit mixinCompilationUnit = compilationUnit.mapMixinInterface(superInterface.getCompilationUnit());
          if (!compilationUnit.equals(mixinCompilationUnit)) {
            superInterfaces.add(compilationUnitAccessCode(superInterface));
          }
        }
        superTypes = superTypes.getTail();
      }
      if (!superInterfaces.isEmpty()) {
        if (classDeclaration.isInterface() && superInterfaces.size() == 1) {
          // if interface has just one super interface, let Ext class extend super interface Ext class:
          classDefinition.set("extend", superInterfaces.get(0));
        } else {
          // is class or has more than one super interface: mix-in interfaces Ext classes
          classDefinition.set("mixins", new JsonArray(superInterfaces.toArray()));
        }
      }
    }
  }

  private String[] collectDependencies(CompilationUnit compilationUnit, Boolean required) throws IOException {
    Set requires = new TreeSet<>();
    Collection dependentCompilationUnits = required == null
            ? compilationUnit.getRuntimeDependencies()
            : compilationUnit.getRuntimeDependencies(required);
    for (String dependentCUId : dependentCompilationUnits) {
      String javaScriptName = dependentCUId;
      String javaScriptNameToRequire = "";
      CompilationUnit dependentCompilationUnitModel = compilationUnitModelResolver.resolveCompilationUnit(dependentCUId);

      IdeDeclaration primaryDeclaration = dependentCompilationUnitModel.getPrimaryDeclaration();

      Annotation nativeAnnotation = primaryDeclaration.getAnnotation(Jooc.NATIVE_ANNOTATION_NAME);
      if (nativeAnnotation != null) {
        String javaScriptAlias = ModuleResolverBase.getNativeAnnotationValue(nativeAnnotation);
        if (javaScriptAlias != null) {
          javaScriptName = javaScriptAlias;
        }
        javaScriptNameToRequire = jsModuleResolver.getNativeAnnotationRequireValue(nativeAnnotation);
      }
      Annotation renameAnnotation = primaryDeclaration.getAnnotation(Jooc.RENAME_ANNOTATION_NAME);
      if (renameAnnotation != null) {
        String renamedName = ModuleResolverBase.getNativeAnnotationValue(renameAnnotation);
        if (renamedName != null) {
          javaScriptName = renamedName;
        }
      }
      if (javaScriptNameToRequire != null) {
        requires.add(javaScriptNameToRequire.isEmpty() ? javaScriptName : javaScriptNameToRequire);
      }
      imports.put(dependentCUId, javaScriptName);
    }

    return requires.toArray(new String[requires.size()]);
  }

  private JsonObject createClassDefinition(ClassDeclaration classDeclaration) {
    JsonObject classDefinition = new JsonObject();
    if (classDeclaration.notExtendsObject()) {
      ClassDeclaration superTypeDeclaration = classDeclaration.getSuperTypeDeclaration();
      classDefinition.set("extend", compilationUnitAccessCode(superTypeDeclaration));
    }
    addOptImplements(classDeclaration, classDefinition);
    String alternateClassName = getAlternateClassName(classDeclaration);
    if (alternateClassName != null) {
      classDefinition.set("alternateClassName", alternateClassName);
    }
    return classDefinition;
  }

  private static String getAlternateClassName(ClassDeclaration classDeclaration) {
    String alternateClassName = classDeclaration.getTargetQualifiedNameStrWithoutRename();
    return classDeclaration.getTargetQualifiedNameStr().equals(alternateClassName) ? null : alternateClassName;
  }

  private void fillClassDefinition(JsonObject classDefinition, ClassDefinitionBuilder classDefinitionBuilder) throws IOException {
    if (!classDefinitionBuilder.metadata.isEmpty()) {
      classDefinition.set("metadata", classDefinitionBuilder.metadata);
    }
    if (!classDefinitionBuilder.mixinConfig.isEmpty()) {
      classDefinition.set("mixinConfig", classDefinitionBuilder.mixinConfig);
    }
    JsonObject members = convertMembers(classDefinitionBuilder.members, false);
    JsonObject bindables = convertBindables(classDefinitionBuilder.members);
    if (!bindables.isEmpty()) {
      members.set("config", bindables);
    }
    JsonObject extPrivateMembers = convertMembers(classDefinitionBuilder.members, true);
    if (!extPrivateMembers.isEmpty()) {
      members.set("privates", extPrivateMembers);
    }
    if (!members.isEmpty()) {
      classDefinition.add(members);
    }
    JsonObject staticMembers = convertMembers(classDefinitionBuilder.staticMembers, false);
    if (!staticMembers.isEmpty() || classDefinitionBuilder.staticCode.length() > 0) {
      if (classDefinitionBuilder.staticCode.length() > 0) {
        String staticInitializer = String.format("function() {\n%s        }",
                classDefinitionBuilder.staticCode.toString());
        staticMembers.set(INIT_STATICS, JsonObject.code(staticInitializer));
      }
      classDefinition.set("statics", staticMembers);
    }
    JsonObject accessors = convertAccessors(classDefinitionBuilder.members);
    JsonObject staticAccessors = convertAccessors(classDefinitionBuilder.staticMembers);
    if (!staticAccessors.isEmpty()) {
      accessors.set("statics", staticAccessors);
    }
    if (!accessors.isEmpty()) {
      classDefinition.set("__accessors__", accessors);
    }
  }

  private JsonObject convertMembers(Map members, boolean extPrivate) {
    JsonObject membersDefinition = new JsonObject();
    for (Map.Entry entry : members.entrySet()) {
      if (entry.getValue().isValueOnly() && !entry.getValue().bindable && entry.getValue().extPrivate == extPrivate) {
        membersDefinition.set(entry.getKey(), JsonObject.code(entry.getValue().value));
      }
    }
    return membersDefinition;
  }

  private JsonObject convertBindables(Map members) {
    JsonObject bindables = new JsonObject();
    for (Map.Entry entry : members.entrySet()) {
      PropertyDefinition member = entry.getValue();
      if (member.bindable) {
        bindables.set(entry.getKey(), JsonObject.code(member.isValueOnly() ? member.value : "null"));
      }
    }
    return bindables;
  }

  private JsonObject convertAccessors(Map members) {
    JsonObject accessors = new JsonObject();
    for (Map.Entry entry : members.entrySet()) {
      if (!entry.getValue().isValueOnly()) {
        accessors.set(entry.getKey(), entry.getValue().asJson());
      }
    }
    return accessors;
  }

  @Override
  public void visitIde(Ide ide) throws IOException {
    out.writeSymbolWhitespace(ide.getIde());
    // take care of reserved words called as functions (Rhino does not like):
    String ideText = ide.getIde().getText();
    if (!out.isWritingComment()) {
      if (expressionMode) {
        if (ide.isSuper()) {
          ideText = getSuperClassPrototypeAccessCode();
        }
        if ("this".equals(ideText) && ide.isRewriteThis()) {
          ideText = "_this";
        } else {
          ideText = convertIdentifier(ideText);
          IdeDeclaration ideDeclaration = ide.getDeclaration(false);
          if (ideDeclaration != null) {
            if (ideDeclaration.isPrimaryDeclaration()) {
              ideText = compilationUnitAccessCode(ideDeclaration);
            } else if (ideDeclaration.isPrivateStatic()) {
              if (ideDeclaration instanceof FunctionDeclaration && ((FunctionDeclaration)ideDeclaration).isGetterOrSetter()) {
                ideText = "get$" + ideText + "$static()";
              } else {
                ideText += "$static";
              }
            }
          }
        }
      } else {
        ideText = convertIdentifier(ideText);
      }
    }
    out.writeTokenForSymbol(ideText, ide.getSymbol());
  }

  // take care of reserved words called as functions (Rhino does not like):
  private String convertIdentifier(String identifier) {
    return SyntacticKeywords.RESERVED_WORDS.contains(identifier) ? identifier + "_" : identifier;
  }

  @Override
  public void visitQualifiedIde(QualifiedIde qualifiedIde) throws IOException {
    if (out.isWritingComment()) {
      super.visitQualifiedIde(qualifiedIde);
    } else {
//      out.beginComment();
//      qualifiedIde.getQualifier().visit(this);
//      out.endComment();
      out.writeSymbolWhitespace(qualifiedIde.getQualifier().getSymbol());
      IdeDeclaration ideDeclaration = qualifiedIde.getDeclaration();
      String compilationUnitAccessCode = compilationUnitAccessCode(ideDeclaration);
      String ideName = qualifiedIde.getName();
      if (!qualifiedIde.getScope().lookupDeclaration(new Ide(ideName), false).isPrimaryDeclaration()) {
        // something non-imported (primary declaration) hides the short name used for import, so rather use the fully qualified name:
        out.writeTokenForSymbol(qualifiedIde.getQualifiedNameStr(), qualifiedIde.getSymbol());
      } else {
        out.writeTokenForSymbol(compilationUnitAccessCode, qualifiedIde.getSymbol());
      }
    }
  }

  @Override
  public void visitIdeWithTypeParam(IdeWithTypeParam ideWithTypeParam) throws IOException {
    if (out.isWritingComment()) {
      out.writeSymbol(ideWithTypeParam.getOriginalIde());
    } else {
      visitIde(ideWithTypeParam);
    }
    out.beginComment();
    out.writeSymbol(ideWithTypeParam.getSymDotLt());
    ideWithTypeParam.getType().visit(this);
    out.writeSymbol(ideWithTypeParam.getSymGt());
    out.endComment();
  }

  @Override
  public void visitNamespacedIde(NamespacedIde namespacedIde) throws IOException {
    // so far, namespaces are only comments:
    out.beginComment();
    out.writeSymbol(namespacedIde.getNamespace().getSymbol());
    out.writeSymbol(namespacedIde.getSymNamespaceSep());
    out.endComment();
    visitIde(namespacedIde);
  }

  @Override
  public void visitIdeExpression(IdeExpr ideExpr) throws IOException {
    visitInExpressionMode(ideExpr.getIde());
  }

  private void visitInExpressionMode(AstNode expr) throws IOException {
    boolean oldExpressionMode = expressionMode;
    expressionMode = !out.isWritingComment();
    try {
      expr.visit(this);
    } finally {
      expressionMode = oldExpressionMode;
    }
  }

  @Override
  public void visitAssignmentOpExpr(AssignmentOpExpr assignmentOpExpr) throws IOException {
    Expr leftHandSide = assignmentOpExpr.getArg1();
    if (assignmentOpExpr.getOp().sym == sym.ANDANDEQ || assignmentOpExpr.getOp().sym == sym.OROREQ) {
      leftHandSide.visit(this);
      writeSymbolReplacement(assignmentOpExpr.getOp(), "=");
      // TODO: refactor for a simpler way to switch off white-space temporarily:
      JoocConfiguration options = (JoocConfiguration) out.getOptions();
      DebugMode mode = options.getDebugMode();
      options.setDebugMode(null);
      leftHandSide.visit(this);
      options.setDebugMode(mode);
      out.writeToken(assignmentOpExpr.getOp().sym == sym.ANDANDEQ ? "&&" : "||");
      out.writeToken("(");
      assignmentOpExpr.getArg2().visit(this);
      out.writeToken(")");
    } else {
      if (leftHandSide instanceof IdeExpr) {
        leftHandSide = ((IdeExpr)leftHandSide).getNormalizedExpr();
      }
      if (leftHandSide instanceof DotExpr) {
        DotExpr dotExpr = (DotExpr) leftHandSide;
        Ide ide = dotExpr.getIde();
        ExpressionType type = dotExpr.getArg().getType();
        if (type != null) {
          IdeDeclaration ideDeclaration = type.resolvePropertyDeclaration(ide.getName());
          if (ideDeclaration != null && ideDeclaration.isPrivateStatic() && (ideDeclaration instanceof PropertyDeclaration
                  || ideDeclaration instanceof FunctionDeclaration
                  && ((FunctionDeclaration)ideDeclaration).isGetterOrSetter())) {
            writeSymbolReplacement(leftHandSide.getSymbol(), "set$" + ide.getName() + "$static");
            writeSymbolReplacement(assignmentOpExpr.getOp(), "(");
            assignmentOpExpr.getArg2().visit(this);
            out.writeToken(")");
            return;
          }
          if (!type.isConfigType()) {
            String setter = resolveBindable(dotExpr, MethodType.SET);
            if (setter != null) {
              AstNode dotExprArg = dotExpr.getArg();
              out.writeSymbolWhitespace(dotExprArg.getSymbol());
              out.write("AS3.setBindable(");
              visitInExpressionMode(dotExprArg);
              writeSymbolReplacement(dotExpr.getOp(), ",");
              out.write(CompilerUtils.quote(setter));
              writeSymbolReplacement(assignmentOpExpr.getOp(), ",");
              assignmentOpExpr.getArg2().visit(this);
              out.writeToken(")");
              return;
            }
          }
        }
      }
      
      visitBinaryOpExpr(assignmentOpExpr);
    }
  }

  private String resolveBindable(DotExpr dotExpr, MethodType methodType) throws IOException {
    //System.err.println("*#*#*#* trying to find " + methodType + "ter for qIde " + qIde.getQualifiedNameStr());
    ExpressionType lhsType = dotExpr.getArg().getType();
    if (lhsType != null && lhsType.getAS3Type() == AS3Type.OBJECT) {
      TypedIdeDeclaration member = findMemberWithBindableAnnotation(dotExpr.getIde(), methodType, lhsType.getDeclaration());
      return member == null ? null : member.getName();
    }
    if (lhsType instanceof Typed) {
      TypeRelation typeRelation = ((Typed) lhsType).getOptTypeRelation();
      if (typeRelation != null) {
        IdeDeclaration typeDeclaration = typeRelation.getType().resolveDeclaration();
        if (typeDeclaration instanceof ClassDeclaration) {
          return resolveBindable(dotExpr.getIde(), methodType, (ClassDeclaration) typeDeclaration);
        }
      }
    }
    return null;
  }

  private String resolveBindable(Ide qIde, MethodType methodType, ClassDeclaration typeDeclaration) throws IOException {
    TypedIdeDeclaration member = findMemberWithBindableAnnotation(qIde, methodType, typeDeclaration);
    return member == null ? null : getBindablePropertyName(methodType, member);
  }

  private static String getBindableEventName(TypedIdeDeclaration member) {
    Object eventAnnotation = getBindablePropertiesByName(member).get("event");
    return eventAnnotation instanceof String ? (String) eventAnnotation : null;
  }

  @Override
  public void visitParameters(Parameters parameters) throws IOException {
    visitIfNotNull(parameters.getHead());
    if (parameters.getSymComma() != null) {
      if (parameters.getTail().getHead().isRest()) {
        out.beginCommentWriteSymbol(parameters.getSymComma());
        parameters.getTail().visit(this);
        out.endComment();
      } else {
        out.writeSymbol(parameters.getSymComma());
        parameters.getTail().visit(this);
      }
    }
  }

  @Override
  public void visitFunctionExpr(FunctionExpr functionExpr) throws IOException {
    out.writeSymbol(functionExpr.getSymFunction());
    if (functionExpr.getIde() != null) {
      out.writeSymbol(functionExpr.getIde().getIde());
    }
    handleParameters(functionExpr);
    generateFunTailCode(functionExpr);
  }

  public void handleParameters(FunctionExpr functionExpr) throws IOException {
    Parameters params = functionExpr.getParams();
    if (functionExpr.hasBody()) {
      if (functionExpr.isArgumentsUsedAsArray()) {
        addBlockStartCodeGenerator(functionExpr.getBody(), ARGUMENT_TO_ARRAY_CODE_GENERATOR);
      }
      if (params != null) {
        // inject into body for generating initializers later:
        JoocConfiguration config = ((Jooc) params.getHead().getIde().getScope().getCompiler()).getConfig();
        addBlockStartCodeGenerator(functionExpr.getBody(),
                config.isUseEcmaParameterInitializerSemantics()
                        ? getEcmaParameterInitializerCodeGenerator(params)
                        : getParameterInitializerCodeGenerator(params));
      }
    }
  }

  public void generateFunTailCode(FunctionExpr functionExpr) throws IOException {
    generateFunctionExprSignature(functionExpr);
    if (functionExpr.hasBody()) {
      functionExpr.getBody().visit(this);
    }
  }

  public CodeGenerator getParameterInitializerCodeGenerator(final Parameters params) {
    return new CodeGenerator() {
      @Override
      public void generate(JsWriter out, boolean first) throws IOException {
        // collect all optional parameters with their position index:
        Map paramByIndex = new HashMap();
        int paramIndex = 0;
        for (Parameters parameters = params; parameters != null; parameters = parameters.getTail()) {
          Parameter param = parameters.getHead();
          if (param.hasInitializer()) {
            paramByIndex.put(paramIndex, param);
          }
          ++paramIndex;
        }
        generateParameterInitializers(out, paramByIndex);
        generateRestParamCode(params);
      }

    };
  }

  public CodeGenerator getEcmaParameterInitializerCodeGenerator(final Parameters params) {
    return new CodeGenerator() {
      @Override
      public void generate(JsWriter out, boolean first) throws IOException {
        for (Parameters parameters = params; parameters != null; parameters = parameters.getTail()) {
          Parameter param = parameters.getHead();
          if (param.hasInitializer()) {
            out.write(ASSIGN_DEFAULT_IF_PARAMETER_IS_UNDEFINED.format(param.getName()));
            generateBodyInitializerCode(param);
            out.write("}");
          }
        }
        generateRestParamCode(params);
      }
    };
  }

  private final MessageFormat IF_ARGUMENT_LENGTH_LTE_$N = new MessageFormat("if(arguments.length<={0})");
  private final MessageFormat SWITCH_$INDEX = new MessageFormat("switch({0,choice,0#arguments.length|0 paramByIndex) throws IOException {
    Iterator> paramByIndexIterator = paramByIndex.entrySet().iterator();
    if (paramByIndexIterator.hasNext()) {
      Map.Entry indexAndParam = paramByIndexIterator.next();
      Integer firstParamIndex = indexAndParam.getKey();
      if (!paramByIndexIterator.hasNext()) {
        // only one parameter initializer: use "if"
        out.write(IF_ARGUMENT_LENGTH_LTE_$N.format(firstParamIndex));
        generateBodyInitializerCode(indexAndParam.getValue());
      } else {
        // more than one parameter initializer: use "switch"
        out.write(SWITCH_$INDEX.format(firstParamIndex));
        out.write("{");

        while (true) {
          out.write(CASE_$N.format(indexAndParam.getKey()));
          generateBodyInitializerCode(indexAndParam.getValue());
          if (!paramByIndexIterator.hasNext()) {
            break;
          }
          indexAndParam = paramByIndexIterator.next();
        }

        out.write("}");
      }
    }
  }

  public void generateRestParamCode(final Parameters params) throws IOException {
    if (params != null) {
      // determine the last parameter and its index:
      int lastParamIndex = 0;
      Parameters parameters = params;
      while (parameters.getTail() != null) {
        ++lastParamIndex;
        parameters = parameters.getTail();
      }
      // now check for ...rest parameter:
      Parameter lastParam = parameters.getHead();
      if (lastParam.isRest()) {
        String lastParamName = lastParam.getName();
        if (lastParamName != null && !(lastParamName.equals(FunctionExpr.ARGUMENTS) && lastParamIndex == 0)) {
          generateToArrayCode(lastParamName, lastParamIndex);
        }
      }
    }
  }

  public void generateBodyInitializerCode(Parameter param) throws IOException {
    out.setSuppressWhitespace(true); // do not output whitespace twice!
    try {
      out.writeToken(param.getName());
      out.writeSymbol(param.getOptInitializer().getSymEq());
      param.getOptInitializer().getValue().visit(this);
      out.write(";");
    } finally {
      out.setSuppressWhitespace(false);
    }
  }

  @Override
  public void visitVectorLiteral(VectorLiteral vectorLiteral) throws IOException {
    out.beginComment();
    out.writeSymbol(vectorLiteral.getSymNew());
    out.writeSymbol(vectorLiteral.getSymLt());
    vectorLiteral.getVectorType().visit(this);
    out.writeSymbol(vectorLiteral.getSymGt());
    out.endComment();
    vectorLiteral.getArrayLiteral().visit(this);
  }

  @Override
  protected void handleExmlAppendPrepend(ObjectField objectField, DotExpr exmlAppendOrPrepend) throws IOException {
    JooSymbol propertySymbol = objectField.getLabel().getSymbol();
    out.writeTokenForSymbol(propertySymbol.getText() + "$at", propertySymbol);
    out.write(":");
    // net.jangaroo.ext.Exml:
    exmlAppendOrPrepend.getArg().visit(this);
    // '.'
    out.writeSymbol(exmlAppendOrPrepend.getOp());
    JooSymbol appendOrPrependSymbol = exmlAppendOrPrepend.getIde().getSymbol();
    // append -> APPEND, prepend -> PREPEND
    out.writeTokenForSymbol(appendOrPrependSymbol.getText().toUpperCase(), appendOrPrependSymbol);
    out.write(", ");
    objectField.getLabel().visit(this);
    out.writeSymbol(objectField.getSymColon());
    // suppress the Exml.append / prepend function name, only render its argument (leave the unnecessary parenthesis):
    ((ApplyExpr) objectField.getValue()).getArgs().visit(this);
  }

  @Override
  public void visitApplyExpr(ApplyExpr applyExpr) throws IOException {
    if (applyExpr.getArgs() != null && applyExpr.getFun() instanceof IdeExpr) {
      Ide funIde = ((IdeExpr) applyExpr.getFun()).getIde();
      JooSymbol lParen = applyExpr.getArgs().getLParen();
      CommaSeparatedList arguments = applyExpr.getArgs().getExpr();
      if (applyExpr.isAssert()) {
        writeSymbolReplacement(funIde.getSymbol(), builtInIdentifierCode(SyntacticKeywords.ASSERT));
        JooSymbol symKeyword = applyExpr.getFun().getSymbol();
        out.writeSymbol(lParen);
        arguments.visit(this);
        out.writeToken(", ");
        out.writeString(new File(symKeyword.getFileName()).getName());
        out.writeToken(", ");
        out.writeInt(symKeyword.getLine());
        out.write(", ");
        out.writeInt(symKeyword.getColumn());
        out.writeSymbol(applyExpr.getArgs().getRParen());
        return;
      } else if (isAddEventListenerMethod(funIde)) {
        out.writeSymbolWhitespace(funIde.getSymbol());
        out.writeToken("AS3.");
        out.writeSymbol(funIde.getIde());
        out.writeSymbol(lParen, false);
        funIde.getQualifier().visit(this);
        writeSymbolReplacement(((QualifiedIde) funIde).getSymDot(), ",");
        out.writeSymbolWhitespace(lParen);
        Expr eventConstant = arguments.getHead();
        if (!(eventConstant instanceof IdeExpr)) {
          throw Jooc.error(eventConstant, String.format("'%s' must be used with event constant.", MxmlUtils.ADD_EVENT_LISTENER_METHOD_NAME));
        }
        Ide eventConstantIde = ((IdeExpr) eventConstant).getIde();
        if (eventConstantIde.getQualifier() == null) {
          throw Jooc.error(eventConstant, String.format("'%s' must be used with event constant from event class.", MxmlUtils.ADD_EVENT_LISTENER_METHOD_NAME));
        }
        visitInExpressionMode(eventConstantIde.getQualifier());
        writeSymbolReplacement(((QualifiedIde) eventConstantIde).getSymDot(), ",");
        out.writeToken("\"");
        out.writeSymbol(eventConstantIde.getIde());
        out.writeToken("\"");
        out.writeSymbol(arguments.getSymComma());
        arguments.getTail().visit(this);
        out.writeSymbol(applyExpr.getArgs().getRParen());
        return;
      }
    }
    generateFunJsCode(applyExpr);
  }

  private boolean isAddEventListenerMethod(Ide funIde) {
    if (MxmlUtils.ADD_EVENT_LISTENER_METHOD_NAME.equals(funIde.getName())) {
      Ide qualifier = funIde.getQualifier();
      if (qualifier != null) {
        IdeDeclaration qualifierDeclaration = qualifier.resolveDeclaration();
        if (qualifierDeclaration instanceof ClassDeclaration) {
          CompilationUnit type = compilationUnitModelResolver.resolveCompilationUnit(qualifierDeclaration.getQualifiedNameStr());
          if (type.getPrimaryDeclaration() != null) {
            // check whether the type implements IObservable:
            return compilationUnitModelResolver.implementsInterface(type, MxmlUtils.EVENT_DISPATCHER_INTERFACE);
          }
        }
      }
    }
    return false;
  }

  private void generateFunJsCode(ApplyExpr applyExpr) throws IOException {
    // handle constructor function if called as type cast function!
    // these old-style type casts are soo ugly....
    ParenthesizedExpr> args = applyExpr.getArgs();
    if (applyExpr.isTypeCast()) {
      out.writeSymbolWhitespace(applyExpr.getFun().getSymbol());
      out.writeToken("AS3.cast");
      out.writeSymbol(args.getLParen());
      applyExpr.getFun().visit(this);
      out.writeToken(",");
      // isTypeCast() ensures that there is exactly one parameter:
      args.getExpr().getHead().visit(this);
      out.writeSymbol(args.getRParen());
    } else if (applyExpr.isTypeCheckObjectLiteralFunctionCall()) {
      // suppress virtual object literal type check function call:
      args.getExpr().getTail().getHead().visit(this);
    } else {
      applyExpr.getFun().visit(this);
      // check for super call:
      if (args != null && applyExpr.getFun() instanceof IdeExpr && ((IdeExpr) applyExpr.getFun()).getIde().isQualifiedBySuper()) {
        generateSuperCallParameters(args);
      } else {
        visitApplyExprArguments(applyExpr);
      }
    }
  }

  @Override
  public void visitClassBody(ClassBody classBody) throws IOException {
    out.beginComment();
    out.writeSymbol(classBody.getLBrace());
    out.endComment();
    visitClassBodyDirectives(classBody.getDirectives());
    out.beginComment();
    out.writeSymbol(classBody.getRBrace());
    out.endComment();
  }

  void generateStaticInitializer(List directives) throws IOException {
    String staticFunctionName = "static$" + staticCodeCounter++;

    out.writeToken(String.format("function %s(){", staticFunctionName));
    for (Directive directive : directives) {
      directive.visit(this);
    }
    out.writeToken("}");

    primaryClassDefinitionBuilder.staticCode.append("          ").append(staticFunctionName).append("();\n");
  }

  @Override
  protected String builtInIdentifierCode(String builtInIdentifier) {
    return "AS3." + builtInIdentifier;
  }

  @Override
  public void visitForInStatement(final ForInStatement forInStatement) throws IOException {
    final Ide exprAuxIde = forInStatement.getExprAuxIde();
    ExpressionType exprType = forInStatement.getExpr().getType();
    boolean iterateArrayMode = exprType != null && exprType.isArrayLike();
    if (exprAuxIde != null && !iterateArrayMode) {
      new SemicolonTerminatedStatement(new VariableDeclaration(SYM_VAR, exprAuxIde, null, null), SYM_SEMICOLON).visit(this);
    }
    out.writeSymbol(forInStatement.getSymKeyword());
    final boolean isForEach = forInStatement.getSymEach() != null;
    if (isForEach) {
      out.beginComment();
      out.writeSymbol(forInStatement.getSymEach());
      out.endComment();
    }
    out.writeSymbol(forInStatement.getLParen());
    if (isForEach || iterateArrayMode) {
      new VariableDeclaration(SYM_VAR, forInStatement.getAuxIde(), null, null).visit(this);
    } else {
      if (forInStatement.getDecl() != null) {
        forInStatement.getDecl().visit(this);
      } else {
        forInStatement.getLValue().visit(this);
      }
    }
    if (iterateArrayMode) {
      String indexVarName = forInStatement.getAuxIde().getName();
      out.write("=0");
      if (exprAuxIde != null) {
        out.write(",");
        out.writeToken(exprAuxIde.getName());
        out.writeToken("=");
        out.beginComment();
        out.writeSymbol(forInStatement.getSymIn());
        out.endComment();
        forInStatement.getExpr().visit(this);
      }
      out.write(";");
      out.write(indexVarName);
      out.write("<");
      if (exprAuxIde != null) {
        out.writeToken(exprAuxIde.getName());
      } else {
        out.beginComment();
        out.writeSymbol(forInStatement.getSymIn());
        out.endComment();
        forInStatement.getExpr().visit(this);
      }
      out.write(".length;");
      out.write("++" + indexVarName);
    } else {
      out.writeSymbol(forInStatement.getSymIn());
      if (exprAuxIde != null) {
        // assign the ^ value to the auxiliary expression value variable once:
        out.writeToken(exprAuxIde.getName());
        out.writeToken("=");
      }
      forInStatement.getExpr().visit(this);
    }
    out.writeSymbol(forInStatement.getRParen());
    if (isForEach || iterateArrayMode) {
      // inject synthesized statement into loop body:
      if (!(forInStatement.getBody() instanceof BlockStatement)) {
        forInStatement.setBody(new BlockStatement(SYM_LBRACE, Arrays.asList(forInStatement.getBody()), SYM_RBRACE));
      }
      addBlockStartCodeGenerator((BlockStatement) forInStatement.getBody(), new CodeGenerator() {
        @Override
        public void generate(JsWriter out, boolean first) throws IOException {
          // synthesize assigning the correct index to the variable given in the original for each statement:
          if (forInStatement.getDecl() != null) {
            forInStatement.getDecl().visit(JsCodeGenerator.this);
          } else {
            forInStatement.getLValue().visit(JsCodeGenerator.this);
          }
          out.writeToken("=");
          if (!isForEach) {
            out.write("String(" + forInStatement.getAuxIde().getName() + ")");
          } else {
            if (exprAuxIde == null) {
              forInStatement.getExpr().visit(JsCodeGenerator.this);
            } else {
              out.write(exprAuxIde.getName());
            }
            out.write("[" + forInStatement.getAuxIde().getName() + "]");
          }
          out.write(";");
        }
      });
    }
    forInStatement.getBody().visit(this);
  }

  @Override
  public void visitParameter(Parameter parameter) throws IOException {
    Debug.assertTrue(parameter.getModifiers() == 0, "Parameters must not have any modifiers");
    if (parameter.isRest()) {
      out.beginCommentWriteSymbol(parameter.getOptSymRest());
      parameter.getIde().visit(this);
      out.endComment();
    } else {
      parameter.getIde().visit(this);
    }
    visitParameterTypeRelation(parameter);
    // in the method signature, comment out initializer code.
    if (parameter.getOptInitializer() != null) {
      out.beginComment();
      parameter.getOptInitializer().visit(this);
      out.endComment();
    }
  }

  @Override
  public void visitVariableDeclaration(VariableDeclaration variableDeclaration) throws IOException {
    if (variableDeclaration.hasPreviousVariableDeclaration()) {
      Debug.assertTrue(variableDeclaration.getOptSymConstOrVar() != null && variableDeclaration.getOptSymConstOrVar().sym == sym.COMMA, "Additional variable declarations must start with a COMMA.");
    }
    visitAll(variableDeclaration.getAnnotations());
    writeExtDefineCodePrefix(variableDeclaration);
    List currentMetadata = buildMetadata(variableDeclaration);
    if ((variableDeclaration.isClassMember() || variableDeclaration.isPrimaryDeclaration()) && !variableDeclaration.isPrivateStatic()) {
      if (!variableDeclaration.isPrimaryDeclaration() && !currentMetadata.isEmpty()) {
        getClassDefinitionBuilder(variableDeclaration).storeCurrentMetadata(
                variableDeclaration.getIde().getName() + (variableDeclaration.isPrivate() ? "$" + variableDeclaration.getClassDeclaration().getQualifiedNameHash() : ""),
                currentMetadata
        );
      }
      out.beginComment();
      writeModifiers(variableDeclaration);
      writeOptSymbol(variableDeclaration.getOptSymConstOrVar());
      variableDeclaration.getIde().visit(this);
      visitIfNotNull(variableDeclaration.getOptTypeRelation());
      if (mustInitializeInStaticCode(variableDeclaration)) {
        out.endComment();
        generateFieldInitializerCode(variableDeclaration);
      } else {
        visitIfNotNull(variableDeclaration.getOptInitializer());
        out.endComment();
      }
      registerField(variableDeclaration, currentMetadata);
      visitIfNotNull(variableDeclaration.getOptNextVariableDeclaration());
      generateFieldEndCode(variableDeclaration);
    } else {
      if (variableDeclaration.hasPreviousVariableDeclaration()) {
        writeOptSymbol(variableDeclaration.getOptSymConstOrVar());
      } else {
        generateVarStartCode(variableDeclaration);
      }
      visitInExpressionMode(variableDeclaration.getIde());
      visitIfNotNull(variableDeclaration.getOptTypeRelation());
      Initializer optInitializer = variableDeclaration.getOptInitializer();
      if (optInitializer == null) {
        if (variableDeclaration.isPrimaryDeclaration() || variableDeclaration.isPrivateStatic()) {
          TypeRelation typeRelation = variableDeclaration.getOptTypeRelation();
          String value = VariableDeclaration.getDefaultValue(typeRelation);
          if (value != null) {
            out.write("=" + value);
          }
        }
      } else {
        if (variableDeclaration.isPrivateStatic() && mustInitializeInStaticCode(variableDeclaration)) {
          out.writeToken(";");
          generateFieldInitializerCode(variableDeclaration);
          registerField(variableDeclaration, currentMetadata);
          visitIfNotNull(variableDeclaration.getOptNextVariableDeclaration());
          // suppress redundant ';' after field initializer:
          writeOptSymbolWhitespace(variableDeclaration.getOptSymSemicolon());
          return;
        } else {
          optInitializer.visit(this);
        }
      }
      visitIfNotNull(variableDeclaration.getOptNextVariableDeclaration());
      writeOptSymbol(variableDeclaration.getOptSymSemicolon());
    }
  }

  private void registerField(VariableDeclaration variableDeclaration, List currentMetadata) {
    String variableName = variableDeclaration.getName();
    boolean isBindable = variableDeclaration.isBindable();
    String value = null;
    if (mustInitializeInStaticCode(variableDeclaration)) {
      if (variableDeclaration.isStatic()) {
        primaryClassDefinitionBuilder.staticCode.append("          ").append(variableName).append("$static_();\n");
      }
      if (isBindable || variableDeclaration.isStatic() && !variableDeclaration.isPrivate()) {
        // make sure that configs are always declared, even with dynamic initializer, so that Ext magic is applied:
        value = "undefined";
      }
    } else {
      if (variableDeclaration.getOptInitializer() != null) {
        Expr initialValue = variableDeclaration.getOptInitializer().getValue();
        JsWriter originalOut = out;
        StringWriter initialValueWriter = new StringWriter();
        out = new JsWriter(initialValueWriter);
        out.setOptions(originalOut.getOptions());
        try {
          initialValue.visit(this);
        } catch (IOException e) {
          // cannot happen
        } finally {
          out = originalOut;
        }
        value = initialValueWriter.toString().trim();
      } else {
        TypeRelation typeRelation = variableDeclaration.getOptTypeRelation();
        value = VariableDeclaration.getDefaultValue(typeRelation);
      }
      if (variableDeclaration.isPrivate() && !variableDeclaration.isStatic()) {
        variableName += "$" + ((ClassDeclaration)compilationUnit.getPrimaryDeclaration()).getQualifiedNameHash();
      }
    }
    if (variableDeclaration.isPrimaryDeclaration()) {
      factory = value == null
              ? variableName + "_"
              : String.format("function() {\n        return(%s);\n      }", value);
      return;
    }
    if (value != null) {
      // special Ext magic: when declaring a public static const xtype:String, replace it by an alias: "widget." + Clazz.xtype:
      if (variableDeclaration.isPublic() && variableDeclaration.isStatic() && variableDeclaration.isConst()
              && "xtype".equals(variableDeclaration.getName())
              && variableDeclaration.getOptInitializer() != null
              && variableDeclaration.getOptInitializer().getValue() instanceof LiteralExpr) {
        getClassDefinitionBuilder(variableDeclaration).members.put("alias",
                new PropertyDefinition(CompilerUtils.quote("widget." + CompilerUtils.unquote(value))));
      } else {
        membersOrStaticMembers(variableDeclaration).put(variableName,
                new PropertyDefinition(value, !variableDeclaration.isConst(), isBindable));
      }
    }

  }

  protected void generateVarStartCode(VariableDeclaration variableDeclaration) throws IOException {
    out.beginComment();
    writeModifiers(variableDeclaration);
    out.endComment();
    if (variableDeclaration.getOptSymConstOrVar() != null) {
      if (variableDeclaration.isConst()) {
        out.beginCommentWriteSymbol(variableDeclaration.getOptSymConstOrVar());
        out.endComment();
        out.writeToken("var");
      } else {
        out.writeSymbol(variableDeclaration.getOptSymConstOrVar());
      }
    }
  }

  protected void generateFieldInitializerCode(VariableDeclaration variableDeclaration) throws IOException {
    Initializer initializer = variableDeclaration.getOptInitializer();
    out.beginComment();
    out.writeSymbol(initializer.getSymEq());
    out.endComment();
    String variableName = variableDeclaration.getName();
    out.writeToken("function");
    out.writeToken(variableName + (variableDeclaration.isStatic() ? "$static" : "") + "_");
    out.writeToken("(){");
    Expr initializerValue = initializer.getValue();
    if (variableDeclaration.isPrivateStatic()) {
      out.write(variableName + "$static=(");
      initializerValue.visit(this);
    } else {
      String target = variableDeclaration.isStatic() ? variableDeclaration.getClassDeclaration().getName() : "this";
      if (!variableDeclaration.isStatic() && initializerValue instanceof FunctionExpr
              && ((FunctionExpr) initializerValue).isThisAliased(false)) {
        ALIAS_THIS_CODE_GENERATOR.generate(out, true);
      }
      String slotName = variableName + (variableDeclaration.isPrivate() ? "$" + variableDeclaration.getClassDeclaration().getQualifiedNameHash() : "");
      if (false /* variableDeclaration.isConst() */) { // keep it compatible, even for constants! 
        out.write("Object.defineProperty(" + target + ",\"" + slotName + "\",{value:");
        initializerValue.visit(this);
        out.writeToken("}");
      } else {
        if (variableDeclaration.isPrimaryDeclaration()) {
          out.writeToken("return");
        } else {
          out.write(target + "." + slotName + "=");
        }
        out.writeToken("(");
        initializerValue.visit(this);
      }
    }
    out.writeToken(");}");
  }

  private boolean mustInitializeInStaticCode(VariableDeclaration variableDeclaration) {
    return variableDeclaration.getOptInitializer() != null && !variableDeclaration.getOptInitializer().getValue().isRuntimeConstant();
  }

  protected void generateFieldEndCode(VariableDeclaration variableDeclaration) throws IOException {
    if (!variableDeclaration.hasPreviousVariableDeclaration()) {
      Debug.assertTrue(variableDeclaration.getOptSymSemicolon() != null, "optSymSemicolon != null");
      out.beginComment();
      out.writeSymbol(variableDeclaration.getOptSymSemicolon());
      out.endComment();
    }
  }

  private static final CodeGenerator ALIAS_THIS_CODE_GENERATOR = (out, first) -> out.write("var _this=this;");

  @Override
  public void visitFunctionDeclaration(FunctionDeclaration functionDeclaration) throws IOException {
    visitAll(functionDeclaration.getAnnotations());
    boolean isPrimaryDeclaration = functionDeclaration.equals(compilationUnit.getPrimaryDeclaration());
    assert functionDeclaration.isClassMember() || (!functionDeclaration.isNative() && !functionDeclaration.isAbstract());
    if (isPrimaryDeclaration) {
      writeExtDefineCodePrefix(functionDeclaration);
      factory = "function() {\n        return " + functionDeclaration.getName() + ";\n      }";
    }
    handleParameters(functionDeclaration.getFun());
    if (functionDeclaration.isClassMember() && functionDeclaration.isThisAliased(false)) {
      addBlockStartCodeGenerator(functionDeclaration.getBody(), ALIAS_THIS_CODE_GENERATOR);
    }
    if (functionDeclaration.isConstructor() && !functionDeclaration.containsSuperConstructorCall() && functionDeclaration.hasBody()
       && needsSuperCallCodeGenerator(functionDeclaration.getClassDeclaration())) {
      addBlockStartCodeGenerator(functionDeclaration.getBody(), new SuperCallCodeGenerator(functionDeclaration.getClassDeclaration()));
    }
    if (!functionDeclaration.isClassMember() && !isPrimaryDeclaration) {
      functionDeclaration.getFun().visit(this);
    } else {
      JooSymbol functionSymbol = functionDeclaration.getIde().getSymbol();
      String functionName = convertIdentifier(functionSymbol.getText());
      String methodName = functionName;
      List currentMetadata = buildMetadata(functionDeclaration);
      if (!isPrimaryDeclaration && !currentMetadata.isEmpty()) {
        getClassDefinitionBuilder(functionDeclaration).storeCurrentMetadata(
                functionName,
                currentMetadata
        );
      }
      out.beginComment();
      writeModifiers(functionDeclaration);
      Map members = membersOrStaticMembers(functionDeclaration);
      if (functionDeclaration.isAbstract() || functionDeclaration.isNative()) {
        out.writeSymbol(functionDeclaration.getFun().getFunSymbol());
        writeOptSymbol(functionDeclaration.getSymGetOrSet());
        functionDeclaration.getIde().visit(this);
        generateFunctionExprSignature(functionDeclaration.getFun());
        writeOptSymbol(functionDeclaration.getOptSymSemicolon());
        out.endComment();
      } else {
        out.endComment();
        out.writeSymbol(functionDeclaration.getFun().getFunSymbol());

        boolean isAccessor = functionDeclaration.isGetterOrSetter();
        if (isAccessor) {
          Metadata bindableAnnotation = Metadata.find(currentMetadata, Jooc.BINDABLE_ANNOTATION_NAME);
          if (bindableAnnotation != null) {
            String accessorPrefix = functionDeclaration.getSymGetOrSet().getText();
            String accessorName = (String) bindableAnnotation.getArgumentValue(DEFAULT_ANNOTATION_PARAMETER_NAME);
            members.put(functionName, new PropertyDefinition("undefined", true, true));
            methodName = accessorName != null ? accessorName : accessorPrefix + MxmlUtils.capitalize(functionName);
            functionName = accessorPrefix + "$" + functionName;
            isAccessor = false;
          }
        } else if (functionDeclaration.isConstructor()) {
          functionName += "$";
        }

        String overriddenMethodName = null;
        PropertyDefinition overriddenPropertyDefinition = null;
        if (functionDeclaration.isPrivate() && !functionDeclaration.isStatic()) {
          String privateMethodName = methodName + "$" + functionDeclaration.getClassDeclaration().getQualifiedNameHash();
          if (functionDeclaration.isOverride()) {
            overriddenMethodName = privateMethodName;
            getClassDefinitionBuilder(functionDeclaration).super$Used = true;
          } else {
            methodName = privateMethodName;
          }
        } else if (functionDeclaration.isStatic()) {
          functionName += "$static";
        }
        if (isAccessor) {
          out.writeSymbolWhitespace(functionDeclaration.getIde().getSymbol());
          out.writeSymbolWhitespace(functionDeclaration.getSymGetOrSet());
          String accessorPrefix = functionDeclaration.getSymGetOrSet().getText() + "$";
          String accessorName = accessorPrefix + functionName;
          out.writeToken(accessorName);
          if (!functionDeclaration.isPrivateStatic()) { // TODO: simulate private static getter when called!
            PropertyDefinition accessorDefinition;
            accessorDefinition = members.get(methodName);
            if (accessorDefinition == null) {
              accessorDefinition = new PropertyDefinition();
              members.put(methodName, accessorDefinition);
            }
            if (functionDeclaration.isGetter()) {
              accessorDefinition.get = accessorName;
              if (functionDeclaration.isOverride() && accessorDefinition.set == null) {
                // inherit non-overwritten counterpart (may be undefined, and may be overwritten later):
                accessorDefinition.set = "super$.__lookupSetter__('" + methodName + "')";
              }
            } else {
              accessorDefinition.set = accessorName;
              if (functionDeclaration.isOverride() && accessorDefinition.get == null) {
                // inherit non-overwritten counterpart (may be undefined, and may be overwritten later):
                accessorDefinition.get = "super$.__lookupGetter__('" + methodName + "')";
              }
            }
            if (overriddenMethodName != null) {
              overriddenPropertyDefinition = new PropertyDefinition();
              overriddenPropertyDefinition.get = "super$.__lookupGetter__('" + methodName + "')";
              overriddenPropertyDefinition.set = "super$.__lookupSetter__('" + methodName + "')";
            }
          }
        } else {
          writeSymbolReplacement(functionSymbol, functionName);
          if (!functionDeclaration.isPrimaryDeclaration() && !functionDeclaration.isPrivateStatic()) {
            members.put(functionDeclaration.isConstructor() ? "constructor" : methodName,
                    new PropertyDefinition(functionName, functionDeclaration.getAnnotation(Jooc.EXT_PRIVATE_ANNOTATION_NAME) != null));
            if (overriddenMethodName != null) {
              overriddenPropertyDefinition = new PropertyDefinition("super$." + methodName);
            }
          }
        }
        if (overriddenMethodName != null) {
          members.put(overriddenMethodName, overriddenPropertyDefinition);
        }
        generateFunTailCode(functionDeclaration.getFun());
      }
      processMixinAnnotations(functionDeclaration, functionName, methodName);
    }
  }

  // See https://docs.sencha.com/extjs/6.5.3/classic/Ext.Mixin.html
  private void processMixinAnnotations(FunctionDeclaration functionDeclaration, String functionName, String jsMethodName) {
    for (Annotation annotation : functionDeclaration.getAnnotations(Jooc.MIXIN_HOOK_ANNOTATION_NAME)) {
      Map propertiesByName = annotation.getPropertiesByName();
      for (Map.Entry propertyWithValues : propertiesByName.entrySet()) {
        String mixinHookType = propertyWithValues.getKey();
        if (mixinHookType == null) {
          mixinHookType = Jooc.MIXIN_HOOK_ANNOTATION_DEFAULT_ATTRIBUTE_NAME;
        }
        if (Jooc.MIXIN_HOOK_ANNOTATION_ATTRIBUTE_NAMES.contains(mixinHookType)) {
          JsonObject mixinConfig = getClassDefinitionBuilder(functionDeclaration).mixinConfig;
          if (Jooc.MIXIN_HOOK_ANNOTATION_EXTENDED_ATTRIBUTE_NAME.equals(mixinHookType)) {
            mixinConfig.set(mixinHookType, JsonObject.code(functionName));
          } else {
            JsonObject mixinKeyConfig = (JsonObject) mixinConfig.get(mixinHookType);
            if (mixinKeyConfig == null) {
              mixinKeyConfig = new JsonObject();
              mixinConfig.set(mixinHookType, mixinKeyConfig);
            }
            Object value = propertyWithValues.getValue();
            if (value instanceof String) {
              mixinKeyConfig.set((String) value, jsMethodName);
            } else if (value instanceof List) {
              @SuppressWarnings("unchecked")
              List values = (List) value;
              for (String item : values) {
                mixinKeyConfig.set(item, jsMethodName);
              }
            }
          }
        } else {
          throw Jooc.error(annotation, "Invalid [MixinHook] attribute '" + mixinHookType + "'.");
        }
      }
    }
  }

  @Override
  public void visitClassDeclaration(ClassDeclaration classDeclaration) throws IOException {
    visitAll(classDeclaration.getAnnotations());
    writeExtDefineCodePrefix(classDeclaration);
    List currentMetadata = buildMetadata(classDeclaration);
    ClassDefinitionBuilder classDefinitionBuilder = classDeclaration.isPrimaryDeclaration()
            ? primaryClassDefinitionBuilder : (secondaryClassDefinitionBuilder = new ClassDefinitionBuilder());
    classDefinitionBuilder.storeCurrentMetadata("", currentMetadata);
    out.beginComment();
    writeModifiers(classDeclaration);
    out.writeSymbol(classDeclaration.getSymClass());
    classDeclaration.getIde().visit(this);
    visitIfNotNull(classDeclaration.getOptExtends());
    visitIfNotNull(classDeclaration.getOptImplements());
    out.endComment();
    classDeclaration.getBody().visit(this);

    for (IdeDeclaration secondaryDeclaration : classDeclaration.getSecondaryDeclarations()) {
      String secondaryDeclarationName = secondaryDeclaration.getName();
      secondaryDeclaration.visit(this);
      JsonObject secondaryClassDefinition = createClassDefinition(secondaryDeclaration, secondaryClassDefinitionBuilder);
      out.write(new MessageFormat("var {0}$static = Ext.define(null, ").format(secondaryDeclarationName));
      out.write(secondaryClassDefinition.toString(-1, -1));
      out.write(");");
    }

    if (!classDeclaration.isInterface() && classDeclaration.getConstructor() == null
            && needsSuperCallCodeGenerator(classDeclaration)) {
      // generate default constructor that calls field initializers:
      String constructorName = classDeclaration.getName() + "$";
      out.write("function " + constructorName + "() {");
      new SuperCallCodeGenerator(classDeclaration).generate(out, true);
      out.write("}");
      classDefinitionBuilder.members.put("constructor", new PropertyDefinition(constructorName));
    }
  }

  private void writeExtDefineCodePrefix(IdeDeclaration declaration) throws IOException {
    if (declaration.isPrimaryDeclaration()) {
      out.writeSymbolWhitespace(declaration.getSymbol());
      out.write("Ext.define(");
      out.write(CompilerUtils.quote(declaration.getTargetQualifiedNameStr()));
      out.write(", function(" + declaration.getName() + ") {");
    }
  }

  private List buildMetadata(Declaration declaration) {
    List metadata = new LinkedList<>();
    for (Annotation annotation : declaration.getAnnotations()) {
      final Metadata m = new Metadata(annotation.getIde().getName());
      m.args = buildMetadataArgs(annotation);
      metadata.add(m);
    }
    return metadata;
  }

  private List buildMetadataArgs(Annotation annotation) {
    CommaSeparatedList annotationParameters = annotation.getOptAnnotationParameters();
    if (annotationParameters == null) {
      return Collections.emptyList();
    }
    List args = new LinkedList<>();
    for (; annotationParameters != null; annotationParameters = annotationParameters.getTail()) {
      final AnnotationParameter annotationParameter = annotationParameters.getHead();
      Ide optName = annotationParameter.getOptName();
      AstNode optValue = annotationParameter.getValue();
      String name = optName == null ? DEFAULT_ANNOTATION_PARAMETER_NAME : optName.getName();
      Object value;
      if (optValue instanceof LiteralExpr) {
        value = ((LiteralExpr) optValue).getValue().getJooValue();
      } else if (optValue instanceof Ide) {
        IdeDeclaration ideDeclaration = ((Ide) optValue).getDeclaration();
        value = JsonObject.code(compilationUnitAccessCode(ideDeclaration));
      } else {
        value = null;
      }
      args.add(new MetadataArgument(name, value));
    }
    return args;
  }

  public static boolean needsSuperCallCodeGenerator(ClassDeclaration classDeclaration) {
    return classDeclaration.notExtendsObject()                         // only if class has a "real" superclass
            || !classDeclaration.getFieldsWithInitializer().isEmpty(); // or there are field initializers
  }

  private class SuperCallCodeGenerator implements CodeGenerator {
    private ClassDeclaration classDeclaration;

    public SuperCallCodeGenerator(ClassDeclaration classDeclaration) {
      this.classDeclaration = classDeclaration;
    }

    @Override
    public void generate(JsWriter out, boolean first) throws IOException {
      generateSuperConstructorCallCode(classDeclaration, null);
      out.writeToken(";");
    }
  }

  @Override
  public void visitNamespaceDeclaration(NamespaceDeclaration namespaceDeclaration) throws IOException {
    visitAll(namespaceDeclaration.getAnnotations());
    if (namespaceDeclaration.isPrimaryDeclaration()) {
      writeExtDefineCodePrefix(namespaceDeclaration);
    }
    out.beginString();
    writeModifiers(namespaceDeclaration);
    out.writeSymbol(namespaceDeclaration.getSymNamespace());
    namespaceDeclaration.getIde().visit(this);
    out.endString();
    out.writeSymbolWhitespace(namespaceDeclaration.getOptInitializer().getSymEq());
    out.writeToken(",");
    namespaceDeclaration.getOptInitializer().getValue().visit(this);
    writeSymbolReplacement(namespaceDeclaration.getOptSymSemicolon(), ",[]");
    if (namespaceDeclaration.isPrimaryDeclaration()) {
      factory = namespaceDeclaration.getName();
    }
  }

  @Override
  public void visitPackageDeclaration(PackageDeclaration packageDeclaration) throws IOException {
    out.beginComment();
    super.visitPackageDeclaration(packageDeclaration);
    out.endComment();
  }

  @Override
  public void visitSuperConstructorCallStatement(SuperConstructorCallStatement superConstructorCallStatement) throws IOException {
    ClassDeclaration classDeclaration = superConstructorCallStatement.getClassDeclaration();
    if (classDeclaration.notExtendsObject() || !classDeclaration.getFieldsWithInitializer().isEmpty()) {
      out.writeSymbolWhitespace(superConstructorCallStatement.getSymbol());
      generateSuperConstructorCallCode(classDeclaration, superConstructorCallStatement.getArgs());
    } else { // suppress for classes extending Object
      // Object super call does nothing anyway:
      out.beginComment();
      out.writeSymbol(superConstructorCallStatement.getSymbol());
      visitIfNotNull(superConstructorCallStatement.getArgs());
      out.endComment();
    }
    out.writeSymbol(superConstructorCallStatement.getSymSemicolon());
  }

  private void generateSuperConstructorCallCode(ClassDeclaration classDeclaration, ParenthesizedExpr> args) throws IOException {
    String superWithLevel = "super$" + classDeclaration.getQualifiedNameHash();
    out.write("this." + superWithLevel);
    if (args == null) {
      out.writeToken("()");
    } else {
      args.visit(this);
    }
    List callSuperCode = new ArrayList<>();
    callSuperCode.add("function() {\n");
    if (classDeclaration.notExtendsObject()) {
      callSuperCode.add("        " + getSuperClassPrototypeAccessCode() + ".constructor.apply(this, arguments);\n");
    }
    for (VariableDeclaration field : classDeclaration.getFieldsWithInitializer()) {
      callSuperCode.add("        " + field.getName() + "_.call(this);\n");
    }
    callSuperCode.add("      }");
    ClassDefinitionBuilder classDefinitionBuilder;
    if (classDeclaration.isPrimaryDeclaration()) {
      classDefinitionBuilder = primaryClassDefinitionBuilder;
    } else {
      classDefinitionBuilder = secondaryClassDefinitionBuilder;
      // secondary class definition does not allow new-lines:
      callSuperCode = callSuperCode.stream()
              .map(String::trim)
              .collect(Collectors.toList());

    }
    classDefinitionBuilder.members.put(superWithLevel, new PropertyDefinition(String.join("", callSuperCode)));
  }

  private String getSuperClassPrototypeAccessCode() {
    return compilationUnitAccessCode(((ClassDeclaration)compilationUnit.getPrimaryDeclaration()).getSuperTypeDeclaration()) + ".prototype";
  }

  private void generateSuperCallParameters(ParenthesizedExpr> args) throws IOException {
    out.writeToken(".call");
    if (args == null) {
      out.writeToken("(this)");
    } else {
      out.writeSymbolToken(args.getLParen());
      out.writeToken("this");
      out.writeSymbolWhitespace(args.getLParen());
      CommaSeparatedList parameters = args.getExpr();
      if (parameters != null && parameters.getHead() != null) {
        out.writeToken(",");
        parameters.visit(this);
      }
      out.writeSymbol(args.getRParen());
    }
  }

  @Override
  public void visitAnnotation(Annotation annotation) throws IOException {
    out.beginComment();
    super.visitAnnotation(annotation);
    out.endComment();
  }

  @Override
  public void visitUseNamespaceDirective(UseNamespaceDirective useNamespaceDirective) throws IOException {
    out.beginComment();
    super.visitUseNamespaceDirective(useNamespaceDirective);
    out.endComment();
  }

  @Override
  public void visitImportDirective(ImportDirective importDirective) throws IOException {
    out.beginComment();
    super.visitImportDirective(importDirective);
    out.endComment();
  }

  private static class PropertyDefinition {
    String value;
    boolean writable;
    boolean configurable;
    String get;
    String set;
    boolean bindable;
    boolean extPrivate;

    private PropertyDefinition() {
    }

    private PropertyDefinition(String value) {
      this.value = value;
    }

    private PropertyDefinition(String value, boolean extPrivate) {
      this.value = value;
      this.extPrivate = extPrivate;
    }

    private PropertyDefinition(String value, boolean writable, boolean bindable) {
      this.value = value;
      this.writable = writable;
      this.bindable = bindable;
    }

    JsonObject asJson() {
      JsonObject result = new JsonObject();
      if (value != null) {
        result.set("value", JsonObject.code(value));
      }
      if (get != null) {
        result.set("get", JsonObject.code(get));
      }
      if (set != null) {
        result.set("set", JsonObject.code(set));
      }
      if (writable) {
        result.set("writable", true);
      }
      if (configurable) {
        result.set("configurable", true);
      }
      return result;
    }

    Object asAbbreviatedJson() {
      if (isValueOnly()) {
        return JsonObject.code(value);
      }
      return asJson();
    }

    boolean isValueOnly() {
      return /*!writable && !configurable && */ get == null && set == null;
    }
  }

  private static class Metadata {
    String name;
    List args = new ArrayList<>();

    static Metadata find(List metadataList, String name) {
      for (Metadata metadata : metadataList) {
        if (metadata.name.equals(name)) {
          return metadata;
        }
      }
      return null;
    }

    private Metadata(String name) {
      this.name = name;
    }

    public Object getArgumentValue(String argumentName) {
      for (MetadataArgument arg : args) {
        if (arg.name.equals(argumentName)) {
          return arg.value;
        }
      }
      return null;
    }
  }

  private static class MetadataArgument {
    String name;
    Object value;

    private MetadataArgument(String name, Object value) {
      this.name = name;
      this.value = value;
    }
  }

  private static class ClassDefinitionBuilder {
    JsonObject metadata = new JsonObject();
    JsonObject mixinConfig = new JsonObject();
    Map members = new LinkedHashMap();
    Map staticMembers = new LinkedHashMap();
    StringBuilder staticCode = new StringBuilder();
    boolean super$Used = false;

    void storeCurrentMetadata(String memberName, List currentMetadata) {
      Object memberMetadata = metadata.get(memberName);
      List allMetadata = memberMetadata instanceof JsonArray
              ? ((JsonArray) memberMetadata).getItems()
              : new LinkedList<>();
      allMetadata.addAll(compress(currentMetadata));
      if (!allMetadata.isEmpty()) {
        metadata.set(memberName, new JsonArray(allMetadata.toArray()));
      }
    }

    public List compress(List metadataList) {
      List compressedMetadataList = new ArrayList();
      for (Metadata metadata : metadataList) {
        if (!Jooc.ANNOTATIONS_FOR_COMPILER_ONLY.contains(metadata.name)) {
          compressedMetadataList.add(metadata.name);
          if (!metadata.args.isEmpty()) {
            ArrayList argNameValues = new ArrayList();
            for (MetadataArgument metadataArgument : metadata.args) {
              argNameValues.add(metadataArgument.name);
              argNameValues.add(metadataArgument.value);
            }
            compressedMetadataList.add(new JsonArray(argNameValues.toArray()));
          }
        }
      }
      return compressedMetadataList;
    }
  }
}