HaxeCheckstyle/haxe-checkstyle

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

Summary

Maintainability
Test Coverage
package checkstyle.checks.block;

/**
    Checks for the placement of left curly braces ("{") for code blocks. The policy to verify is specified using the property "option".
**/
@name("LeftCurly")
@desc("Checks for the placement of left curly braces (`{`) for code blocks. The policy to verify is specified using the property `option`.")
class LeftCurlyCheck extends Check {
    /**
        matches only left curlys specified in tokens list:
        - CLASS_DEF = class definition "class Test {}"
        - ENUM_DEF = enum definition "enum Test {}"
        - ABSTRACT_DEF = abstract definition "abstract Test {}"
        - TYPEDEF_DEF = typdef definition "typedef Test = {}"
        - INTERFACE_DEF = interface definition "interface Test {}"
        - OBJECT_DECL = object declaration "{ x: 0, y: 0, z: 0}"
        - FUNCTION = function body "funnction test () {}"
        - FOR = for body "for (i in 0..10) {}"
        - IF = if / else body "if (test) {} else {}"
        - WHILE = while body "while (test) {}"
        - SWITCH = switch / case body "switch (test) { case: {} default: {} }"
        - TRY = try body "try {}"
        - CATCH = catch body "catch (e:Dynamic) {}"
        - REIFICATION = macro reification "$i{}"
        - ARRAY_COMPREHENSION = array comprehension "[for (i in 0...10) {i * 2}]"
    **/
    public var tokens:Array<LeftCurlyCheckToken>;

    /**
        placement of left curly
        - eol = should occur at end of line
        - nl = should occur on a new line
        - nlow = should occur at end of line unless in wrapped code, then it should occur on a new line
    **/
    public var option:LeftCurlyCheckOption;

    /**
        allow empty single line blocks
    **/
    public var ignoreEmptySingleline:Bool;

    /**
        allow single line blocks
    **/
    public var ignoreSingleline:Bool;

    public function new() {
        super(TOKEN);
        tokens = [
            CLASS_DEF,
            ENUM_DEF,
            ABSTRACT_DEF,
            TYPEDEF_DEF,
            INTERFACE_DEF,
            // OBJECT_DECL, // => allow inline object declarations
            FUNCTION,
            FOR,
            IF,
            WHILE,
            SWITCH,
            TRY,
            CATCH
        ];
        option = EOL;
        ignoreEmptySingleline = true;
        ignoreSingleline = false;
    }

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

    override function actualRun() {
        var root:TokenTree = checker.getTokenTree();
        var allBrOpen:Array<TokenTree> = root.filterCallback(function(token:TokenTree, index:Int):FilterResult {
            return switch (token.tok) {
                case BrOpen:
                    FoundGoDeeper;
                default:
                    GoDeeper;
            }
        });

        for (brOpen in allBrOpen) {
            if (isPosSuppressed(brOpen.pos)) continue;
            if (skipSingleLine(brOpen)) continue;
            var type:BrOpenType = TokenTreeCheckUtils.getBrOpenType(brOpen);
            switch (type) {
                case Block:
                case TypedefDecl:
                    if (!hasToken(TYPEDEF_DEF)) continue;
                case ObjectDecl:
                    if (!hasToken(OBJECT_DECL)) continue;
                case AnonType:
                    if (!hasToken(ANON_TYPE)) continue;
                case Unknown:
            }
            var parent:ParentToken = findParentToken(brOpen.parent);
            if (!parent.hasToken) continue;
            check(brOpen, isParentWrapped(parent.token, brOpen));
        }
    }

    function skipSingleLine(brOpen:TokenTree):Bool {
        var brClose:TokenTree = brOpen.access().firstOf(BrClose).token;
        if (brClose == null) return false;
        if (ignoreEmptySingleline && (brOpen.pos.max == brClose.pos.min)) return true;
        var lStart:Int = checker.getLinePos(brOpen.pos.min).line;
        var lEnd:Int = checker.getLinePos(brClose.pos.min).line;
        if (ignoreSingleline && lStart == lEnd) return true;
        return false;
    }

    /**
        find effective parent token and check against configured tokens
    **/
    function findParentToken(token:TokenTree):ParentToken {
        if ((token == null) || (token.tok == Root)) return {token: token, hasToken: false};
        switch (token.tok) {
            case Kwd(KwdClass):
                return {token: token, hasToken: hasToken(CLASS_DEF)};
            case Kwd(KwdInterface):
                return {token: token, hasToken: hasToken(INTERFACE_DEF)};
            case Kwd(KwdAbstract):
                return {token: token, hasToken: hasToken(ABSTRACT_DEF)};
            case Kwd(KwdTypedef):
                return {token: token, hasToken: hasToken(TYPEDEF_DEF)};
            case Kwd(KwdEnum):
                return {token: token, hasToken: hasToken(ENUM_DEF)};
            case Kwd(KwdFunction):
                return {token: token, hasToken: hasToken(FUNCTION)};
            case Kwd(KwdIf), Kwd(KwdElse):
                return {token: token, hasToken: hasToken(IF)};
            case Kwd(KwdFor):
                if (isArrayComprehension(token.parent)) {
                    return {token: token, hasToken: hasToken(ARRAY_COMPREHENSION)};
                }
                return {token: token, hasToken: hasToken(FOR)};
            case Kwd(KwdWhile):
                return {token: token, hasToken: hasToken(WHILE)};
            case Kwd(KwdTry):
                return {token: token, hasToken: hasToken(TRY)};
            case Kwd(KwdCatch):
                return {token: token, hasToken: hasToken(CATCH)};
            case Kwd(KwdSwitch), Kwd(KwdDefault):
                return {token: token, hasToken: hasToken(SWITCH)};
            case Kwd(KwdCase):
                return {token: token, hasToken: hasToken(OBJECT_DECL)};
            case DblDot:
                return findParentTokenDblDot(token);
            case POpen, BkOpen, BrOpen, Kwd(KwdReturn):
                return {token: token, hasToken: hasToken(OBJECT_DECL)};
            case Dollar(_):
                return {token: token, hasToken: hasToken(REIFICATION)};
            case Binop(OpAssign):
                // could be OBJECT_DECL or TYPEDEF_DEF
                if ((token.parent != null) && (token.parent.parent != null)) {
                    if (token.parent.parent.tok.match(Kwd(KwdTypedef))) {
                        return {token: token, hasToken: hasToken(TYPEDEF_DEF)};
                    }
                }
                return {token: token, hasToken: hasToken(OBJECT_DECL)};
            default:
                return findParentToken(token.parent);
        }
    }

    function findParentTokenDblDot(token:TokenTree):ParentToken {
        if ((token == null) || (token.tok == Root)) return {token: token, hasToken: false};
        var type:ColonType = TokenTreeCheckUtils.getColonType(token);
        switch (type) {
            case SwitchCase:
                return {token: token, hasToken: hasToken(SWITCH)};
            case TypeHint:
                return {token: token, hasToken: hasToken(TYPEDEF_DEF)};
            case TypeCheck:
                return {token: token, hasToken: hasToken(TYPEDEF_DEF)};
            case Ternary:
                return {token: token, hasToken: false};
            case ObjectLiteral:
                return {token: token, hasToken: hasToken(OBJECT_DECL)};
            case At:
                return {token: token, hasToken: false};
            case Unknown:
                return {token: token, hasToken: false};
        }
    }

    function isParentWrapped(parent:TokenTree, brOpen:TokenTree):Bool {
        var lineNumStart:Int = checker.getLinePos(parent.pos.min).line;
        var previous:TokenTree = brOpen.previousSibling;
        while (previous != null) {
            switch (previous.tok) {
                case Comment(_), CommentLine(_), At:
                    previous = previous.previousSibling;
                default:
                    break;
            }
        }
        var lineNumEnd:Int;
        if (previous == null) {
            lineNumEnd = checker.getLinePos(brOpen.parent.pos.max).line;
        }
        else {
            lineNumEnd = checker.getLinePos(previous.getPos().max).line;
        }
        return (lineNumStart != lineNumEnd);
    }

    function isArrayComprehension(token:TokenTree):Bool {
        return switch (token.tok) {
            case BkOpen: true;
            case Kwd(KwdFunction): false;
            case Kwd(KwdVar): false;
            default: isArrayComprehension(token.parent);
        }
    }

    function check(token:TokenTree, wrapped:Bool) {
        var lineNum:Int = checker.getLinePos(token.pos.min).line;
        var line:String = checker.lines[lineNum];
        checkLeftCurly(line, wrapped, token.pos);
    }

    function checkLeftCurly(line:String, wrapped:Bool = false, pos:Position) {
        // must have at least one non whitespace character before curly
        // and only whitespace, /* + comment or // + comment after curly
        var curlyAtEOL:Bool = ~/^\s*\S.*\{\s*(|\/\*.*|\/\/.*)$/.match(line);
        // must have only whitespace before curly
        var curlyOnNL:Bool = ~/^\s*\{/.match(line);

        try {
            if (curlyAtEOL) {
                logErrorIf((option == NL), "Left curly should be on new line (only whitespace before curly)", pos);
                logErrorIf((option == NLOW) && wrapped, "Left curly should be on new line (previous expression is split over multiple lines)", pos);
                logErrorIf((option != EOL) && (option != NLOW), "Left curly unknown option ${option}", pos);
                return;
            }
            logErrorIf((option == EOL), "Left curly should be at EOL (only line break or comment after curly)", pos);
            logErrorIf((!curlyOnNL), "Left curly should be on new line (only whitespace before curly)", pos);
            logErrorIf((option == NLOW) && !wrapped, "Left curly should be at EOL (previous expression is not split over multiple lines)", pos);
            logErrorIf((option != NL) && (option != NLOW), "Left curly unknown option ${option}", pos);
        }
        catch (e:Exception) {
            // one of the error messages fired -> do nothing
        }
    }

    function logErrorIf(condition:Bool, msg:String, pos:Position) {
        if (condition) {
            logPos(msg, pos);
            throw new Exception("exit");
        }
    }

    override public function detectableInstances():DetectableInstances {
        return [{
            fixed: [{
                propertyName: "tokens",
                value: [
                    CLASS_DEF,
                    ENUM_DEF,
                    ABSTRACT_DEF,
                    INTERFACE_DEF,
                    FUNCTION,
                    FOR,
                    IF,
                    WHILE,
                    SWITCH,
                    TRY,
                    CATCH
                ]
            }],
            properties: [{
                propertyName: "option",
                values: [EOL, NLOW, NL]
            }, {
                propertyName: "ignoreEmptySingleline",
                values: [true, false]
            }, {
                propertyName: "ignoreSingleline",
                values: [true, false]
            }]
        }, {
            fixed: [{
                propertyName: "tokens",
                value: [TYPEDEF_DEF]
            }],
            properties: [{
                propertyName: "option",
                values: [EOL, NLOW, NL]
            }, {
                propertyName: "ignoreEmptySingleline",
                values: [true, false]
            }, {
                propertyName: "ignoreSingleline",
                values: [true, false]
            }]
        }];
    }
}

typedef ParentToken = {
    var token:TokenTree;
    var hasToken:Bool;
}

enum abstract LeftCurlyCheckToken(String) {
    var CLASS_DEF = "CLASS_DEF";
    var ENUM_DEF = "ENUM_DEF";
    var ABSTRACT_DEF = "ABSTRACT_DEF";
    var TYPEDEF_DEF = "TYPEDEF_DEF";
    var INTERFACE_DEF = "INTERFACE_DEF";
    var ANON_TYPE = "ANON_TYPE";
    var OBJECT_DECL = "OBJECT_DECL";
    var FUNCTION = "FUNCTION";
    var FOR = "FOR";
    var IF = "IF";
    var WHILE = "WHILE";
    var SWITCH = "SWITCH";
    var TRY = "TRY";
    var CATCH = "CATCH";
    var REIFICATION = "REIFICATION";
    var ARRAY_COMPREHENSION = "ARRAY_COMPREHENSION";
}

enum abstract LeftCurlyCheckOption(String) {
    var EOL = "eol";
    var NL = "nl";
    var NLOW = "nlow";
}