/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.tool.checkstyle; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.apache.commons.lang3.StringUtils; import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.CheckstyleException; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.FileContents; import com.puppycrawl.tools.checkstyle.api.FullIdent; import com.puppycrawl.tools.checkstyle.api.TextBlock; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.AnnotationUtility; /** * Verify if Unstable annotations should be removed. * * @version $Id: a7b36f974c2cc32b696b3f955573bbecffd6a45a $ * @since 7.0M1 */ public class UnstableAnnotationCheck extends AbstractCheck { private String packageName; private String classOrInterfaceName; private String currentVersion; private int currentVersionMajor; @Override public int[] getDefaultTokens() { return new int[]{ TokenTypes.PACKAGE_DEF, TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF, TokenTypes.METHOD_DEF, }; } public void setCurrentVersion(String currentVersion) throws CheckstyleException { if (currentVersion != null && !currentVersion.isEmpty()) { this.currentVersion = currentVersion; this.currentVersionMajor = extractMajor(currentVersion); if (this.currentVersionMajor == -1) { throw new CheckstyleException("The passed version [" + this.currentVersionMajor + "] must be of the type Major.* (e.g. 7.0-SNAPSHOT)"); } } } @Override public void visitToken(DetailAST ast) { if (this.currentVersion == null) { // If not current version is set, just ignore this check return ; } switch (ast.getType()) { case TokenTypes.PACKAGE_DEF: // Save the package FullIdent ident = FullIdent.createFullIdent(ast.getLastChild().getPreviousSibling()); this.packageName = ident.getText(); return; case TokenTypes.CLASS_DEF: case TokenTypes.INTERFACE_DEF: this.classOrInterfaceName = ast.findFirstToken(TokenTypes.IDENT).getText(); break; } if (AnnotationUtility.containsAnnotation(ast)) { DetailAST holder = AnnotationUtility.getAnnotationHolder(ast); for (DetailAST annotation : findAllTokens(holder, TokenTypes.ANNOTATION)) { String annotationName = annotation.findFirstToken(TokenTypes.IDENT).getText(); if (annotationName.equals("Unstable")) { FileContents contents = getFileContents(); String annotatedElementName = ast.findFirstToken(TokenTypes.IDENT).getText(); // Get the Javadoc before the annotation in order to locate a @Since annotation and to extract // the XWiki version mentioned there. List<String> sinceVersions = Collections.emptyList(); TextBlock cmt = contents.getJavadocBefore(ast.getLineNo()); if (cmt != null) { sinceVersions = extractSinceVersionsFromJavadoc(cmt.getText(), annotation, annotatedElementName); } if (sinceVersions.isEmpty()) { log(annotation.getLineNo(), annotation.getColumnNo(), String.format("There is an @Unstable " + "annotation for [%s] but the @since javadoc tag is missing, you must add it!", computeElementName(annotatedElementName))); return; } checkSinceVersions(sinceVersions, annotation, annotatedElementName); } } } } private void checkSinceVersions(List<String> sinceVersions, DetailAST annotation, String annotatedElementName) { List<String> versions = new ArrayList<>(); boolean failing = false; for (String sinceVersion : sinceVersions) { int sinceMajor = extractMajor(sinceVersion); if (sinceMajor == -1) { log(annotation.getLineNo(), annotation.getColumnNo(), String.format("The @since version [%s] " + "must be of the type Major.* (e.g. 7.0-SNAPSHOT)", sinceVersion)); return; } else { versions.add(sinceVersion); // We fail only if all since are failing since when we introduce a new API and backport it in an // older version, we don't want to start the grace period to be that of the backport version. if (this.currentVersionMajor - 2 >= sinceMajor) { failing = true; } else { failing = false; break; } } } if (failing) { log(annotation.getLineNo(), annotation.getColumnNo(), String.format("The @Unstable annotation " + "for [%s] must be removed since it''s been there for more than a full " + "development cycle (was introduced in %s and current version is [%s])", computeElementName(annotatedElementName), StringUtils.join(versions), this.currentVersion)); } } private String computeElementName(String annotatedElementName) { return String.format("%s.%s%s", this.packageName, this.classOrInterfaceName, annotatedElementName.equals(this.classOrInterfaceName) ? "" : "." + annotatedElementName + "()"); } /** * @return null if the since format is wrong */ private List<String> extractSinceVersionsFromJavadoc(String[] javadocLines, DetailAST annotation, String annotatedElementName) { List<String> sinceVersions = new ArrayList<>(); for (String javadocLine : javadocLines) { int pos = javadocLine.indexOf("@since"); if (pos > -1) { String sinceVersion = javadocLine.substring(pos + "@since".length() + 1); sinceVersions.add(sinceVersion); } } return sinceVersions; } private int extractMajor(String version) { if (version == null) { return -1; } int major; int pos = version.indexOf("."); if (pos > -1) { try { major = Integer.parseInt(version.substring(0, pos)); } catch (NumberFormatException e) { major = -1; } } else { major = -1; } return major; } public List<DetailAST> findAllTokens(DetailAST ast, int aType) { List<DetailAST> results = new ArrayList<>(); DetailAST firstToken = ast.findFirstToken(aType); DetailAST token = firstToken; while (token != null) { results.add(token); token = token.getNextSibling(); if (token == null || token.getType() != aType) { break; } } return results; } }