Package org.geomajas.checkstyle

Source Code of org.geomajas.checkstyle.ApiCompatibilityCheck$VersionAndCheck

/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/

package org.geomajas.checkstyle;

import com.puppycrawl.tools.checkstyle.api.Check;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FileContents;
import com.puppycrawl.tools.checkstyle.api.TextBlock;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Checkstyle check which verifies Geomajas' API compatibility rules.
*
* @author Joachim Van der Auwera
*/
public class ApiCompatibilityCheck extends Check {

  private String packageName;
  private String fullyQualifiedClassName;
  private Map<String, VersionAndCheck> checkApi = new LinkedHashMap<String, VersionAndCheck>();
  private List<String> api = new ArrayList<String>();
  private boolean isAnnotated;
  private boolean isAllMethods;
  private boolean isInterface;
  private String classSince;

  private String basedir;
  private String checkInputFile = "src/main/resources/api.txt";
  private String checkOutputFile = "target/api.txt";

  public void setBasedir(String basedir) {
    this.basedir = basedir;
  }

  public void setCheckInputFile(String checkInputFile) {
    this.checkInputFile = checkInputFile;
  }

  public void setCheckOutputFile(String checkOutputFile) {
    this.checkOutputFile = checkOutputFile;
  }

  @Override
  public int[] getDefaultTokens() {
    return getAcceptableTokens();
  }

  @Override
  public int[] getAcceptableTokens() {
    return new int[]{
        TokenTypes.PACKAGE_DEF,
        TokenTypes.CLASS_DEF,
        TokenTypes.INTERFACE_DEF,
        TokenTypes.METHOD_DEF,
        TokenTypes.CTOR_DEF,
        TokenTypes.VARIABLE_DEF,
    };
  }

  @Override
  public int[] getRequiredTokens() {
    return getAcceptableTokens();
  }

  @Override
  public void beginTree(DetailAST rootAst) {
    super.beginTree(rootAst);
    packageName = "";
    fullyQualifiedClassName = "";
    isAnnotated = false;
    isAllMethods = false;
    classSince = "?";
    isInterface = false;
  }

  @Override
  public void finishTree(DetailAST rootAst) {
    super.finishTree(rootAst);
  }

  @Override
  public void visitToken(DetailAST ast) {
    switch (ast.getType()) {
      case TokenTypes.PACKAGE_DEF:
        packageName = getPackage(ast);
        break;
      case TokenTypes.CLASS_DEF:
      case TokenTypes.INTERFACE_DEF:
        fullyQualifiedClassName = packageName + "." + getName(ast);
        checkClassAnnotation(ast);
        if (TokenTypes.INTERFACE_DEF == ast.getType()) {
          isInterface = true;
        }
        if (isAnnotated) {
          String since = getSince(ast);
          api.add(fullyQualifiedClassName + "::" + since);

          // @since needs to be specified
          if ("?".equals(since)) {
            log(ast, "classMissingSince", fullyQualifiedClassName);
          }
          // check that class/interface @since has not changed and mark as encountered
          VersionAndCheck vac = checkApi.get(fullyQualifiedClassName + ":");
          if (null != vac) {
            if (!"?".equals(since) && !since.equals(vac.getVersion())) {
              log(ast, "wrongClassSince", vac.getVersion(), since, fullyQualifiedClassName);
            }
            vac.setEncountered(true);
          }
        }
        break;
      case TokenTypes.METHOD_DEF:
      case TokenTypes.CTOR_DEF:
      case TokenTypes.VARIABLE_DEF:
        String signature = getSignature(ast);
        if (isApi(ast)) {
          String since = getSince(ast);
          api.add(fullyQualifiedClassName + ":" + signature + ":" + since);

          // check that class/interface @since has not changed and mark as encountered
          VersionAndCheck vac = checkApi.get(fullyQualifiedClassName + ":" + signature);
          if (null != vac) {
            if (!since.equals(vac.getVersion())) {
              log(ast, "wrongMethodSince", vac.getVersion(), since, fullyQualifiedClassName,
                  signature);
            }
            vac.setEncountered(true);
          } else {
            // check that version is different from class version (indicates added without @since)
            vac = checkApi.get(fullyQualifiedClassName + ":");
            if (null != vac && since.equals(vac.getVersion())) {
              log(ast, "missingMethodSince", fullyQualifiedClassName, signature);
            }
          }
        }
        break;
      default:
        log(ast, "oops, unexpected node");
        break;
    }
  }

  private boolean isApi(DetailAST ast) {
    if ("serialVersionUID".equals(getName(ast))) {
      // this should not be considered API
      return false;
    }
    DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
    if (null != modifiers) {
      if (isAllMethods) {
        // if public then it is API
        if (isInterface || null != modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC)) {
          return true;
        }
      }
      DetailAST check = modifiers.getFirstChild();
      while (null != check) {
        if (TokenTypes.ANNOTATION == check.getType() && "Api".equals(getName(check))) {
          return true;
        }
        check = check.getNextSibling();
      }
    }
    return false;
  }

  private String getSignature(DetailAST ast) {
    String returnType = "";
    String name = getName(ast);
    String parameters = "";
    if (TokenTypes.METHOD_DEF == ast.getType() || TokenTypes.VARIABLE_DEF == ast.getType()) {
      DetailAST modifiersAst = ast.findFirstToken(TokenTypes.MODIFIERS);
      if (null != modifiersAst) {
        if (null != modifiersAst.findFirstToken(TokenTypes.LITERAL_STATIC)) {
          returnType += "static ";
        }
        if (null != modifiersAst.findFirstToken(TokenTypes.FINAL)) {
          returnType += "final ";
        }
      }
      returnType += getTypeAsString(ast.findFirstToken(TokenTypes.TYPE)) + " ";
    }
    if (TokenTypes.METHOD_DEF == ast.getType() || TokenTypes.CTOR_DEF == ast.getType()) {
      DetailAST parametersAst = ast.findFirstToken(TokenTypes.PARAMETERS);
      if (null != parametersAst) {
        DetailAST check = parametersAst.getFirstChild();
        while (null != check) {
          if (TokenTypes.PARAMETER_DEF == check.getType()) {
            parameters += getTypeAsString(check.findFirstToken(TokenTypes.TYPE)) + ", ";
          }
          check = check.getNextSibling();
        }
        parameters = "(" + parameters + ")";
      }
    }
    return returnType + name + parameters;
  }

  private String getTypeAsString(DetailAST typeAst) {
    String type = "";
    if (null != typeAst) {
      DetailAST ast = typeAst.getFirstChild();
      if (TokenTypes.ARRAY_DECLARATOR == ast.getType()) {
        type += getTypeAsString(ast);
        type += "[]";
      } else {
        type += ast.getText();
        if (TokenTypes.IDENT == ast.getType()) {
          ast = ast.getNextSibling();
          if (null != ast && TokenTypes.TYPE_ARGUMENTS == ast.getType()) {
            DetailAST genAst = ast.getFirstChild();
            while (null != genAst) {
              if (TokenTypes.TYPE_ARGUMENT == genAst.getType()) {
                type += getTypeAsString(genAst);
              } else {
                type += genAst.getText();
              }
              genAst = genAst.getNextSibling();
            }
          }
        }
      }
    }
    return type;
  }

  private void checkClassAnnotation(DetailAST ast) {
    DetailAST check = ast.getFirstChild();
    if (TokenTypes.MODIFIERS == check.getType()) {
      check = check.getFirstChild();
      while (null != check) {
        if (TokenTypes.ANNOTATION == check.getType() && "Api".equals(getName(check))) {
          isAnnotated = true;
          classSince = getSince(ast);
          DetailAST param = getToken(TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR, check);
          if (null != param) {
            DetailAST expr = param.getLastChild();
            isAllMethods = "true".equals(expr.getFirstChild().getText());
          }
        }
        check = check.getNextSibling();
      }
    }
  }

  private String getName(DetailAST ast) {
    DetailAST check = ast.getFirstChild();
    String name = null;
    while (null == name && null != check) {
      if (TokenTypes.IDENT == check.getType()) {
        name = check.getText();
      }
      check = check.getNextSibling();
    }
    return name;
  }

  private DetailAST getToken(int type, DetailAST ast) {
    DetailAST check = ast.getFirstChild();
    while (null != check) {
      if (type == check.getType()) {
        return check;
      }
      check = check.getNextSibling();
    }
    return null;
  }

  private String getPackage(DetailAST ast) {
    switch (ast.getType()) {
      case TokenTypes.DOT:
        return getPackage(ast.getFirstChild()) + "." + getPackage(ast.getLastChild());
      case TokenTypes.IDENT:
        return ast.getText();
      case TokenTypes.PACKAGE_DEF:
        DetailAST check = ast.getFirstChild();
        String name = null;
        while (null == name && null != check) {
          name = getPackage(check);
          check = check.getNextSibling();
        }
        return name;
      default:
        return null;
    }
  }

  private String getSince(DetailAST ast) {
    String since = classSince;
    final FileContents contents = getFileContents();
    final TextBlock javadoc = contents.getJavadocBefore(ast.getLineNo());
    if (null != javadoc) {
      for (String line : javadoc.getText()) {
        int index = line.indexOf("@since");
        if (index >= 0) {
          since = line.substring(index + 6).trim();
        }
      }
    }
    return since;
  }

  @Override
  public void init() {
    try {
      File file = new File(basedir, checkInputFile);
      if (file.exists()) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
        String line;
        while (null != (line = reader.readLine())) {
          if (line.length() > 0 && !line.startsWith("//")) {
            int pos = line.lastIndexOf(':');
            checkApi.put(line.substring(0, pos), new VersionAndCheck(line.substring(pos + 1)));
          }
        }
      }
    } catch (IOException ioe) {
      log(0, "Cannot read src/main/resources/api.txt, " + ioe.getMessage());
    }
  }

  @Override
  public void destroy() {
    // output api.txt for comparisons
    Collections.sort(api);
    try {
      File file = new File(basedir, checkOutputFile);
      Writer writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
      for (String line : api) {
        writer.write(line);
        writer.write('\n');
      }
      writer.close();
    } catch (IOException ioe) {
      log(0, "Cannot write target/api.txt, " + ioe.getMessage());
    }

    String problems = "";
    // check that all previous API parts have been encountered
    for (Map.Entry<String, VersionAndCheck> entry : checkApi.entrySet()) {
      if (!entry.getValue().isEncountered()) {
        problems += entry.getKey() + '\n';
      }
    }
    if (problems.length() > 0) {
      throw new RuntimeException("Missing in the API:\n" + problems);
    }
  }

  private class VersionAndCheck {

    private String version;
    private boolean encountered;

    public VersionAndCheck(String version) {
      this.version = version;
    }

    public String getVersion() {
      return version;
    }

    public boolean isEncountered() {
      return encountered;
    }

    public void setEncountered(boolean encountered) {
      this.encountered = encountered;
    }
  }
}
TOP

Related Classes of org.geomajas.checkstyle.ApiCompatibilityCheck$VersionAndCheck

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.