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

com.igormaznitsa.meta.checker.CheckerMojo Maven / Gradle / Ivy

/*
 * Copyright 2016 Igor Maznitsa.
 *
 * 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.igormaznitsa.meta.checker;

import com.igormaznitsa.meta.checker.jversion.LongComparator;
import com.igormaznitsa.meta.checker.jversion.JavaVersion;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.ParameterAnnotationEntry;

import com.igormaznitsa.meta.Complexity;
import com.igormaznitsa.meta.annotation.Weight;
import com.igormaznitsa.meta.checker.extracheck.MethodParameterChecker;
import com.igormaznitsa.meta.common.utils.Assertions;
import com.igormaznitsa.meta.common.utils.GetUtils;
import com.igormaznitsa.meta.common.utils.StrUtils;

@Mojo(name = "check", defaultPhase = LifecyclePhase.PACKAGE, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE)
public class CheckerMojo extends AbstractMojo {

  private static class AbortException extends RuntimeException {

    private static final long serialVersionUID = -1153122159632822978L;

    public AbortException(String message) {
      super(message);
    }
  }

  private static final String DELIMITER = "................................";
  private static final String FAILURE_STRING = "Detected annotation '%s' defined to be recognized as error";
  private static final String[] BANNER = new String[]{
    "  __  __  ____  ____   __   ",
    " (  \\/  )( ___)(_  _) /__\\  ",
    "  )    (  )__)   )(  /(__)\\ ",
    " (_/\\/\\_)(____) (__)(__)(__)",
    "https://github.com/raydac/meta",
    ""};

//  @Parameter (defaultValue = "${project}", readonly = true, required = true)
//  private MavenProject project;
  /**
   * Folder which will be recursively used as the source of class files.
   */
  @Parameter(defaultValue = "${project.build.outputDirectory}", name = "targetDirectory")
  private String targetDirectory;

  /**
   * Restrict compiled class format version. Also '=','<=','>=','<','>' can be used. Java version can be
   * '1.1','1.2','1.3','1.4','5','6','7','8','5.0','6.0','7.0','8.0'.
   *
   * 
   * <restrictClassFormat><![CDATA[<8]]></restrictClassFormat>
   * 
   *
   * @since 1.0.2
   */
  @Parameter(name = "restrictClassFormat")
  private String restrictClassFormat;

  /**
   * List of annotations in full canonical or short form. If checker met annotation from the list then it will be recognized as error. NB! Annotation names are case insensitive.
   */
  @Parameter(name = "failForAnnotations")
  private String[] failForAnnotations;

  /**
   * List of classes to be ignored by checker. Class name must be defined in canonical form and wildcat chars '*' and '?' are allowed.
   *
   * @since 1.1.0
   */
  @Parameter(name = "ignoreClasses")
  private String[] ignoreClasses;

  /**
   * Check that method object arguments are marked by @Nullable or @Nonnull (also allowed Intellij IDEA annotations).
   *
   * @since 1.1.0
   */
  @Parameter(name = "checkNullable", defaultValue = "false")
  private boolean checkNullable;

  /**
   * Check that list and object array types in methods are marked by @MayContainNull and @MustNotContainNull annotations.
   *
   * @since 1.1.0
   */
  @Parameter(name = "checkMayContainNull", defaultValue = "false")
  private boolean checkMayContainNull;

  /**
   * Define max allowed value for detected weight annotation, if detected annotation has bigger weight then it will be recognized as error.
   *
   * @since 1.1.2
   */
  @Parameter(name = "maxAllowedWeight")
  private String maxAllowedWeight;

  /**
   * Define max allowed value for detected time complexity annotation, if detected annotation has bigger complexity then it will be recognized as error.
   *
   * @since 1.1.2
   */
  @Parameter(name = "maxAllowedTimeComplexity")
  private String maxAllowedTimeComplexity;

  /**
   * Define max allowed value for detected memory complexity annotation, if detected annotation has bigger complexity then it will be recognized as error.
   *
   * @since 1.1.2
   */
  @Parameter(name = "maxAllowedMemoryComplexity")
  private String maxAllowedMemoryComplexity;

  /**
   * Hide pseudo-graphic banner.
   *
   * @since 1.1.0
   */
  @Parameter(name = "hideBanner", defaultValue = "false")
  private boolean hideBanner;

  private LongComparator comparatorForJavaVersion;
  private JavaVersion decodedJavaVersion;
  private Pattern[] ignoreClassesAsPatterns;

  public String getMaxAllowedWeight() {
    return this.maxAllowedWeight;
  }
  
  public String getMaxAllowedTimeComplexity() {
    return this.maxAllowedTimeComplexity;
  }
  
  public String getMaxAllowedMemoryComplexity() {
    return this.maxAllowedMemoryComplexity;
  }
  
  private static Weight.Unit decodeWeight(final String value) {
    final String normalized = StrUtils.pressing(GetUtils.ensureNonNullStr(value)).replace("_", "");
    if (normalized.isEmpty()) {
      return null;
    }

    for (final Weight.Unit u : Weight.Unit.values()) {
      if (normalized.equalsIgnoreCase(StrUtils.pressing(u.name()).replace("_", ""))) {
        return u;
      }
    }
    throw new NoSuchElementException("Can't recognize weight unit for its name : " + value);
  }

  private static Complexity decodeComplexity(final String value) {
    final String normalized = StrUtils.pressing(GetUtils.ensureNonNullStr(value)).replace("_", "");
    if (normalized.isEmpty()) {
      return null;
    }

    Complexity detected = null;

    for (final Complexity c : Complexity.values()) {
      final String name = StrUtils.pressing(c.name()).replace("_", "");
      final String formula = StrUtils.pressing(c.getFormula()).replace("_", "");
      if (normalized.equalsIgnoreCase(name) || normalized.equalsIgnoreCase(formula)) {
        detected = c;
        break;
      }
    }

    if (detected == null) {
      throw new NoSuchElementException("Can't recognize complexity level from string value : " + value);
    }

    return detected;
  }

  @Override
  public void execute() throws MojoExecutionException, MojoFailureException {
    prepareIgnoreClassPatterns();

    final File targetDirectoryFile = new File(this.targetDirectory);
    if (!targetDirectoryFile.isDirectory()) {
      getLog().warn("Can't find directory for investigation, may be there are not classes for compilation : " + this.targetDirectory);
      return;
    } else {
      if (!this.hideBanner) {
        for (final String s : BANNER) {
          getLog().info(s);
        }
        getLog().info(DELIMITER);
      }
      getLog().info("Folder to look for classes : " + targetDirectoryFile.getAbsolutePath());
      getLog().info(DELIMITER);
    }

    if (this.restrictClassFormat != null) {
      String javaClassVersion = this.restrictClassFormat.trim();
      if (javaClassVersion.isEmpty()) {
        throw new IllegalArgumentException("Detected empty value for 'restrictClassFormat'");
      }

      this.comparatorForJavaVersion = LongComparator.find(javaClassVersion);

      final int versionOffset;
      if (this.comparatorForJavaVersion == null) {
        if (Character.isDigit(javaClassVersion.charAt(0))) {
          this.comparatorForJavaVersion = LongComparator.EQU;
        }
        versionOffset = 0;
      } else {
        versionOffset = this.comparatorForJavaVersion.getText().length();
      }
      javaClassVersion = javaClassVersion.substring(versionOffset).trim();

      this.decodedJavaVersion = JavaVersion.decode(javaClassVersion);
      if (this.decodedJavaVersion == null) {
        throw new IllegalArgumentException("Illegal java version in 'restrictClassFormat': " + javaClassVersion);
      }
    }

    final Map counters = new HashMap();

    final AtomicInteger counterWarings = new AtomicInteger();
    final AtomicInteger counterErrors = new AtomicInteger();
    final AtomicInteger counterInfo = new AtomicInteger();

    final Complexity theMaxAllowedTimeComplexity;
    final Complexity theMaxAllowedMemoryComplexity;
    final Weight.Unit theMaxAllowedWeight;

    try{
      theMaxAllowedWeight = decodeWeight(getMaxAllowedWeight());
    }catch(NoSuchElementException ex){
      getLog().error("Can't recognize weight value : "+getMaxAllowedWeight());
      getLog().error("Allowed values : "+Arrays.toString(Weight.Unit.values()));
      throw new MojoExecutionException(ex.getMessage(), ex);
    }
    
    try {
      theMaxAllowedTimeComplexity = decodeComplexity(getMaxAllowedTimeComplexity());
      theMaxAllowedMemoryComplexity = decodeComplexity(getMaxAllowedMemoryComplexity());
    } catch (NoSuchElementException ex) {
      getLog().error("Can't recognize a complexity constant");
      getLog().error("Allowed values : " + Arrays.toString(Complexity.values()));
      throw new MojoExecutionException(ex.getMessage(), ex);
    }

    final Context context = new Context() {
      FieldOrMethod node;
      JavaClass klazz;
      int itemIndex;

      @Override
      public Weight.Unit getMaxAllowedWeightLevel() {
        return theMaxAllowedWeight;
      }

      @Override
      public Complexity getMaxAllowedTimeComplexity() {
        return theMaxAllowedTimeComplexity;
      }

      @Override
      public Complexity getMaxAllowedMemoryComplexity() {
        return theMaxAllowedMemoryComplexity;
      }

      @Override
      public JavaClass getProcessingClass() {
        return this.klazz;
      }

      @Override
      public int getItemIndex() {
        return this.itemIndex;
      }

      @Override
      public boolean isCheckNullableArgs() {
        return checkNullable;
      }

      @Override
      public boolean isCheckMayContainNullArgs() {
        return checkMayContainNull;
      }

      @Override
      public File getTargetDirectoryFolder() {
        return targetDirectoryFile;
      }

      public String currentProcessingItemAsString() {
        final StringBuilder builder = new StringBuilder();

        final int line = Utils.findLineNumber(this.node);
        String klazzName = Utils.normalizeClassNameAndRemoveSubclassName(Assertions.assertNotNull(this.klazz).getClassName());
        final String nodeName = Utils.asString(this.klazz, this.node);

        builder.append(klazzName).append(".java").append(":[");
        if (line < 0) {
          builder.append("-,-]");
        } else {
          builder.append(line).append(',').append(1).append(']');
        }
        builder.append(' ');
        if (this.node != null) {
          if (this.node instanceof Field) {
            builder.append("field ").append(nodeName);
          } else {
            final Method method = (Method) node;
            if (method.getName().equals("")) {
              builder.append("constructor ");
            } else {
              builder.append("method ");
            }
            builder.append(nodeName).append(" (flags: #").append(Integer.toHexString(method.getAccessFlags()).toUpperCase(Locale.ENGLISH)).append(") ");
          }
        } else {
          builder.append("whole class");
        }

        return builder.toString();
      }

      @Override
      public void setProcessingItem(final JavaClass klazz, final FieldOrMethod node, final int itemIndex) {
        this.node = node;
        this.klazz = klazz;
        this.itemIndex = itemIndex;
      }

      @Override
      public FieldOrMethod getNode() {
        return this.node;
      }

      @Override
      public void info(final String info, final boolean showProcessingItem) {
        counterInfo.incrementAndGet();
        getLog().info((showProcessingItem ? currentProcessingItemAsString() + ' ' : "") + info);
      }

      @Override
      public void warning(final String warning, final boolean showProcessingItem) {
        counterWarings.incrementAndGet();
        getLog().warn((showProcessingItem ? currentProcessingItemAsString() + ' ' : "") + warning);
      }

      @Override
      public void error(final String error, final boolean showProcessingItem) {
        counterErrors.incrementAndGet();
        getLog().error((showProcessingItem ? currentProcessingItemAsString() + ' ' : "") + error);
      }

      @Override
      public void abort(final String error, final boolean showProcessingItem) {
        throw new AbortException((showProcessingItem ? currentProcessingItemAsString() + ' ' : "") + error);
      }

      @Override
      public void countDetectedAnnotation(final String annotationClassName) {
        AtomicInteger counter = counters.get(annotationClassName);
        if (counter == null) {
          counter = new AtomicInteger();
          counters.put(annotationClassName, counter);
        }
        counter.incrementAndGet();
      }
    };

    final long startTime = System.currentTimeMillis();
    int processedClasses = 0;
    try {
      final Iterator iterator = FileUtils.iterateFiles(targetDirectoryFile, new String[]{"class", "CLASS"}, true);
      int classIndex = 0;
      while (iterator.hasNext()) {
        final File file = iterator.next();
        getLog().debug(String.format("Processing class file : %s", file.getAbsolutePath()));
        try {
          final JavaClass parsed = new ClassParser(file.getAbsolutePath()).parse();

          if (isClassIgnored(parsed)) {
            context.info(String.format("Ignored class file : %s", file.getAbsolutePath()), false);
            continue;
          }

          processedClasses++;

          if (!isClassVersionAllowed(parsed)) {
            context.error(String.format("Detected class version violator, version %s at %s", JavaVersion.decode(parsed.getMajor()), file.getAbsolutePath()), false);
            counterErrors.incrementAndGet();
            break;
          }
          countAllDetectedAnnotations(context, parsed);
          classIndex++;
          for (final MetaAnnotations p : MetaAnnotations.values()) {
            p.getInstance().processClass(context, parsed, classIndex);
          }
          checkMethodsForMarkedObjectTypes(context, parsed);
        } catch (AbortException ex) {
          throw new MojoFailureException(ex.getMessage());
        } catch (IOException ex) {
          context.error(String.format("Can't read class file : %s", file.getAbsolutePath()), false);
        } catch (ClassFormatException ex) {
          context.error(String.format("Can't parse class file : %s", file.getAbsolutePath()), false);
        }
      }

      if (this.failForAnnotations != null && this.failForAnnotations.length > 0) {
        getLog().debug("Defined annotations to be interpreted as error : " + Arrays.toString(this.failForAnnotations));
        for (final Map.Entry detected : counters.entrySet()) {
          if (detected.getValue().get() > 0) {
            final String name = detected.getKey().toLowerCase(Locale.ENGLISH);
            final String shortName = Utils.extractShortNameOfClass(name);
            for (final String s : this.failForAnnotations) {
              if (s.indexOf('.') < 0) {
                if (shortName.equalsIgnoreCase(s)) {
                  final String text = String.format(FAILURE_STRING, s);
                  context.error(text, false);
                }
              } else if (name.equalsIgnoreCase(s)) {
                final String text = String.format(FAILURE_STRING, s);
                context.error(text, false);
              }
            }
          }
        }

        if (counterErrors.get() > 0) {
          throw new MojoFailureException(String.format("Detected %d error(s)", counterErrors.get()));
        }
      } else {
        getLog().debug("There are not defined annotations to be interpreted as error");
      }
    } finally {
      if ((counterErrors.get() | counterInfo.get() | counterWarings.get()) != 0) {
        getLog().info(DELIMITER);
      }

      int totalAnnotations = 0;
      for (final Map.Entry e : counters.entrySet()) {
        totalAnnotations += e.getValue().get();
      }

      getLog().info(String.format("Processed classes : %d", processedClasses));
      getLog().info(String.format("Detected annotations : %d", totalAnnotations));
      getLog().info(String.format("Detected To-Do : %d", extractCounter(counters, MetaAnnotations.TODO)));
      getLog().info(String.format("Detected risks : %d", extractCounter(counters, MetaAnnotations.RISKY)));
      getLog().info(String.format("Detected experimental : %d", extractCounter(counters, MetaAnnotations.EXPERIMENTAL)));

      if (counterWarings.get() > 0) {
        getLog().warn(String.format("Total warnings : %d", counterWarings.get()));
      } else {
        getLog().info(String.format("Total warnings : %d", counterWarings.get()));
      }

      if (counterErrors.get() > 0) {
        getLog().error(String.format("Total errors : %d", counterErrors.get()));
      } else {
        getLog().info(String.format("Total errors : %d", counterErrors.get()));
      }

      getLog().info(DELIMITER);
      getLog().info(String.format("Total spent time : %s", Utils.printTimeDelay(System.currentTimeMillis() - startTime)));
    }

    if (counterErrors.get() > 0) {
      throw new MojoFailureException(String.format("Detected %d error(s), see the log", counterErrors.get()));
    }
  }

  public boolean isHideBanner() {
    return this.hideBanner;
  }

  public String getRestrictClassFormat() {
    return this.restrictClassFormat;
  }

  public boolean isCheckMayContainNullArgs() {
    return this.checkMayContainNull;
  }

  public boolean isCheckNullableArgs() {
    return this.checkNullable;
  }

  public String[] getIgnoreClasses() {
    return this.ignoreClasses;
  }

  public String getTargetDirectory() {
    return this.targetDirectory;
  }

  public String[] getFailForAnnotations() {
    return this.failForAnnotations;
  }

  private void prepareIgnoreClassPatterns() {
    this.ignoreClassesAsPatterns = null;
    if (this.ignoreClasses != null) {
      this.ignoreClassesAsPatterns = new Pattern[this.ignoreClasses.length];
      int index = 0;
      for (final String str : this.ignoreClasses) {
        this.ignoreClassesAsPatterns[index++] = Pattern.compile(Utils.escapeRegexToWildCat(str));
      }
    }
  }

  private void checkMethodsForMarkedObjectTypes(final Context context, final JavaClass clazz) {
    if ((context.isCheckMayContainNullArgs() || context.isCheckNullableArgs()) && !(clazz.isAnnotation() || clazz.isSynthetic())) {
      int index = 0;
      for (final Method m : clazz.getMethods()) {
        context.setProcessingItem(clazz, m, index++);
        final String name = m.getName();
        if ("".equals(name)) {
          continue;
        }
        if ((m.getModifiers() & (0x40 | 0x1000)) == 0) {

          final boolean skipCheckParameters = clazz.isNested() && m.getAccessFlags() == 0 && "".equals(m.getName());

          if (clazz.isEnum() && ("values".equals(name) || "valueOf".equals(name) || "".equals(name))) {
            continue;
          }
          if (context.isCheckNullableArgs()) {
            MethodParameterChecker.checkReturnTypeForNullable(context, m);
            if (!skipCheckParameters) {
              MethodParameterChecker.checkParamsTypeForNullable(context, m);
            }
          }
          if (context.isCheckMayContainNullArgs()) {
            MethodParameterChecker.checkReturnTypeForMayContainNull(context, m);
            if (!skipCheckParameters) {
              MethodParameterChecker.checkParamsTypeForMayContainNull(context, m);
            }
          }
        }
      }
    }
  }

  private boolean isClassIgnored(final JavaClass clazz) {
    if (this.ignoreClassesAsPatterns == null || this.ignoreClassesAsPatterns.length == 0) {
      return false;
    }

    final String klazzName = clazz.getClassName();

    for (final Pattern pattern : this.ignoreClassesAsPatterns) {
      if (pattern.matcher(klazzName).matches()) {
        return true;
      }
    }

    return false;
  }

  private boolean isClassVersionAllowed(final JavaClass klazz) {
    if (this.comparatorForJavaVersion == null) {
      return true;
    }
    return this.comparatorForJavaVersion.compare(klazz.getMajor(), Assertions.assertNotNull(this.decodedJavaVersion).getValue());
  }

  private static void countAllDetectedAnnotations(final Context context, final JavaClass clazz) {
    for (final AnnotationEntry ae : clazz.getAnnotationEntries()) {
      context.countDetectedAnnotation(Utils.classNameToNormalView(ae.getAnnotationType()));
    }
    for (final Field field : clazz.getFields()) {
      for (final AnnotationEntry ae : field.getAnnotationEntries()) {
        context.countDetectedAnnotation(Utils.classNameToNormalView(ae.getAnnotationType()));
      }
    }
    for (final Method method : clazz.getMethods()) {
      for (final AnnotationEntry ae : method.getAnnotationEntries()) {
        context.countDetectedAnnotation(Utils.classNameToNormalView(ae.getAnnotationType()));
      }
      for (final ParameterAnnotationEntry pae : method.getParameterAnnotationEntries()) {
        for (final AnnotationEntry ae : pae.getAnnotationEntries()) {
          context.countDetectedAnnotation(Utils.classNameToNormalView(ae.getAnnotationType()));
        }
      }
    }
  }

  private static int extractCounter(final Map counters, final MetaAnnotations annotation) {
    final AtomicInteger result = counters.get(annotation.getAnnotationClassName());
    return result == null ? 0 : result.get();
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy