@Override
  public boolean processChangedClass(DifferentiateContext context, Difference.Change<JvmClass, JvmClass.Diff> change, Utils future, Utils present) {
    JvmClass changedClass = change.getPast();
    JvmClass.Diff classDiff = change.getDiff();
    debug("Processing changed class ", changedClass.getName());
    
    if (classDiff.superClassChanged() || classDiff.signatureChanged() || !classDiff.interfaces().unchanged()) {
      boolean extendsChanged = classDiff.superClassChanged() && !classDiff.extendsAdded();
      boolean affectUsages = classDiff.signatureChanged() || extendsChanged || !classDiff.interfaces().unchanged();
      affectSubclasses(context, future, change.getNow().getReferenceID(), affectUsages);

      if (extendsChanged) {
        TypeRepr.ClassType exClass = new TypeRepr.ClassType(changedClass.getName());
        for (JvmClass depClass : flat(map(context.getGraph().getDependingNodes(changedClass.getReferenceID()), dep -> present.getNodes(dep, JvmClass.class)))) {
          for (JvmMethod method : depClass.getMethods()) {
            if (contains(method.getExceptions(), exClass)) {
              context.affectUsage(method.createUsage(depClass.getReferenceID()));
              debug("Affecting usages of methods throwing ", exClass.getJvmName(), " exception; class ", depClass.getName());
            }
          }
        }
      }

      if (!changedClass.isAnonymous()) {
        Set<JvmNodeReferenceID> parents = collect(present.allSupertypes(changedClass.getReferenceID()), new HashSet<>());
        parents.removeAll(collect(future.allSupertypes(changedClass.getReferenceID()), new HashSet<>()));
        for (JvmNodeReferenceID parent : parents) {
          debug("Affecting usages in generic type parameter bounds of class: ", parent);
          context.affectUsage(new ClassAsGenericBoundUsage(parent));
        }
      }
    }
    else if (change.getNow().isSealed()) {
      ReferenceID fromClass = change.getNow().getReferenceID();
      Set<ReferenceID> permitted = collect(map(filter(change.getNow().getUsages(), u -> u instanceof ClassPermitsUsage), Usage::getElementOwner), new SmartHashSet<>());
      debug("Affecting non-permitted subclasses of a sealed class: ", fromClass);
      for (ReferenceID cl : filter(future.directSubclasses(fromClass), c -> !permitted.contains(c))) {
        affectNodeSources(context, cl, "Affecting source file of a non-permitted subclass: ", future);
      }
    }

    JVMFlags addedFlags = classDiff.getAddedFlags();

    if (addedFlags.isInterface() || classDiff.getRemovedFlags().isInterface()) {
      debug("Class-to-interface or interface-to-class conversion detected, added class usage to affected usages");
      context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
    }

    if (changedClass.isAnnotation() && changedClass.getRetentionPolicy() == RetentionPolicy.SOURCE) {
      debug("Annotation, retention policy = SOURCE => a switch to non-incremental mode requested");
      if (!affectOnNonIncrementalChange(context, changedClass.getReferenceID(), changedClass, present)) {
        debug("End of Differentiate, returning false");
        return false;
      }
    }

    if (addedFlags.isProtected()) {
      debug("Introduction of 'protected' modifier detected, adding class usage + inheritance constraint to affected usages");
      context.affectUsage(new ClassUsage(changedClass.getReferenceID()), new InheritanceConstraint(future, changedClass));
    }

    if (!changedClass.getFlags().isPackageLocal() && change.getNow().getFlags().isPackageLocal()) {
      debug("Introduction of 'package-private' access detected, adding class usage + package constraint to affected usages");
      context.affectUsage(new ClassUsage(changedClass.getReferenceID()), new PackageConstraint(changedClass.getPackageName()));
    }

    if (addedFlags.isFinal() || addedFlags.isPrivate()) {
      debug("Introduction of 'private' or 'final' modifier(s) detected, adding class usage to affected usages");
      context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
    }

    if (addedFlags.isAbstract() || addedFlags.isStatic()) {
      debug("Introduction of 'abstract' or 'static' modifier(s) detected, adding class new usage to affected usages");
      context.affectUsage(new ClassNewUsage(changedClass.getReferenceID()));
    }

    if (!changedClass.isAnonymous() && !changedClass.isPrivate() && classDiff.flagsChanged() && changedClass.isInnerClass()) {
      debug("Some modifiers (access flags) were changed for non-private inner class, adding class usage to affected usages");
      context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
    }

    if (changedClass.isAnnotation()) {
      debug("Class is annotation, performing annotation-specific analysis");

      if (classDiff.retentionPolicyChanged()) {
        debug("Retention policy change detected, adding class usage to affected usages");
        context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
      }
      else if (classDiff.targetAttributeCategoryMightChange()) {
        debug("Annotation's attribute category in bytecode might be affected because of TYPE_USE or RECORD_COMPONENT target, adding class usage to affected usages");
        context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
      }
      else {
        Difference.Specifier<ElemType, ?> targetsDiff = classDiff.annotationTargets();
        Set<ElemType> removedTargets = collect(targetsDiff.removed(), EnumSet.noneOf(ElemType.class));

        if (removedTargets.contains(ElemType.LOCAL_VARIABLE)) {
          debug("Removed target contains LOCAL_VARIABLE => a switch to non-incremental mode requested");
          if (!affectOnNonIncrementalChange(context, changedClass.getReferenceID(), changedClass, present)) {
            debug("End of Differentiate, returning false");
            return false;
          }
        }

        if (!removedTargets.isEmpty()) {
          debug("Removed some annotation targets, adding annotation query");
          TypeRepr.ClassType classType = new TypeRepr.ClassType(changedClass.getName());
          context.affectUsage(asIterable(changedClass.getReferenceID()), node -> {
            for (Usage usage : node.getUsages()) {
              if (usage instanceof AnnotationUsage) {
                AnnotationUsage annotUsage = (AnnotationUsage)usage;
                if (classType.equals(annotUsage.getClassType())) {
                  for (ElemType target : annotUsage.getTargets()) {
                    if (removedTargets.contains(target)) {
                      return true;
                    }
                  }
                }
              }
            }
            return false;
          });
        }

        for (JvmMethod m : classDiff.methods().added()) {
          if (m.getValue() == null) {
            debug("Added method with no default value: ", m.getName());
            debug("Adding class usage to affected usages");
            context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
            break;
          }
        }
      }
      debug("End of annotation-specific analysis");
    }

    if (changedClass.getFlags().isEnum() && !isEmpty(classDiff.fields().added()))  {
      debug("Constants added to enum, affecting class usages " + changedClass.getName());
      // only mark synthetic classes used to implement switch statements: this will limit the number of recompiled classes to those where switch statements on changed enum are used
      context.affectUsage(new ClassUsage(changedClass.getReferenceID()), n -> n instanceof JVMClassNode<?, ?> && ((JVMClassNode<?, ?>)n).isSynthetic());
    }

    boolean wasLambdaTarget = present.isLambdaTarget(change.getPast());
    boolean isLambdaTarget = future.isLambdaTarget(change.getNow());
    if (wasLambdaTarget && !isLambdaTarget) {
      // affectLambdaInstantiations
      for (ReferenceID id : present.withAllSubclasses(changedClass.getReferenceID())) {
        if (id.equals(changedClass.getReferenceID()) || present.isLambdaTarget(id)) {
          String clsName = present.getNodeName(id);
          if (clsName != null) {
            debug("The interface could be not a SAM interface anymore => affecting lambda instantiations for ", clsName);
            context.affectUsage(new ClassNewUsage(clsName));
          }
        }
      }
    }
    else if (!wasLambdaTarget && isLambdaTarget) {
      // should affect lambda instantiations on overloads, because some calls may have become ambiguous
      TypeRepr.ClassType samType = new TypeRepr.ClassType(changedClass.getName());
      for (JvmClass depClass : flat(map(context.getGraph().getDependingNodes(changedClass.getReferenceID()), dep -> present.getNodes(dep, JvmClass.class)))) {
        JvmMethod methodWithSAMType = find(depClass.getMethods(), m -> contains(m.getArgTypes(), samType));
        if (methodWithSAMType == null) {
          continue;
        }
        Iterable<Utils.OverloadDescriptor> overloaded = future.findAllOverloads(depClass, m -> {
          if (!Objects.equals(methodWithSAMType.getName(), m.getName()) || m.isSame(methodWithSAMType)) {
            return null;
          }
          // find methods with the same name and arg count as a pattern method, but different signature.
          // calls to found method should look like a call to a pattern method
          Iterator<TypeRepr> patternSignatureTypes = methodWithSAMType.getArgTypes().iterator();
          for (TypeRepr arg : m.getArgTypes()) {
            if (!patternSignatureTypes.hasNext()) {
              return null;
            }
            TypeRepr patternArg = patternSignatureTypes.next();
            if (patternArg.equals(samType)) {
              if (arg.equals(samType) || !(arg instanceof TypeRepr.ClassType)) {
                return null;
              }
            }
            else {
              if (Boolean.FALSE.equals(future.isSubtypeOf(arg, patternArg)) && Boolean.FALSE.equals(future.isSubtypeOf(patternArg, arg))) {
                return null;
              }
            }
          }
          return patternSignatureTypes.hasNext()? null : m.getFlags();
        });
        for (Utils.OverloadDescriptor descr : overloaded) {
          debug("Found method ", methodWithSAMType, " that uses SAM interface ", samType.getJvmName(), " in its signature --- affect potential lambda-target usages of overloaded method: ", descr.overloadMethod);
          affectMemberUsages(
            context,
            descr.owner.getReferenceID(),
            descr.overloadMethod,
            future.collectSubclassesWithoutMethod(descr.owner.getReferenceID(), descr.overloadMethod),
            n -> n instanceof JvmClass && future.isVisibleIn(depClass, methodWithSAMType, (JvmClass)n)
          );
        }
      }
    }

    Difference.Specifier<ElementAnnotation, ElementAnnotation.Diff> annotationsDiff = classDiff.annotations();
    if (!annotationsDiff.unchanged()) {
      EnumSet<AnnotationChangesTracker.Recompile> toRecompile = EnumSet.noneOf(AnnotationChangesTracker.Recompile.class);
      for (AnnotationChangesTracker tracker : ourAnnotationChangeTrackers) {
        if (toRecompile.containsAll(AnnotationChangesTracker.RECOMPILE_ALL)) {
          break;
        }
        Set<AnnotationChangesTracker.Recompile> result = tracker.classAnnotationsChanged(changedClass, annotationsDiff);
        if (result.contains(AnnotationChangesTracker.Recompile.USAGES)) {
          debug("Extension ", tracker.getClass().getName(), " requested class usages recompilation because of changes in annotations list --- adding class usage to affected usages");
        }
        if (result.contains(AnnotationChangesTracker.Recompile.SUBCLASSES)) {
          debug("Extension ", tracker.getClass().getName(), " requested subclasses recompilation because of changes in annotations list --- adding subclasses to affected usages");
        }
        toRecompile.addAll(result);
      }
      boolean affectUsages = toRecompile.contains(AnnotationChangesTracker.Recompile.USAGES);
      if (affectUsages) {
        context.affectUsage(new ClassUsage(changedClass.getReferenceID()));
      }
      if (toRecompile.contains(AnnotationChangesTracker.Recompile.SUBCLASSES)) {
        affectSubclasses(context, future, changedClass.getReferenceID(), affectUsages);
      }
    }

    return true;
  }
@Override
  public boolean processChangedClass(DifferentiateContext context, Difference.Change<JvmClass, JvmClass.Diff> change, Utils future, Utils present)