com.puppycrawl.tools.checkstyle.checks.coding.CustomDeclarationOrderCheck.java Source code

Java tutorial

Introduction

Here is the source code for com.puppycrawl.tools.checkstyle.checks.coding.CustomDeclarationOrderCheck.java

Source

////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2010  Oliver Burn
//
// This library 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 library 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 library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.coding;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.commons.beanutils.ConversionException;

import com.puppycrawl.tools.checkstyle.api.Check;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FastStack;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.api.Utils;

/**
 * <p>
 * Checks that the parts of a class(main, nested, member inner) declaration
 * appear in the rules order set by user using regular expressions.
 * <p>
 * The check forms line which consists of class member annotations, modifiers,
 * type and name from your code and compares it with your RegExp.
 * </p>
 * The rule consists of:
 *
 * <pre>
 * ClassMember(RegExp)
 * </pre>
 * </p>
 * To set class order use the following notation of the class members (case
 * insensitive):
 * <p>
 * <ol>
 * <li>"Field" to denote the Fields</li>
 * <li>"Ctor" to denote the Constructors</li>
 * <li>"Method" to denote the Methods</li>
 * <li>"InnerClass" to denote the Inner Classes</li>
 * </ol>
 * </p>
 * RegExp can include:
 * <p>
 * <ol>
 * <li>Annotations</li>
 * <li>Modifiers(public, protected, private, abstract, static,
 * final)</li>
 * <li>Type</li>
 * <li>Name</li>
 * </ol>
 * </p>
 * ATTENTION!
 * <p>
 * Use separator <code>' ', '.', '\s'</code> between declaration in the RegExp.
 * </p>
 * <pre>
 * Example:
 *      Field(public.*final.*)
 *      Field(public final.*)
 *      Field(public<code>\s*</code>final.*)
 * </pre>
 * NOTICE!
 * <p>
 * If you set empty RegExp e.g. <code>Field()</code>, it means that class member
 * doesn't have modifiers(default modifier) and checking the type and name of
 * member doesn't occur.
 * </p>
 * <p>
 * Between the declaration of a array and generic can't be whitespaces.
 * E.g.: <code>ArrayList&lt;String[]&gt; someName</code>
 * </p>
 * <p>
 * Use the separator '###' between the class declarations.
 * </p>
 * <p>
 * For Example:
 * </p>
 * <p>
 * <code>Field(private static final long serialVersionUID) ###
 * Field(public static final.*) ### Field(.*private.*) ### Ctor(.*) ###
 * Method(.*public.*final.*|@Ignore.*public.*) ###
 * Method(public static.*(final|(new|edit|create).*).*) ###
 * InnerClass(public abstract.*)</code>
 * </p>
 *
 * @author <a href="mailto:solid.danil@gmail.com">Danil Lopatin</a>
 */
public class CustomDeclarationOrderCheck extends Check {

    /** Default format for custom declaration check */
    private static final String DEFAULT_DECLARATION = "Field(.*public.*)"
            + "### Field(.*protected.*) ### Field(.*private.*) ### CTOR(.*)" + "### Method(.*) ### InnerClass(.*)";

    /** List of order declaration customizing by user */
    private final ArrayList<FormatMatcher> mCustomOrderDeclaration = new ArrayList<FormatMatcher>();

    /**
     * List of Declaration States. This is necessary due to inner classes that
     * have their own state.
     */
    private final FastStack<ClassStates> mClassStates = new FastStack<ClassStates>();

    /** Initialization declaration order from an initial position */
    private static final int INITIAL_STATE = 0;

    /** save compile flags for further usage */
    private int mCompileFlags;

    /** Is current class as root */
    private boolean mClassRoot = true;

    /** allow check inner classes */
    private boolean mInnerClass;

    /** Private class to encapsulate the state. */
    private static class ClassStates {
        /** new state */
        private int mClassStates = INITIAL_STATE;
    }

    /** Constructor to set default format. */
    public CustomDeclarationOrderCheck() {
        setCustomDeclarationOrder(DEFAULT_DECLARATION);
    }

    /**
     * Parsing input line with custom declaration order into massive.
     *
     * @param aInputOrderDeclaration The string line with the user custom
     *            declaration.
     */
    public void setCustomDeclarationOrder(final String aInputOrderDeclaration) {
        if (!mCustomOrderDeclaration.isEmpty()) {
            mCustomOrderDeclaration.clear();
        }
        for (String currentState : aInputOrderDeclaration.split("\\s*###\\s*")) {
            mCustomOrderDeclaration.add(parseInputDeclarationRule(currentState));
        }
    }

    /**
     * Parse input current declaration rule and create new instance of
     * FormatMather with matcher
     *
     * @param aCurrentState input string with MemberDefinition and RegExp.
     * @return new FormatMatcher with parsed and compile rule
     */
    private FormatMatcher parseInputDeclarationRule(final String aCurrentState) {
        String classMember;
        String regExp;
        try {
            // parse mClassMember
            classMember = aCurrentState.substring(0, aCurrentState.indexOf('(')).trim();
            final String classMemberNormalized = normalizeMembersNames(classMember.toLowerCase());
            if (classMember.toLowerCase().equals(classMemberNormalized)) {
                // if Class Member has been specified wrong
                throw new ConversionException("unable to parse " + classMember);
            } else {
                classMember = classMemberNormalized;
            }

            // parse regExp
            regExp = aCurrentState.substring(aCurrentState.indexOf('(') + 1, aCurrentState.lastIndexOf(')'));
            if (regExp.isEmpty()) {
                regExp = "package"; // package level
            }

        } catch (StringIndexOutOfBoundsException exp) {
            //if the structure of the input rule isn't correct
            throw new StringIndexOutOfBoundsException("unable to parse input rule: " + aCurrentState + " " + exp);
        }

        final FormatMatcher matcher = new FormatMatcher(aCurrentState, classMember, mCompileFlags);
        matcher.updateRegexp(regExp, mCompileFlags);
        return matcher;
    }

    /**
     * Finds correspondence between the reduced name of class member of and
     * its complete naming in system.
     *
     * @param aInputMemberName a string name which must be normalize.
     * @return correct name of member or initial string if no matches was
     *         found.
     */
    private static String normalizeMembersNames(String aInputMemberName) {
        String member = aInputMemberName;
        if ("field".equals(aInputMemberName)) {
            member = "VARIABLE_DEF";
        } else {
            if ("method".equals(aInputMemberName)) {
                member = "METHOD_DEF";
            } else {
                if ("ctor".equals(aInputMemberName)) {
                    member = "CTOR_DEF";
                } else {
                    if ("innerclass".equals(aInputMemberName)) {
                        member = "CLASS_DEF";
                    }
                }
            }
        }
        return member;
    }

    /**
     * Set whether or not the match is case sensitive.
     *
     * @param aCaseInsensitive true if the match is case insensitive.
     */
    public void setIgnoreRegExCase(final boolean aCaseInsensitive) {
        if (aCaseInsensitive) {
            if (!mCustomOrderDeclaration.isEmpty()) {
                for (FormatMatcher currentRule : mCustomOrderDeclaration) {
                    currentRule.setCompileFlags(Pattern.CASE_INSENSITIVE);
                }
            } else {
                mCompileFlags = Pattern.CASE_INSENSITIVE;
            }
        }
    }

    @Override
    public int[] getDefaultTokens() {
        //HashSet for unique Tokens
        final HashSet<String> classMembers = new HashSet<String>();

        for (FormatMatcher currentRule : mCustomOrderDeclaration) {
            // check existing of InnerClass in rule
            if ("CLASS_DEF".equals(currentRule.getClassMember())) {
                mInnerClass = true;
            } else {
                classMembers.add(currentRule.mClassMember); //add Tokens
            }
        }

        final int defaultTokens[] = new int[classMembers.size() + 1];
        defaultTokens[0] = TokenTypes.CLASS_DEF;

        int index = 1;
        for (String token : classMembers) {
            if ("VARIABLE_DEF".equals(token)) {
                defaultTokens[index] = TokenTypes.VARIABLE_DEF;
            } else if ("METHOD_DEF".equals(token)) {
                defaultTokens[index] = TokenTypes.METHOD_DEF;
            } else if ("CTOR_DEF".equals(token)) {
                defaultTokens[index] = TokenTypes.CTOR_DEF;
            } else {
                defaultTokens[index] = defaultTokens[0];
            }
            ++index;
        }
        return defaultTokens;
    }

    @Override
    public void visitToken(DetailAST aAST) {

        if (aAST.getType() == TokenTypes.CLASS_DEF) {
            if (mClassRoot) {
                mClassStates.push(new ClassStates());
                mClassRoot = false;
            } else {
                if (mInnerClass) {
                    //if we have condition to check Inner Classes order
                    checkOrderLogic(aAST);
                }
                mClassStates.push(new ClassStates());
            }
        } else {
            final int parentParentType = aAST.getParent().getParent().getType();
            if (parentParentType == TokenTypes.CLASS_DEF) {
                checkOrderLogic(aAST);
            }
        }
    }

    /**
     * Check class declaration order with custom declaration order.
     *
     * @param aAST current DetailAST state.
     */
    private void checkOrderLogic(final DetailAST aAST) {
        final ClassStates previousState = mClassStates.peek();
        final int currentState = getPosition(aAST);
        if (currentState >= 0) {
            if (previousState.mClassStates > currentState) {
                writeLog(aAST, currentState, previousState.mClassStates);
            } else {
                previousState.mClassStates = currentState;
            }
        }
    }

    /**
     * Search in existing custom declaration order current aAST state. It's
     * necessary for getting order of declarations.
     *
     * @param aAST current DetailAST state.
     * @return position in the list of the sequence declaration if
     *         correspondence has been found. Else -1.
     */
    private int getPosition(final DetailAST aAST) {
        int result = -1;
        final String modifiers = getUniteModifiersList(aAST);
        for (int index = 0; index < mCustomOrderDeclaration.size(); index++) {
            final FormatMatcher currentRule = mCustomOrderDeclaration.get(index);
            if (currentRule.getClassMember().equals(aAST.getText())) {
                // find correspondence between list of modifiers and RegExp
                if (currentRule.getRegexp().matcher(modifiers).find()) {
                    result = index;
                    break;
                }
            }
        }
        return result;
    }

    /**
     * Writes log according to met type of token.
     *
     * @param aAST state for log.
     * @param aExpectPosition the expected first position
     * @param aCurrentPosition the current wrong position
     */
    private void writeLog(final DetailAST aAST, final int aExpectPosition, final int aCurrentPosition) {
        String token;
        switch (aAST.getType()) {
        case TokenTypes.VARIABLE_DEF:
            token = "custom.declaration.order.field";
            break;
        case TokenTypes.METHOD_DEF:
            token = "custom.declaration.order.method";
            break;
        case TokenTypes.CTOR_DEF:
            token = "custom.declaration.order.constructor";
            break;
        case TokenTypes.CLASS_DEF:
            token = "custom.declaration.order.class";
            break;
        default:
            token = "Unknown element: " + aAST.getType();
        }
        log(aAST, token, mCustomOrderDeclaration.get(aExpectPosition).getRule(),
                mCustomOrderDeclaration.get(aCurrentPosition).getRule());
    }

    @Override
    public void leaveToken(DetailAST aAST) {
        if (aAST.getType() == TokenTypes.CLASS_DEF) {
            mClassStates.pop();
            if (mClassStates.isEmpty()) {
                mClassRoot = true;
            }
        }
    }

    /**
     * Use for concatenation modifiers, annotations, type and
     * name of member in single line. <br>
     * Contains TokenTypes parameters for entry in child. </br>
     *
     * @param aAST current DetailAST state.
     * @return the unit annotations and modifiers and list.
     */
    private String getUniteModifiersList(final DetailAST aAST) {
        final StringBuffer modifiers = new StringBuffer();
        DetailAST ast = aAST.findFirstToken(TokenTypes.MODIFIERS);
        if (null == ast.getFirstChild()) {
            //if we met package level modifier
            modifiers.append("package ");
        }
        while (ast.getType() != TokenTypes.IDENT) {
            if (ast != null && ast.getFirstChild() != null) {
                modifiers.append(concatLogic(ast.getFirstChild()));
                modifiers.append(" ");
            }
            ast = ast.getNextSibling();
        }
        // add IDENT(name)
        modifiers.append(ast.getText());

        return modifiers.toString();
    }

    /**
     * Use for recursive tree traversal from first child of current tree top.
     *
     * @param aAST current DetailAST state, first child of current tree top.
     * @return the unit modifiers and annotation list.
     */
    private String concatLogic(final DetailAST aAST) {
        DetailAST ast = aAST;
        String separator = "";
        final StringBuffer modifiers = new StringBuffer();

        if (ast.getParent().getType() == TokenTypes.MODIFIERS) {
            // add separator between access modifiers and annotations
            separator = " ";
        }
        while (ast != null) {
            if (ast.getFirstChild() != null) {
                modifiers.append(concatLogic(ast.getFirstChild()));
            } else {
                if (ast.getType() == TokenTypes.RBRACK) {
                    //if array
                    modifiers.append("[");
                }
                modifiers.append(ast.getText());
            }
            modifiers.append(separator);
            ast = ast.getNextSibling();
        }
        return modifiers.toString().trim();
    }

    /**
     * private class for members of class and their patterns.
     */
    private static class FormatMatcher {
        /**
         * Save compile flag. It can be necessary to further change the logic of
         * check.
         */
        private final int mCompileFlags;
        /** The regexp to match against */
        private Pattern mRegExp;
        /** The Member of Class */
        private final String mClassMember;
        /** The input full one rule with original names */
        private final String mRule;
        /** The string format of the RegExp */
        private String mFormat;

        /**
         * Creates a new <code>FormatMatcher</code> instance.
         *
         * @param aInputRule input string with MemberDefinition and RegExp.
         * @param aClassMember the member of class
         * @param aCompileFlags the Pattern flags to compile the regexp with.
         *            See {@link Pattern#compile(java.lang.String, int)}
         */
        public FormatMatcher(final String aInputRule, final String aClassMember, final int aCompileFlags) {
            mClassMember = aClassMember;
            mCompileFlags = aCompileFlags;
            mRule = aInputRule;

        }

        /** @return the RegExp to match against */
        public final Pattern getRegexp() {
            return mRegExp;
        }

        /** @return the original immutable input rule */
        public final String getRule() {
            return mRule;
        }

        /** @return the Class Member */
        public final String getClassMember() {
            return mClassMember;
        }

        /**
         * Set the compile flags for the regular expression.
         *
         * @param aCompileFlags the compile flags to use.
         */
        public final void setCompileFlags(final int aCompileFlags) {
            updateRegexp(mFormat, aCompileFlags);
        }

        /**
         * Updates the regular expression using the supplied format and compiler
         * flags. Will also update the member variables.
         *
         * @param aFormat the format of the regular expression.
         * @param aCompileFlags the compiler flags to use.
         */
        private void updateRegexp(final String aFormat, final int aCompileFlags) {
            try {
                mRegExp = Utils.getPattern(aFormat, aCompileFlags);
                mFormat = aFormat;
            } catch (final PatternSyntaxException e) {
                throw new ConversionException("unable to parse " + aFormat, e);
            }
        }
    }

}