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

org.jetbrains.plugins.groovy.codeInspection.control.finalVar.GrFinalVariableAccessInspection Maven / Gradle / Ivy

Go to download

A packaging of the IntelliJ Community Edition groovy-psi library. This is release number 1 of trunk branch 142.

The newest version!
/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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 org.jetbrains.plugins.groovy.codeInspection.control.finalVar;

import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.openapi.util.Condition;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyBundle;
import org.jetbrains.plugins.groovy.codeInspection.BaseInspection;
import org.jetbrains.plugins.groovy.codeInspection.BaseInspectionVisitor;
import org.jetbrains.plugins.groovy.codeInspection.utils.ControlFlowUtils;
import org.jetbrains.plugins.groovy.lang.psi.*;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierList;
import org.jetbrains.plugins.groovy.lang.psi.api.formatter.GrControlStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrEnumConstant;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.controlFlow.Instruction;
import org.jetbrains.plugins.groovy.lang.psi.controlFlow.ReadWriteVariableInstruction;
import org.jetbrains.plugins.groovy.lang.psi.controlFlow.impl.ControlFlowBuilder;
import org.jetbrains.plugins.groovy.lang.psi.controlFlow.impl.GrFieldControlFlowPolicy;
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;

import java.util.*;

/**
 * @author Max Medvedev
 */
public class GrFinalVariableAccessInspection extends BaseInspection {
  @NotNull
  @Override
  protected BaseInspectionVisitor buildVisitor() {
    return new BaseInspectionVisitor() {
      @Override
      public void visitMethod(GrMethod method) {
        super.visitMethod(method);

        final GrOpenBlock block = method.getBlock();
        if (block != null) {
          processLocalVars(block);
        }

        if (method.isConstructor()) {
          processFieldsInConstructors(method);
        }
      }

      @Override
      public void visitFile(GroovyFileBase file) {
        super.visitFile(file);

        if (file instanceof GroovyFile && file.isScript()) {
          processLocalVars(file);
        }
      }

      @Override
      public void visitField(GrField field) {
        super.visitField(field);

        final GrExpression initializer = field.getInitializerGroovy();
        if (initializer != null) {
          processLocalVars(initializer);
        }

        if (field.hasModifierProperty(PsiModifier.FINAL)) {
          if (!isFieldInitialized(field)) {
            registerError(field.getNameIdentifierGroovy(),
                          GroovyBundle.message("variable.0.might.not.have.been.initialized", field.getName()), LocalQuickFix.EMPTY_ARRAY,
                          ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
          }
        }
      }

      @Override
      public void visitReferenceExpression(GrReferenceExpression ref) {
        super.visitReferenceExpression(ref);

        final PsiElement resolved = ref.resolve();
        if (resolved instanceof GrField && ((GrField)resolved).hasModifierProperty(PsiModifier.FINAL)) {
          final GrField field = (GrField)resolved;
          final PsiClass containingClass = field.getContainingClass();

          if (PsiUtil.isLValue(ref)) {
            if (containingClass == null || !PsiTreeUtil.isAncestor(containingClass, ref, true)) {
              registerError(ref, GroovyBundle.message("cannot.assign.a.value.to.final.field.0", field.getName()), LocalQuickFix.EMPTY_ARRAY,
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
            }
          }
          else if (PsiUtil.isUsedInIncOrDec(ref)) {
            if (containingClass == null || !isInsideConstructorOrInitializer(containingClass, ref, field.hasModifierProperty(PsiModifier.STATIC))) {
              registerError(ref, GroovyBundle.message("cannot.assign.a.value.to.final.field.0", field.getName()), LocalQuickFix.EMPTY_ARRAY,
                            ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
            }
          }
        }
        else if (resolved instanceof GrParameter &&
                 ((GrParameter)resolved).getDeclarationScope() instanceof GrMethod &&
                 ((GrParameter)resolved).hasModifierProperty(PsiModifier.FINAL) &&
                 PsiUtil.isUsedInIncOrDec(ref)) {
          registerError(ref, GroovyBundle.message("cannot.assign.a.value.to.final.parameter.0", ((GrParameter)resolved).getName()),
                        LocalQuickFix.EMPTY_ARRAY, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
        }
      }

      @Override
      public void visitClassInitializer(GrClassInitializer initializer) {
        super.visitClassInitializer(initializer);

        processLocalVars(initializer.getBlock());
        processFieldsInClassInitializer(initializer);
      }

      private void processFieldsInConstructors(@NotNull GrMethod constructor) {
        final GrOpenBlock block = constructor.getBlock();
        if (block == null) return;

        final GrTypeDefinition clazz = (GrTypeDefinition)constructor.getContainingClass();
        if (clazz == null) return;

        final GrClassInitializer[] initializers = clazz.getInitializers();
        final List fields = getFinalFields(clazz);

        Set initializedFields = ContainerUtil.newHashSet();
        appendFieldInitializedInDeclaration(false, fields, initializedFields);
        appendFieldsInitializedInClassInitializer(initializers, null, false, fields, initializedFields);
        appendInitializationFromChainedConstructors(constructor, fields, initializedFields);

        final Instruction[] flow = buildFlowForField(block);
        final Map variables = buildVarMap(fields, false);

        highlightInvalidWriteAccess(flow, variables, initializedFields);

      }

      private void processFieldsInClassInitializer(@NotNull GrClassInitializer initializer) {
        final GrTypeDefinition clazz = (GrTypeDefinition)initializer.getContainingClass();
        if (clazz == null) return;

        final boolean isStatic = initializer.isStatic();

        final GrClassInitializer[] initializers = clazz.getInitializers();
        final List fields = getFinalFields(clazz);

        Set initializedFields = ContainerUtil.newHashSet();
        appendFieldInitializedInDeclaration(isStatic, fields, initializedFields);
        appendFieldsInitializedInClassInitializer(initializers, initializer, isStatic, fields, initializedFields);

        final Instruction[] flow = buildFlowForField(initializer.getBlock());
        final Map variables = buildVarMap(fields, isStatic);
        highlightInvalidWriteAccess(flow, variables, initializedFields);
      }

      private void processLocalVars(@NotNull GroovyPsiElement scope) {
        final MultiMap scopes = collectVariables(scope);

        for (final Map.Entry> entry : scopes.entrySet()) {
          final PsiElement scopeToProcess = entry.getKey();

          final Set forInParameters = ContainerUtil.newHashSet();
          final Map variables = ContainerUtil.newHashMap();
          for (final GrVariable var : entry.getValue()) {
            variables.put(var.getName(), var);
            if (var instanceof GrParameter && ((GrParameter)var).getDeclarationScope() instanceof GrForStatement) {
              forInParameters.add(var);
            }
          }

          final Instruction[] flow = getFlow(scopeToProcess);
          highlightInvalidWriteAccess(flow, variables, forInParameters);
        }
      }

      private void highlightInvalidWriteAccess(@NotNull Instruction[] flow,
                                               @NotNull Map variables,
                                               @NotNull Set initializedVariables) {
        final List result =
          InvalidWriteAccessSearcher.findInvalidWriteAccess(flow, variables, initializedVariables);

        if (result == null) return;

        for (final ReadWriteVariableInstruction instruction : result) {
          if (variables.containsKey(instruction.getVariableName())) {
            registerError(instruction.getElement(),
                          GroovyBundle.message("cannot.assign.a.value.to.final.field.0", instruction.getVariableName()),
                          LocalQuickFix.EMPTY_ARRAY, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
          }
        }
      }
    };
  }

  private static boolean isInsideConstructorOrInitializer(@NotNull PsiClass containingClass, @NotNull GrReferenceExpression place, boolean isStatic) {
    PsiElement container = ControlFlowUtils.findControlFlowOwner(place);

    PsiClass aClass = null;
    if (!isStatic && container instanceof GrMethod && ((GrMethod)container).isConstructor()) {
      aClass = ((GrMethod)container).getContainingClass();
    }
    else if (container instanceof GrClassInitializer && ((GrClassInitializer)container).isStatic() == isStatic) {
      aClass = ((GrClassInitializer)container).getContainingClass();
    }
    return aClass != null && containingClass.getManager().areElementsEquivalent(aClass, containingClass);
  }

  @NotNull
  private static List getFinalFields(@NotNull GrTypeDefinition clazz) {
    final GrField[] fields = clazz.getCodeFields();
    return ContainerUtil.filter(fields, new Condition() {
      @Override
      public boolean value(GrField field) {
        final GrModifierList list = field.getModifierList();
        return list != null && list.hasModifierProperty(PsiModifier.FINAL);
      }
    });
  }

  private static void appendFieldInitializedInDeclaration(boolean isStatic,
                                                          @NotNull List fields,
                                                          @NotNull Set initializedFields) {
    for (GrField field : fields) {
      if (field.hasModifierProperty(PsiModifier.STATIC) == isStatic && field.getInitializerGroovy() != null) {
        initializedFields.add(field);
      }
    }
  }

  private static void appendFieldsInitializedInClassInitializer(@NotNull GrClassInitializer[] initializers,
                                                                @Nullable GrClassInitializer initializerToStop,
                                                                boolean isStatic,
                                                                @NotNull List fields,
                                                                @NotNull Set initializedFields) {
    for (GrClassInitializer curInit : initializers) {
      if (curInit.isStatic() != isStatic) continue;
      if (curInit == initializerToStop) break;

      final GrOpenBlock block = curInit.getBlock();
      final Instruction[] flow = buildFlowForField(block);

      for (GrField field : fields) {
        if (field.hasModifierProperty(PsiModifier.STATIC) == isStatic &&
            !initializedFields.contains(field) &&
            VariableInitializationChecker.isVariableDefinitelyInitializedCached(field, block, flow)) {
          initializedFields.add(field);
        }
      }
    }
  }

  private static void appendInitializationFromChainedConstructors(@NotNull GrMethod constructor,
                                                                  @NotNull List fields,
                                                                  @NotNull Set initializedFields) {
    final List chained = getChainedConstructors(constructor);
    chained.remove(0);

    for (GrMethod method : chained) {
      final GrOpenBlock block = method.getBlock();
      if (block == null) continue;

      final Instruction[] flow = buildFlowForField(block);

      for (GrField field : fields) {
        if (!field.hasModifierProperty(PsiModifier.STATIC) &&
            !initializedFields.contains(field) &&
            VariableInitializationChecker.isVariableDefinitelyInitializedCached(field, block, flow)) {
          initializedFields.add(field);
        }
      }
    }
  }

  @NotNull
  private static Map buildVarMap(@NotNull List fields, boolean isStatic) {
    Map result = ContainerUtil.newHashMap();
    for (GrField field : fields) {
      if (field.hasModifierProperty(PsiModifier.STATIC) == isStatic) {
        result.put(field.getName(), field);
      }
    }
    return result;
  }

  private static boolean isFieldInitialized(@NotNull GrField field) {
    if (field instanceof GrEnumConstant) return true;
    if (field.getInitializerGroovy() != null) return true;

    if (isImmutableField(field)) return true;

    final boolean isStatic = field.hasModifierProperty(PsiModifier.STATIC);

    final GrTypeDefinition aClass = ((GrTypeDefinition)field.getContainingClass());
    if (aClass == null) return true;

    GrClassInitializer[] initializers = aClass.getInitializers();
    for (GrClassInitializer initializer : initializers) {
      if (initializer.isStatic() != isStatic) continue;

      final GrOpenBlock block = initializer.getBlock();
      final Instruction[] initializerFlow = buildFlowForField(block);
      if (VariableInitializationChecker.isVariableDefinitelyInitializedCached(field, block, initializerFlow)) {
        return true;
      }
    }

    if (isStatic) return false;

    final GrMethod[] constructors = aClass.getCodeConstructors();
    if (constructors.length == 0) return false;

    Set initializedConstructors = ContainerUtil.newHashSet();
    Set notInitializedConstructors = ContainerUtil.newHashSet();

    NEXT_CONSTR:
    for (GrMethod constructor : constructors) {
      if (constructor.getBlock() == null) return false;
      final List chained = getChainedConstructors(constructor);

      NEXT_CHAINED:
      for (GrMethod method : chained) {
        if (initializedConstructors.contains(method)) {
          continue NEXT_CONSTR;
        }
        else if (notInitializedConstructors.contains(method)) {
          continue NEXT_CHAINED;
        }

        final GrOpenBlock block = method.getBlock();
        assert block != null;
        final boolean initialized =
          VariableInitializationChecker.isVariableDefinitelyInitializedCached(field, block, buildFlowForField(block));

        if (initialized) {
          initializedConstructors.add(method);
          continue NEXT_CONSTR;
        }
        else {
          notInitializedConstructors.add(method);
        }
      }

      return false;
    }
    return true;
  }

  private static boolean isImmutableField(@NotNull GrField field) {
    GrModifierList fieldModifierList = field.getModifierList();
    if (fieldModifierList != null && fieldModifierList.hasExplicitVisibilityModifiers()) return false;

    PsiClass aClass = field.getContainingClass();
    if (aClass == null) return false;

    PsiModifierList modifierList = aClass.getModifierList();
    if (modifierList == null) return false;

    return PsiImplUtil.hasImmutableAnnotation(modifierList);
  }

  @NotNull
  private static List getChainedConstructors(@NotNull GrMethod constructor) {
    final HashSet visited = ContainerUtil.newHashSet();

    final ArrayList result = ContainerUtil.newArrayList(constructor);
    while (true) {
      final GrConstructorInvocation invocation = PsiUtil.getConstructorInvocation(constructor);
      if (invocation != null && invocation.isThisCall()) {
        final PsiMethod method = invocation.resolveMethod();
        if (method != null && method.isConstructor() && visited.add(method)) {
          result.add((GrMethod)method);
          constructor = (GrMethod)method;
          continue;
        }
      }
      return result;
    }
  }

  @NotNull
  private static Instruction[] buildFlowForField(@NotNull GrOpenBlock block) {
    return new ControlFlowBuilder(block.getProject(), GrFieldControlFlowPolicy.getInstance()).buildControlFlow(block);
  }


  /**
   * @return map: scope -> variables defined in the scope
   */
  @NotNull
  private static MultiMap collectVariables(@NotNull GroovyPsiElement scope) {
    final MultiMap scopes = MultiMap.create();
    scope.accept(new GroovyRecursiveElementVisitor() {
      @Override
      public void visitVariable(GrVariable variable) {
        super.visitVariable(variable);
        if (!(variable instanceof PsiField) && variable.hasModifierProperty(PsiModifier.FINAL)) {
          final PsiElement varScope = findScope(variable);
          if (varScope != null) {
            scopes.putValue(varScope, variable);
          }
        }
      }
    });
    return scopes;
  }

  @NotNull
  private static Instruction[] getFlow(@NotNull PsiElement element) {
    return element instanceof GrControlFlowOwner
           ? ((GrControlFlowOwner)element).getControlFlow()
           : new ControlFlowBuilder(element.getProject()).buildControlFlow((GroovyPsiElement)element);
  }


  @Nullable
  private static PsiElement findScope(@NotNull GrVariable variable) {
    GroovyPsiElement result = PsiTreeUtil.getParentOfType(variable, GrControlStatement.class, GrControlFlowOwner.class);
    if (result instanceof GrForStatement) {
      final GrStatement body = ((GrForStatement)result).getBody();
      if (body != null) {
        result = body;
      }
    }
    return result;
  }
}