HaxeCheckstyle/haxe-checkstyle

View on GitHub
src/checkstyle/checks/block/NeedBracesCheck.hx

Summary

Maintainability
Test Coverage
package checkstyle.checks.block;

/**
    Checks for braces on function, if, for and while statements. It has an option to allow single line statements without
    braces using property "allowSingleLineStatement" like "if (b) return 10;".
**/
@name("NeedBraces")
@desc("Checks for braces on function, if, for and while statements. It has an option to allow single line statements without braces using property `allowSingleLineStatement` like `if (b) return 10;`.")
class NeedBracesCheck extends Check {
    /**
        matches only statements specified in tokens list:

        - FUNCTION = function body "funnction test () {}"
        - FOR = for body "for (i in 0..10) {}"
        - IF = if body "if (test) {} else {}"
        - ELSE_IF = if body "if (test) {} else if {}"
        - WHILE = while body "while (test) {}"
        - DO_WHILE = do…while body "do {} while (test)"
        - CATCH = catch body "catch (e:Dynamic) {}"
    **/
    public var tokens:Array<NeedBracesCheckToken>;

    /**
        allow / disallow use of single line statements without braces
    **/
    public var allowSingleLineStatement:Bool;

    public function new() {
        super(TOKEN);
        tokens = [FOR, IF, ELSE_IF, WHILE, DO_WHILE];
        allowSingleLineStatement = true;
    }

    function hasToken(token:NeedBracesCheckToken):Bool {
        return (tokens.length == 0 || tokens.contains(token));
    }

    override function actualRun() {
        var wantFunction = hasToken(FUNCTION);
        var wantFor = hasToken(FOR);
        var wantIf = hasToken(IF);
        var wantWhile = hasToken(WHILE);
        var wantDoWhile = hasToken(DO_WHILE);
        var wantCatch = hasToken(CATCH);

        if (!(wantFunction || wantFor || wantIf || wantWhile || wantDoWhile || wantCatch)) return;

        var root:TokenTree = checker.getTokenTree();
        var allTokens:Array<TokenTree> = root.filterCallback(function(token:TokenTree, depth:Int):FilterResult {
            return switch (token.tok) {
                case Kwd(KwdFunction) if (wantFunction):
                    FoundGoDeeper;
                case Kwd(KwdFor) if (wantFor):
                    FoundGoDeeper;
                case Kwd(KwdIf) | Kwd(KwdElse) if (wantIf):
                    FoundGoDeeper;
                case Kwd(KwdWhile) if (wantWhile):
                    FoundGoDeeper;
                case Kwd(KwdDo) if (wantDoWhile):
                    FoundGoDeeper;
                case Kwd(KwdCatch) if (wantCatch):
                    FoundGoDeeper;
                default:
                    GoDeeper;
            }
        });

        for (tok in allTokens) {
            if (isPosSuppressed(tok.pos)) continue;
            switch (tok.tok) {
                case Kwd(KwdIf):
                    checkIfChild(tok);
                case Kwd(KwdElse):
                    var firstChild = tok.getFirstChild();
                    if (firstChild == null) continue;
                    if (firstChild.matches(Kwd(KwdIf))) checkIfChild(firstChild);
                    else checkLastChild(tok);
                case Kwd(KwdFunction):
                    checkFunctionChild(tok);
                case Kwd(KwdDo):
                    checkLastChild(tok);
                case Kwd(KwdWhile):
                    checkWhileChild(tok);
                default:
                    checkLastChild(tok);
            }
        }
    }

    function checkIfChild(token:TokenTree) {
        if (token == null || !token.hasChildren()) return;

        var lastChild:TokenTree = token.getLastChild();
        if (Type.enumEq(lastChild.tok, Kwd(KwdElse))) {
            lastChild = lastChild.previousSibling;
        }
        switch (lastChild.tok) {
            case POpen, BrOpen:
                return;
            default:
                checkNoBraces(token, lastChild);
        }
    }

    function checkFunctionChild(token:TokenTree) {
        if (token == null || !token.hasChildren()) return;

        var body:TokenTree = token;
        if (token.children.length == 1) {
            body = token.getFirstChild();
        }
        body = TokenTreeAccessHelper.access(body).firstOf(POpen).token;
        if ((body == null) || (body.nextSibling == null)) {
            return;
        }
        body = body.nextSibling;
        if (body.matches(DblDot)) {
            var lastChild:TokenTree = TokenTreeCheckUtils.getLastToken(token);
            if (lastChild.matches(Semicolon)) {
                return;
            }
            body = body.nextSibling;
        }
        if ((body == null) || (body.matches(BrOpen))) {
            return;
        }
        checkNoBraces(token, body);
    }

    function checkWhileChild(token:TokenTree) {
        if (token == null || !token.hasChildren() || Type.enumEq(token.parent.tok, Kwd(KwdDo))) return;
        checkLastChild(token);
    }

    function checkLastChild(token:TokenTree) {
        if (token == null || !token.hasChildren()) return;
        var lastChild:TokenTree = TokenTreeAccessHelper.access(token).firstOf(BrOpen).token;
        if (lastChild != null) return;
        lastChild = TokenTreeAccessHelper.access(token).lastChild().token;
        checkNoBraces(token, lastChild);
    }

    function checkNoBraces(parent:TokenTree, child:TokenTree) {
        if ((parent == null) || (child == null)) return;
        var minLine:LinePos = checker.getLinePos(parent.pos.min);
        var maxLine:LinePos = checker.getLinePos(child.getPos().max);
        var singleLine:Bool = (minLine.line == maxLine.line);

        if (allowSingleLineStatement) {
            if (singleLine) return;
            if (checkIfElseSingleline(parent, child)) return;
        }
        else {
            if (singleLine) {
                logPos('Body of "$parent" on same line', child.getPos());
                return;
            }
        }
        logPos('No braces used for body of "$parent"', child.getPos());
    }

    function checkIfElseSingleline(parent:TokenTree, child:TokenTree):Bool {
        if (!hasToken(ELSE_IF)) return false;
        switch (parent.tok) {
            case Kwd(KwdElse):
            default:
                return false;
        }
        switch (child.tok) {
            case Kwd(KwdIf):
            default:
                return false;
        }
        var minLine:LinePos = checker.getLinePos(parent.pos.min);
        var maxLine:LinePos = checker.getLinePos(child.getFirstChild().getPos().max);
        return (minLine.line == maxLine.line);
    }
}

enum abstract NeedBracesCheckToken(String) {
    var FUNCTION = "FUNCTION";
    var FOR = "FOR";
    var IF = "IF";
    var ELSE_IF = "ELSE_IF";
    var WHILE = "WHILE";
    var DO_WHILE = "DO_WHILE";
    var CATCH = "CATCH";
}