com.intellij.plugins.haxe.ide.annotator.HaxeSemanticAnnotator.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.plugins.haxe.ide.annotator.HaxeSemanticAnnotator.java

Source

/*
 * Copyright 2000-2013 JetBrains s.r.o.
 * Copyright 2014-2015 AS3Boyan
 * Copyright 2014-2014 Elias Ku
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.plugins.haxe.ide.annotator;

import com.intellij.lang.annotation.Annotation;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.plugins.haxe.HaxeBundle;
import com.intellij.plugins.haxe.lang.lexer.HaxeTokenTypes;
import com.intellij.plugins.haxe.lang.psi.*;
import com.intellij.plugins.haxe.model.*;
import com.intellij.plugins.haxe.model.fixer.HaxeFixer;
import com.intellij.plugins.haxe.model.fixer.HaxeModifierAddFixer;
import com.intellij.plugins.haxe.model.fixer.HaxeModifierRemoveFixer;
import com.intellij.plugins.haxe.model.fixer.HaxeModifierReplaceVisibilityFixer;
import com.intellij.plugins.haxe.model.type.HaxeTypeCompatible;
import com.intellij.plugins.haxe.model.type.HaxeTypeResolver;
import com.intellij.plugins.haxe.model.type.ResultHolder;
import com.intellij.plugins.haxe.util.HaxeAbstractEnumUtil;
import com.intellij.plugins.haxe.util.HaxeResolveUtil;
import com.intellij.plugins.haxe.util.PsiFileUtils;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.*;

import static com.intellij.util.containers.ContainerUtil.getFirstItem;

public class HaxeSemanticAnnotator implements Annotator {
    @Override
    public void annotate(PsiElement element, AnnotationHolder holder) {
        analyzeSingle(element, holder);
    }

    static void analyzeSingle(final PsiElement element, AnnotationHolder holder) {
        if (element instanceof HaxePackageStatement) {
            PackageChecker.check((HaxePackageStatement) element, holder);
        } else if (element instanceof HaxeMethod) {
            MethodChecker.check((HaxeMethod) element, holder);
        } else if (element instanceof HaxeClass) {
            ClassChecker.check((HaxeClass) element, holder);
        } else if (element instanceof HaxeType) {
            TypeChecker.check((HaxeType) element, holder);
        } else if (element instanceof HaxeVarDeclaration) {
            FieldChecker.check((HaxeVarDeclaration) element, holder);
        }
    }
}

class TypeTagChecker {
    public static void check(final PsiElement erroredElement, final HaxeTypeTag tag,
            final HaxeVarInit initExpression, boolean requireConstant, final AnnotationHolder holder) {
        final ResultHolder type1 = HaxeTypeResolver.getTypeFromTypeTag(tag, erroredElement);
        final ResultHolder type2 = getTypeFromVarInit(initExpression);

        final HaxeDocumentModel document = HaxeDocumentModel.fromElement(tag);
        if (!type1.canAssign(type2)) {
            // @TODO: Move to bundle
            Annotation annotation = holder.createErrorAnnotation(erroredElement,
                    "Incompatible type " + type1 + " can't be assigned from " + type2);
            annotation.registerFix(new HaxeFixer("Change type") {
                @Override
                public void run() {
                    document.replaceElementText(tag, ":" + type2.toStringWithoutConstant());
                }
            });
            annotation.registerFix(new HaxeFixer("Remove init") {
                @Override
                public void run() {
                    document.replaceElementText(initExpression, "", StripSpaces.BEFORE);
                }
            });
        } else if (requireConstant && type2.getType().getConstant() == null) {
            // TODO: Move to bundle
            holder.createErrorAnnotation(erroredElement,
                    "Parameter default type should be constant but was " + type2);
        }
    }

    @NotNull
    static ResultHolder getTypeFromVarInit(HaxeVarInit init) {
        final ResultHolder abstractEnumFieldInitType = HaxeAbstractEnumUtil
                .getStaticMemberExpression(init.getExpression());
        if (abstractEnumFieldInitType != null) {
            return abstractEnumFieldInitType;
        }
        // fallback to simple init expression
        return HaxeTypeResolver.getPsiElementType(init);
    }
}

class FieldChecker {
    public static void check(final HaxeVarDeclaration var, final AnnotationHolder holder) {
        HaxeFieldModel field = new HaxeFieldModel(var);
        if (field.isProperty()) {
            checkProperty(field, holder);
        }
        if (field.hasInitializer() && field.hasTypeTag()) {
            TypeTagChecker.check(field.getPsi(), field.getTypeTagPsi(), field.getInitializerPsi(), false, holder);
        }

        // Checking for variable redefinition.
        HashSet<HaxeClassModel> classSet = new HashSet<HaxeClassModel>();
        HaxeClassModel fieldDeclaringClass = field.getDeclaringClass();
        classSet.add(fieldDeclaringClass);
        while (fieldDeclaringClass != null) {
            fieldDeclaringClass = fieldDeclaringClass.getParentClass();
            if (classSet.contains(fieldDeclaringClass)) {
                break;
            } else {
                classSet.add(fieldDeclaringClass);
            }
            if (fieldDeclaringClass != null) {
                for (HaxeFieldModel parentField : fieldDeclaringClass.getFields()) {
                    if (parentField.getName().equals(field.getName())) {
                        if (parentField.isStatic()) {
                            holder.createWarningAnnotation(field.getNameOrBasePsi(),
                                    "Field '" + field.getName() + "' overrides a static field of a superclass.");
                        } else {
                            holder.createErrorAnnotation(field.getDeclarationPsi(),
                                    "Redefinition of variable '" + field.getName()
                                            + "' in subclass is not allowed. Previously declared at '"
                                            + fieldDeclaringClass.getName() + "'.");
                        }
                        break;
                    }
                }
            }
        }
    }

    public static void checkProperty(final HaxeFieldModel field, final AnnotationHolder holder) {
        final HaxeDocumentModel document = field.getDocument();

        if (field.getGetterPsi() != null && !field.getGetterType().isValidGetter()) {
            holder.createErrorAnnotation(field.getGetterPsi(), "Invalid getter accessor");
        }

        if (field.getSetterPsi() != null && !field.getSetterType().isValidSetter()) {
            holder.createErrorAnnotation(field.getSetterPsi(), "Invalid setter accessor");
        }

        checkPropertyAccessorMethods(field, holder);

        if (field.isProperty() && !field.isRealVar() && field.hasInitializer()) {
            final HaxeVarInit psi = field.getInitializerPsi();
            Annotation annotation = holder.createErrorAnnotation(field.getInitializerPsi(),
                    "This field cannot be initialized because it is not a real variable");
            annotation.registerFix(new HaxeFixer("Remove init") {
                @Override
                public void run() {
                    document.replaceElementText(psi, "", StripSpaces.BEFORE);
                }
            });
            annotation.registerFix(new HaxeFixer("Add @:isVar") {
                @Override
                public void run() {
                    field.getModifiers().addModifier(HaxeModifierType.IS_VAR);
                }
            });
            if (field.getSetterPsi() != null) {
                annotation.registerFix(new HaxeFixer("Make setter null") {
                    @Override
                    public void run() {
                        document.replaceElementText(field.getSetterPsi(), "null");
                    }
                });
            }
        }
    }

    static void checkPropertyAccessorMethods(final HaxeFieldModel field, final AnnotationHolder holder) {
        if (field.getDeclaringClass().isInterface()) {
            return;
        }

        if (field.getGetterType() == HaxeAccessorType.GET) {
            final String methodName = "get_" + field.getName();
            HaxeMethodModel method = field.getDeclaringClass().getMethod(methodName);
            if (method == null) {
                Annotation annotation = holder.createErrorAnnotation(field.getGetterPsi(),
                        "Can't find method " + methodName);
                annotation.registerFix(new HaxeFixer("Add method") {
                    @Override
                    public void run() {
                        field.getDeclaringClass().addMethod(methodName);
                    }
                });
            }
        }

        if (field.getSetterType() == HaxeAccessorType.SET) {
            final String methodName = "set_" + field.getName();
            HaxeMethodModel method = field.getDeclaringClass().getMethod(methodName);
            if (method == null) {
                Annotation annotation = holder.createErrorAnnotation(field.getSetterPsi(),
                        "Can't find method " + methodName);
                annotation.registerFix(new HaxeFixer("Add method") {
                    @Override
                    public void run() {
                        field.getDeclaringClass().addMethod(methodName);
                    }
                });
            }
        }
    }
}

class TypeChecker {
    static public void check(final HaxeType type, final AnnotationHolder holder) {
        if (true) {
            // HACK - Find the identifier manually, rather than just getting it from the reference.
            // There is an error in the BNF that maps a number of reference types to HaxeReferenceExpression
            // even though the types do not necessarily have one.  If an identifier doesn't exist,
            // HaxeReferenceExpression.getIdentifier() will throw a NotNull exception; searching the
            // children doesn't.
            HaxeReferenceExpression expression = type.getReferenceExpression();
            HaxeIdentifier identifier = PsiTreeUtil.getChildOfType(expression, HaxeIdentifier.class);
            check(identifier, holder);
        } else {
            check(type.getReferenceExpression().getIdentifier(), holder);
        }
    }

    static public void check(final PsiIdentifier identifier, final AnnotationHolder holder) {
        if (identifier == null)
            return;
        final String typeName = identifier.getText();
        if (!HaxeClassModel.isValidClassName(typeName)) {
            Annotation annotation = holder.createErrorAnnotation(identifier, "Type name must start by upper case");
            annotation.registerFix(new HaxeFixer("Change name") {
                @Override
                public void run() {
                    HaxeDocumentModel.fromElement(identifier).replaceElementText(identifier,
                            typeName.substring(0, 1).toUpperCase() + typeName.substring(1));
                }
            });
        }
    }
}

class ClassChecker {
    static public void check(final HaxeClass clazzPsi, final AnnotationHolder holder) {
        HaxeClassModel clazz = clazzPsi.getModel();
        checkDuplicatedFields(clazz, holder);
        checkClassName(clazz, holder);
        checkInterfaces(clazz, holder);
        checkExtends(clazz, holder);
        checkInterfacesMethods(clazz, holder);
    }

    static private void checkDuplicatedFields(final HaxeClassModel clazz, final AnnotationHolder holder) {
        Map<String, HaxeMemberModel> map = new HashMap<String, HaxeMemberModel>();
        Set<HaxeMemberModel> repeatedMembers = new HashSet<HaxeMemberModel>();
        for (HaxeMemberModel member : clazz.getMembersSelf()) {
            final String memberName = member.getName();
            HaxeMemberModel repeatedMember = map.get(memberName);
            if (repeatedMember != null) {
                repeatedMembers.add(member);
                repeatedMembers.add(repeatedMember);
            } else {
                map.put(memberName, member);
            }
        }

        for (HaxeMemberModel member : repeatedMembers) {
            holder.createErrorAnnotation(member.getNameOrBasePsi(),
                    "Duplicate class field declaration : " + member.getName());
        }

        //Duplicate class field declaration
    }

    static private void checkClassName(final HaxeClassModel clazz, final AnnotationHolder holder) {
        TypeChecker.check(clazz.getNamePsi(), holder);
    }

    static public void checkExtends(final HaxeClassModel clazz, final AnnotationHolder holder) {
        HaxeClassReferenceModel reference = clazz.getParentClassReference();
        if (reference != null) {
            HaxeClassModel aClass1 = reference.getHaxeClass();
            if (aClass1 != null) {
                if (isAnonymousType(clazz)) {
                    if (!isAnonymousType(aClass1)) {
                        // @TODO: Move to bundle
                        holder.createErrorAnnotation(reference.getPsi(), "Not an anonymous type");
                    }
                } else if (clazz.isInterface()) {
                    if (!aClass1.isInterface()) {
                        // @TODO: Move to bundle
                        holder.createErrorAnnotation(reference.getPsi(), "Not an interface");
                    }
                } else if (!aClass1.isClass()) {
                    // @TODO: Move to bundle
                    holder.createErrorAnnotation(reference.getPsi(), "Not a class");
                }

                final String qname1 = aClass1.haxeClass.getQualifiedName();
                final String qname2 = clazz.haxeClass.getQualifiedName();
                if (qname1.equals(qname2)) {
                    // @TODO: Move to bundle
                    holder.createErrorAnnotation(reference.getPsi(), "Cannot extend self");
                }
            }
        }
    }

    static private boolean isAnonymousType(HaxeClassModel clazz) {
        if (clazz != null && clazz.haxeClass != null) {
            HaxeClass haxeClass = clazz.haxeClass;
            if (haxeClass instanceof HaxeAnonymousType) {
                return true;
            }
            if (haxeClass instanceof HaxeTypedefDeclaration) {
                HaxeTypeOrAnonymous anonOrType = getFirstItem(
                        ((HaxeTypedefDeclaration) haxeClass).getTypeOrAnonymousList());
                if (anonOrType != null) {
                    return anonOrType.getAnonymousType() != null;
                }
            }
        }
        return false;
    }

    static public void checkInterfaces(final HaxeClassModel clazz, final AnnotationHolder holder) {
        for (HaxeClassReferenceModel interfaze : clazz.getImplementingInterfaces()) {
            if (interfaze.getHaxeClass() == null || !interfaze.getHaxeClass().isInterface()) {
                // @TODO: Move to bundle
                holder.createErrorAnnotation(interfaze.getPsi(), "Not an interface");
            }
        }
    }

    static public void checkInterfacesMethods(final HaxeClassModel clazz, final AnnotationHolder holder) {
        for (HaxeClassReferenceModel reference : clazz.getImplementingInterfaces()) {
            checkInterfaceMethods(clazz, reference, holder);
        }
    }

    static public void checkInterfaceMethods(final HaxeClassModel clazz, final HaxeClassReferenceModel intReference,
            final AnnotationHolder holder) {
        final List<HaxeMethodModel> missingMethods = new ArrayList<HaxeMethodModel>();
        final List<String> missingMethodsNames = new ArrayList<String>();

        if (intReference.getHaxeClass() != null) {
            for (HaxeMethodModel intMethod : intReference.getHaxeClass().getMethods()) {
                if (!intMethod.isStatic()) {
                    HaxeMethodModel selfMethod = clazz.getMethodSelf(intMethod.getName());
                    if (selfMethod == null) {
                        missingMethods.add(intMethod);
                        missingMethodsNames.add(intMethod.getName());
                    } else {
                        MethodChecker.checkMethodsSignatureCompatibility(selfMethod, intMethod, holder);
                    }
                }
            }
        }

        if (missingMethods.size() > 0) {
            // @TODO: Move to bundle
            Annotation annotation = holder.createErrorAnnotation(intReference.getPsi(),
                    "Not implemented methods: " + StringUtils.join(missingMethodsNames, ", "));
            annotation.registerFix(new HaxeFixer("Implement methods") {
                @Override
                public void run() {
                    clazz.addMethodsFromPrototype(missingMethods);
                }
            });
        }
    }
}

class MethodChecker {
    static public void check(final HaxeMethod methodPsi, final AnnotationHolder holder) {
        final HaxeMethodModel currentMethod = methodPsi.getModel();
        checkTypeTagInInterfacesAndExternClass(currentMethod, holder);
        checkMethodArguments(currentMethod, holder);
        checkOverride(methodPsi, holder);
        if (HaxeSemanticAnnotatorConfig.ENABLE_EXPERIMENTAL_BODY_CHECK) {
            MethodBodyChecker.check(methodPsi, holder);
        }
        //currentMethod.getBodyPsi()
    }

    static public void checkTypeTagInInterfacesAndExternClass(final HaxeMethodModel currentMethod,
            final AnnotationHolder holder) {
        HaxeClassModel currentClass = currentMethod.getDeclaringClass();
        if (currentClass.isExtern() || currentClass.isInterface()) {
            if (currentMethod.getReturnTypeTagPsi() == null) {
                holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(),
                        HaxeBundle.message("haxe.semantic.type.required"));
            }
            for (final HaxeParameterModel param : currentMethod.getParameters()) {
                if (param.getTypeTagPsi() == null) {
                    holder.createErrorAnnotation(param.getNameOrBasePsi(),
                            HaxeBundle.message("haxe.semantic.type.required"));
                }
            }
        }
    }

    static public void checkMethodArguments(final HaxeMethodModel currentMethod, final AnnotationHolder holder) {
        boolean hasOptional = false;
        HashMap<String, PsiElement> argumentNames = new HashMap<String, PsiElement>();
        for (final HaxeParameterModel param : currentMethod.getParameters()) {
            String paramName = param.getName();

            if (param.hasOptionalPsi() && param.getVarInitPsi() != null) {
                // @TODO: Move to bundle
                holder.createWarningAnnotation(param.getOptionalPsi(),
                        "Optional not needed when specified an init value");
            }
            if (param.getVarInitPsi() != null && param.getTypeTagPsi() != null) {
                TypeTagChecker.check(param.getPsi(), param.getTypeTagPsi(), param.getVarInitPsi(), true, holder);
            }
            if (param.isOptional()) {
                hasOptional = true;
            } else if (hasOptional) {
                // @TODO: Move to bundle
                holder.createWarningAnnotation(param.getPsi(), "Non-optional argument after optional argument");
            }

            if (argumentNames.containsKey(paramName)) {
                // @TODO: Move to bundle
                holder.createWarningAnnotation(param.getNameOrBasePsi(),
                        "Repeated argument name '" + paramName + "'");
                holder.createWarningAnnotation(argumentNames.get(paramName),
                        "Repeated argument name '" + paramName + "'");
            } else {
                argumentNames.put(paramName, param.getNameOrBasePsi());
            }
        }
    }

    static public void checkOverride(final HaxeMethod methodPsi, final AnnotationHolder holder) {
        final HaxeMethodModel currentMethod = methodPsi.getModel();
        final HaxeClassModel currentClass = currentMethod.getDeclaringClass();
        final HaxeModifiersModel currentModifiers = currentMethod.getModifiers();

        final HaxeClassReferenceModel parentClass = (currentClass != null) ? currentClass.getParentClassReference()
                : null;
        final HaxeMethodModel parentMethod = ((parentClass != null) && parentClass.getHaxeClass() != null)
                ? parentClass.getHaxeClass().getMethod(currentMethod.getName())
                : null;
        final HaxeModifiersModel parentModifiers = (parentMethod != null) ? parentMethod.getModifiers() : null;

        boolean requiredOverride = false;

        if (currentMethod.isConstructor()) {
            requiredOverride = false;
            if (currentModifiers.hasModifier(HaxeModifierType.STATIC)) {
                // @TODO: Move to bundle
                holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(), "Constructor can't be static")
                        .registerFix(new HaxeModifierRemoveFixer(currentModifiers, HaxeModifierType.STATIC));
            }
        } else if (currentMethod.isStaticInit()) {
            requiredOverride = false;
            if (!currentModifiers.hasModifier(HaxeModifierType.STATIC)) {
                holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(), "__init__ must be static")
                        .registerFix(new HaxeModifierAddFixer(currentModifiers, HaxeModifierType.STATIC));
            }
        } else if (parentMethod != null) {
            if (parentMethod.isStatic()) {
                holder.createWarningAnnotation(currentMethod.getNameOrBasePsi(),
                        "Method '" + currentMethod.getName() + "' overrides a static method of a superclass");
            } else {
                requiredOverride = true;

                if (parentModifiers.hasAnyModifier(HaxeModifierType.INLINE, HaxeModifierType.STATIC,
                        HaxeModifierType.FINAL)) {
                    Annotation annotation = holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(),
                            "Can't override static, inline or final methods");
                    for (HaxeModifierType mod : new HaxeModifierType[] { HaxeModifierType.FINAL,
                            HaxeModifierType.INLINE, HaxeModifierType.STATIC }) {
                        if (parentModifiers.hasModifier(mod)) {
                            annotation.registerFix(new HaxeModifierRemoveFixer(parentModifiers, mod,
                                    "Remove " + mod.s + " from " + parentMethod.getFullName()));
                        }
                    }
                }

                if (currentModifiers.getVisibility().hasLowerVisibilityThan(parentModifiers.getVisibility())) {
                    Annotation annotation = holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(),
                            "Field " + currentMethod.getName()
                                    + " has less visibility (public/private) than superclass one");
                    annotation.registerFix(new HaxeModifierReplaceVisibilityFixer(currentModifiers,
                            parentModifiers.getVisibility(), "Change current method visibility"));
                    annotation.registerFix(new HaxeModifierReplaceVisibilityFixer(parentModifiers,
                            currentModifiers.getVisibility(), "Change parent method visibility"));
                }
            }
        }

        //System.out.println(aClass);
        if (currentModifiers.hasModifier(HaxeModifierType.OVERRIDE) && !requiredOverride) {
            holder.createErrorAnnotation(currentModifiers.getModifierPsi(HaxeModifierType.OVERRIDE),
                    "Overriding nothing")
                    .registerFix(new HaxeModifierRemoveFixer(currentModifiers, HaxeModifierType.OVERRIDE));
        } else if (requiredOverride) {
            if (!currentModifiers.hasModifier(HaxeModifierType.OVERRIDE)) {
                holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(), "Must override")
                        .registerFix(new HaxeModifierAddFixer(currentModifiers, HaxeModifierType.OVERRIDE));
            } else {
                // It is rightly overriden. Now check the signature.
                checkMethodsSignatureCompatibility(currentMethod, parentMethod, holder);
            }
        }
    }

    static public void checkMethodsSignatureCompatibility(@NotNull final HaxeMethodModel currentMethod,
            @NotNull final HaxeMethodModel parentMethod, final AnnotationHolder holder) {
        final HaxeDocumentModel document = currentMethod.getDocument();

        List<HaxeParameterModel> currentParameters = currentMethod.getParameters();
        final List<HaxeParameterModel> parentParameters = parentMethod.getParameters();
        int minParameters = Math.min(currentParameters.size(), parentParameters.size());

        if (currentParameters.size() > parentParameters.size()) {
            for (int n = minParameters; n < currentParameters.size(); n++) {
                final HaxeParameterModel currentParam = currentParameters.get(n);
                holder.createErrorAnnotation(currentParam.getPsi(), "Unexpected argument")
                        .registerFix(new HaxeFixer("Remove argument") {
                            @Override
                            public void run() {
                                currentParam.remove();
                            }
                        });
            }
        } else if (currentParameters.size() != parentParameters.size()) {
            holder.createErrorAnnotation(currentMethod.getNameOrBasePsi(), "Not matching arity expected "
                    + parentParameters.size() + " arguments but found " + currentParameters.size());
        }

        for (int n = 0; n < minParameters; n++) {
            final HaxeParameterModel currentParam = currentParameters.get(n);
            final HaxeParameterModel parentParam = parentParameters.get(n);
            if (!HaxeTypeCompatible.canAssignToFrom(currentParam.getType(), parentParam.getType())) {
                holder.createErrorAnnotation(currentParam.getPsi(),
                        "Type " + currentParam.getType() + " is not compatible with " + parentParam.getType())
                        .registerFix(new HaxeFixer("Change type") {
                            @Override
                            public void run() {
                                document.replaceElementText(currentParam.getTypeTagPsi(),
                                        parentParam.getTypeTagPsi().getText());
                            }
                        });
            }
        }

        ResultHolder currentResult = currentMethod.getResultType();
        ResultHolder parentResult = parentMethod.getResultType();
        if (!currentResult.canAssign(parentResult)) {
            PsiElement psi = currentMethod.getReturnTypeTagOrNameOrBasePsi();
            holder.createErrorAnnotation(psi,
                    "Not compatible return type " + currentResult + " != " + parentResult);
        }
    }
}

class PackageChecker {
    static public void check(final HaxePackageStatement element, final AnnotationHolder holder) {
        final HaxeReferenceExpression expression = element.getReferenceExpression();
        String packageName = (expression != null) ? expression.getText() : "";
        PsiDirectory fileDirectory = element.getContainingFile().getParent();
        List<PsiFileSystemItem> fileRange = PsiFileUtils.getRange(PsiFileUtils.findRoot(fileDirectory),
                fileDirectory);
        fileRange.remove(0);
        String actualPath = PsiFileUtils.getListPath(fileRange);
        final String actualPackage = actualPath.replace('/', '.');
        final String actualPackage2 = HaxeResolveUtil.getPackageName(element.getContainingFile());
        // @TODO: Should use HaxeResolveUtil

        for (String s : StringUtils.split(packageName, '.')) {
            if (!s.substring(0, 1).toLowerCase().equals(s.substring(0, 1))) {
                //HaxeSemanticError.addError(element, new HaxeSemanticError("Package name '" + s + "' must start with a lower case character"));
                // @TODO: Move to bundle
                holder.createErrorAnnotation(element,
                        "Package name '" + s + "' must start with a lower case character");
            }
        }

        if (!packageName.equals(actualPackage)) {
            holder.createErrorAnnotation(element,
                    "Invalid package name! '" + packageName + "' should be '" + actualPackage + "'")
                    .registerFix(new HaxeFixer("Fix package") {
                        @Override
                        public void run() {
                            Document document = PsiDocumentManager.getInstance(element.getProject())
                                    .getDocument(element.getContainingFile());

                            if (expression != null) {
                                TextRange range = expression.getTextRange();
                                document.replaceString(range.getStartOffset(), range.getEndOffset(), actualPackage);
                            } else {
                                int offset = element.getNode().findChildByType(HaxeTokenTypes.OSEMI).getTextRange()
                                        .getStartOffset();
                                document.replaceString(offset, offset, actualPackage);
                            }
                        }
                    });
        }
    }
}

class MethodBodyChecker {
    public static void check(HaxeMethod psi, AnnotationHolder holder) {
        final HaxeMethodModel method = psi.getModel();
        HaxeTypeResolver.getPsiElementType(method.getBodyPsi(), holder);
    }
}