diff --git a/checkstyle.json b/checkstyle.json index 05a13609..684a0ccf 100644 --- a/checkstyle.json +++ b/checkstyle.json @@ -538,6 +538,71 @@ }, "type": "VariableInitialisation" }, + { + "props": { + "mode": "Around", + "tokens": [ + "=>", + "Assign", + "Unary", + "Compare", + "Bitwise", + "Bool", + "{", + "}" + ], + "contexts": [ + "Object", + "Function", + "Field", + "Switch", + "Switch", + "Array" + ], + "severity": "WARNING" + }, + "type": "Whitespace" + }, + { + "props": { + "mode": "None", + "tokens": [ + ".", + ":", + "(", + ")" + ], + "contexts": [ + "Object", + "Function", + "Field", + "Switch", + "Switch", + "Array" + ], + "severity": "WARNING" + }, + "type": "Whitespace" + }, + { + "props": { + "mode": "After", + "tokens": [ + ",", + ";" + ], + "contexts": [ + "Object", + "Function", + "Field", + "Switch", + "Switch", + "Array" + ], + "severity": "WARNING" + }, + "type": "Whitespace" + }, { "props": { "tokens": [ diff --git a/resources/default-config.json b/resources/default-config.json index d1d8cd9f..833c3511 100644 --- a/resources/default-config.json +++ b/resources/default-config.json @@ -532,6 +532,29 @@ }, "type": "VariableInitialisation" }, + { + "props": { + "mode": "around", + "tokens": [ + "=>", + "Assign", + "Unary", + "Compare", + "Bitwise", + "Bool" + ], + "contexts": [ + "Object", + "Function", + "Field", + "Switch", + "Switch", + "Array" + ], + "severity": "IGNORE" + }, + "type": "Whitespace" + }, { "props": { "tokens": [ diff --git a/src/checkstyle/checks/whitespace/WhitespaceCheck.hx b/src/checkstyle/checks/whitespace/WhitespaceCheck.hx new file mode 100644 index 00000000..761f018d --- /dev/null +++ b/src/checkstyle/checks/whitespace/WhitespaceCheck.hx @@ -0,0 +1,205 @@ +package checkstyle.checks.whitespace; + +import checkstyle.Checker.LinePos; +import checkstyle.token.TokenTree; +import checkstyle.utils.TokenTreeCheckUtils; +import haxeparser.Data; +import haxe.macro.Expr; + +using checkstyle.utils.ArrayUtils; + +@name("Whitespace") +@desc("Checks that is present or absent around a token.") +class WhitespaceCheck extends Check { + + public var mode:WhitespaceMode; + public var tokens:Array; + public var contexts:Array; + + public function new() { + super(TOKEN); + tokens = [ + ARROW, ASSIGN, UNARY, COMPARE, BITWISE, BOOL + ]; + mode = AROUND; + contexts = [OBJECT_DECL, FUNCTION, FIELD, SWITCH, TRY_CATCH, ARRAY_ACCESS]; + + categories = [Category.STYLE, Category.CLARITY]; + } + + override function actualRun() { + var tokenList:Array = []; + + for (token in tokens) { + switch (token) { + case ASSIGN: + tokenList = tokenList.concat([ + Binop(OpAssign), + Binop(OpAssignOp(OpAdd)), + Binop(OpAssignOp(OpSub)), + Binop(OpAssignOp(OpMult)), + Binop(OpAssignOp(OpDiv)), + Binop(OpAssignOp(OpMod)), + Binop(OpAssignOp(OpShl)), + Binop(OpAssignOp(OpShr)), + Binop(OpAssignOp(OpUShr)), + Binop(OpAssignOp(OpOr)), + Binop(OpAssignOp(OpAnd)), + Binop(OpAssignOp(OpXor)) + ]); + case UNARY: + tokenList = tokenList.concat([ + Unop(OpNot), + Unop(OpIncrement), + Unop(OpDecrement) + ]); + case COMPARE: + tokenList = tokenList.concat([ + Binop(OpGt), + Binop(OpLt), + Binop(OpGte), + Binop(OpLte), + Binop(OpEq), + Binop(OpNotEq) + ]); + case ARITHMETIC: + tokenList = tokenList.concat([ + Binop(OpAdd), + Binop(OpSub), + Binop(OpMult), + Binop(OpDiv), + Binop(OpMod) + ]); + case BITWISE: + tokenList = tokenList.concat([ + Binop(OpAnd), + Binop(OpOr), + Binop(OpXor), + Binop(OpShl), + Binop(OpShr), + Binop(OpUShr) + ]); + case BOOL: + tokenList.push(Binop(OpBoolAnd)); + tokenList.push(Binop(OpBoolOr)); + case ARROW: + tokenList.push(Arrow); + case COMMA: + tokenList.push(Comma); + case SEMICOLON: + tokenList.push(Semicolon); + case POPEN: + tokenList.push(POpen); + case PCLOSE: + tokenList.push(PClose); + case BROPEN: + tokenList.push(BrOpen); + case BRCLOSE: + tokenList.push(BrClose); + case BKOPEN: + tokenList.push(BkOpen); + case BKCLOSE: + tokenList.push(BkClose); + case DBLDOT: + tokenList.push(DblDot); + case DOT: + tokenList.push(Dot); + case INTERVAL: + tokenList.push(Binop(OpInterval)); + } + } + + if (tokenList.length <= 0) return; + checkTokens(tokenList); + } + + function checkTokens(tokenList:Array) { + var root:TokenTree = checker.getTokenTree(); + var allTokens:Array = root.filter(tokenList, ALL); + + for (tok in allTokens) { + if (isPosSuppressed(tok.pos)) continue; + if (!checkContext(tok)) continue; + + checkWhitespace(tok); + } + } + + function checkWhitespace(tok:TokenTree) { + var linePos:LinePos = checker.getLinePos(tok.pos.min); + var line:String = checker.lines[linePos.line]; + var before:String = line.substr(0, linePos.ofs); + var tokLen:Int = TokenDefPrinter.print(tok.tok).length; + var after:String = line.substr(linePos.ofs + tokLen); + + var whitespaceBefore:Bool = ~/^(.*\s|)$/.match(before); + var whitespaceAfter:Bool = ~/^(\s.*|)$/.match(after); + + switch (mode) { + case BEFORE: + if (whitespaceBefore && !whitespaceAfter) return; + case AFTER: + if (!whitespaceBefore && whitespaceAfter) return; + case NONE: + if (!whitespaceBefore && !whitespaceAfter) return; + case AROUND: + if (whitespaceBefore && whitespaceAfter) return; + } + + logPos('Whitespace mode "$mode" violated by "${TokenDefPrinter.print(tok.tok)}"', tok.pos); + } + + function checkContext(token:TokenTree):Bool { + if (TokenTreeCheckUtils.isTypeParameter(token)) return false; + if (TokenTreeCheckUtils.isImportMult(token)) return false; + if (TokenTreeCheckUtils.filterOpSub(token)) return false; + + // TODO check contexts + + return true; + } + + function hasContext(context:WhitespaceContext):Bool { + return contexts.contains(context); + } +} + +@:enum +abstract WhitespaceMode(String) { + var BEFORE = "before"; + var AFTER = "after"; + var AROUND = "around"; + var NONE = "none"; +} + +@:enum +abstract WhitespaceContext(String) { + var OBJECT_DECL = "Object"; + var FUNCTION = "Function"; + var FIELD = "Field"; + var SWITCH = "Switch"; + var TRY_CATCH = "Switch"; + var ARRAY_ACCESS = "Array"; +} + +@:enum +abstract WhitespaceToken(String) { + var ASSIGN = "Assign"; + var UNARY = "Unary"; + var COMPARE = "Compare"; + var ARITHMETIC = "Arithmetic"; + var BITWISE = "Bitwise"; + var BOOL = "Bool"; + var ARROW = "=>"; + var COMMA = ","; + var SEMICOLON = ";"; + var POPEN = "("; + var PCLOSE = ")"; + var BROPEN = "{"; + var BRCLOSE = "}"; + var BKOPEN = "["; + var BKCLOSE = "]"; + var DBLDOT = ":"; + var DOT = "."; + var INTERVAL = "..."; +} \ No newline at end of file diff --git a/test/checks/whitespace/WhitespaceCheckTest.hx b/test/checks/whitespace/WhitespaceCheckTest.hx new file mode 100644 index 00000000..c3a039d8 --- /dev/null +++ b/test/checks/whitespace/WhitespaceCheckTest.hx @@ -0,0 +1,259 @@ +package checks.whitespace; + +import checkstyle.checks.whitespace.WhitespaceCheck; + +class WhitespaceCheckTest extends CheckTestCase { + + static inline var MSG_EQUALS:String = 'Whitespace mode "around" violated by "="'; + + public function testCorrectWhitespace() { + var check = new WhitespaceCheck(); + assertNoMsg(check, CORRECT_WHITESPACE_AROUND); + assertNoMsg(check, ISSUE_70); + assertNoMsg(check, ISSUE_71); + assertNoMsg(check, ISSUE_72); + assertNoMsg(check, ISSUE_77); + assertNoMsg(check, ISSUE_80); + assertNoMsg(check, ISSUE_81); + assertNoMsg(check, ISSUE_98); + assertNoMsg(check, MINUS_CONSTANT); + assertNoMsg(check, CONDITIONAL_STAR_IMPORT_ISSUE_160); + assertNoMsg(check, CONDITIONAL_ELSE_STAR_IMPORT); + assertNoMsg(check, CONDITIONAL_ELSEIF_STAR_IMPORT); + assertNoMsg(check, NEGATIVE_VARS); + assertNoMsg(check, NEGATIVE_NUMS); + assertNoMsg(check, OPGT); + } + + public function testIncorrectWhitespace() { + var check = new WhitespaceCheck(); + assertMsg(check, NO_WHITESPACE_OBJECT_DECL, MSG_EQUALS); + assertMsg(check, NO_WHITESPACE_TYPEDEF, MSG_EQUALS); + assertMsg(check, ISSUE_59, MSG_EQUALS); + assertMsg(check, ISSUE_63, MSG_EQUALS); + } + + public function testIncorrectWhitespaceToken() { + var check = new WhitespaceCheck(); + check.tokens = [ASSIGN]; + assertNoMsg(check, CORRECT_WHITESPACE_AROUND); + assertMsg(check, NO_WHITESPACE_GT, MSG_EQUALS); + assertMsg(check, NO_WHITESPACE_OBJECT_DECL, MSG_EQUALS); + assertMsg(check, NO_WHITESPACE_TYPEDEF, MSG_EQUALS); + assertMsg(check, NO_WHITESPACE_VAR_INIT, MSG_EQUALS); + + check.tokens = [COMPARE]; + assertNoMsg(check, CORRECT_WHITESPACE_AROUND); + assertNoMsg(check, NO_WHITESPACE_VAR_INIT); + assertNoMsg(check, NO_WHITESPACE_GT); + } + + public function testStarImport() { + var check = new WhitespaceCheck(); + check.tokens = [ARITHMETIC]; + assertNoMsg(check, ISSUE_70); + assertNoMsg(check, CONDITIONAL_STAR_IMPORT_ISSUE_160); + assertNoMsg(check, CONDITIONAL_ELSE_STAR_IMPORT); + assertNoMsg(check, CONDITIONAL_ELSEIF_STAR_IMPORT); + } +} + +@:enum +abstract WhitespaceCheckTests(String) to String { + var CORRECT_WHITESPACE_AROUND = " + import haxe.macro.*; + + class Test { + function test(param1:String, param2:String) { + var x = { x: 100, y: 100, + z: 20 * 10 + }; + var y:Array = []; + } + } + + typedef Test = { + x:Int, + y:Int, z:Int + } + + enum Test { + Monday; + Tuesday; + Wednesday; + Thursday; + Friday; Weekend(day:String); + }"; + + var NO_WHITESPACE_OBJECT_DECL = " + class Test { + function test(param1:String, param2:String) { + var x={ x: 100, y: 100,z: 20 }; + } + }"; + + var NO_WHITESPACE_TYPEDEF = " + typedef Test ={ + x:Int, + y:Int,z:Int + }"; + + var NO_WHITESPACE_VAR_INIT = " + class Test { + function test(param1:String, param2:String) { + var test:Array=[]; + } + }"; + + var NO_WHITESPACE_GT = " + class Test { + function test(param1:String, param2:String) { + var test:Array= []; + } + }"; + + var ISSUE_58 = " + class Test { + public function new() { + var x:Int, y:Int; + } + }"; + + var ISSUE_59 = " + typedef Test=Int + "; + + var ISSUE_63 = " + typedef Test =#if true Int #else String #end + "; + + var ISSUE_70 = " + import haxe.macro.*; + "; + + var ISSUE_71 = " + class Test { + function foo() { + trace((null : Array)); + } + }"; + + var ISSUE_72 = " + abstract Test(Array) {} + "; + + var ISSUE_77 = " + // comment + class Test // comment + { // comment + function foo() // comment + { // comment + switch ('Test') // comment + { // comment + } // comment + } // comment + } // comment + "; + + var ISSUE_80 = " + interface Test implements Dynamic {} + "; + + var ISSUE_81 = " + class Test { + function foo() { + do a++ while (true); + do ++a while (true); + } + }"; + + var ISSUE_98 = " + class Test { + // °öäüßÖÄÜ@łĸŋđđðſðæµ”“„¢«»Ø→↓←Ŧ¶€Ł}][{¬½¼³² + var test:Int = 0; + }"; + + var MINUS_CONSTANT = " + class Test { + function test() { + if (re.match(line) && line.indexOf('//') == -1) { + log('Tab after non-space character, Use space for aligning', i + 1, line.length, null, Reflect.field(SeverityLevel, severity)); + return -1; + } + a = 1 - -2; + b = 1.2 - -2.1; + return -1; + } + }"; + + var CONDITIONAL_STAR_IMPORT_ISSUE_160 = " + #if macro + import haxe.macro.*; + #end"; + + var CONDITIONAL_ELSEIF_STAR_IMPORT = " + #if macro + import haxe.macro.Type; + #elseif neko + import haxe.macro.*; + #elseif neko + import haxe.macro.*; + #else + #if linux + import haxe.macro.Type; + #else + import haxe.macro.*; + #end + #end + import haxe.macro.Type;"; + + var CONDITIONAL_ELSE_STAR_IMPORT = " + #if macro + import haxe.macro.Type; + #else + import haxe.macro.*; + #end + import haxe.macro.Type;"; + + var NEGATIVE_VARS = " + class Test { + function test() { + var rest = if (neg) { -noFractions; } + else { -noFractions; } + var rest = if (neg) -noFractions; + else -noFractions; + var x = neg ? -frag : -frag; + calc ([-width, -node.right, root], -node.left, {x : -x, y: -y}); + (-a); + (1 * -a); + do -a * 2 while(true); + for (a in [-1, -2]) -a + 2; + return -a; + } + }"; + + var NEGATIVE_NUMS = " + class Test { + function test() { + var rest = if (neg) { -8; } + else { -9; } + var rest = if (neg) -10; + else -11; + var x = neg ? -12 : -13; + calc ([-14, -node.right, root], -node.left, {x : -xi15x, y: -16}); + (-16); + (1 * -17); + do -18 * 2 while(true); + for (a in [-1, -2]) -18 + 2; + } + }"; + + var OPGT = " + class Test { + function test() { + if (a > b) return a >= b; + if (a >> b > c) return a >>= b; + if (a >>> b > c) return a >>>= b; + } + }"; +} \ No newline at end of file