1 // Copyright Brian Schott (Hackerpilot) 2015. 2 // Distributed under the Boost Software License, Version 1.0. 3 // (See accompanying file LICENSE_1_0.txt or copy at 4 // http://www.boost.org/LICENSE_1_0.txt) 5 6 module dscanner.analysis.line_length; 7 8 import dscanner.analysis.base : BaseAnalyzer; 9 10 import dparse.ast; 11 import dparse.lexer; 12 13 import std.typecons : tuple, Tuple; 14 15 /** 16 * Checks for lines longer than 120 characters 17 */ 18 class LineLengthCheck : BaseAnalyzer 19 { 20 /// 21 this(string fileName, const(Token)[] tokens, bool skipTests = false) 22 { 23 super(fileName, null, skipTests); 24 this.tokens = tokens; 25 } 26 27 override void visit(const Module) 28 { 29 size_t endColumn; 30 lastErrorLine = ulong.max; 31 foreach (i, token; tokens) 32 { 33 immutable info = tokenLength(token, i > 0 ? tokens[i - 1].line : 0); 34 if (info.multiLine) 35 endColumn = checkMultiLineToken(token, endColumn); 36 else if (info.newLine) 37 endColumn = info.length + token.column - 1; 38 else 39 { 40 immutable wsChange = i > 0 41 ? token.column - (tokens[i - 1].column + tokenByteLength(tokens[i - 1])) 42 : 0; 43 endColumn += wsChange + info.length; 44 } 45 if (endColumn > MAX_LINE_LENGTH) 46 triggerError(token); 47 } 48 } 49 50 alias visit = BaseAnalyzer.visit; 51 52 private: 53 54 ulong lastErrorLine = ulong.max; 55 56 void triggerError(ref const Token tok) 57 { 58 if (tok.line != lastErrorLine) 59 { 60 addErrorMessage(tok.line, tok.column, KEY, MESSAGE); 61 lastErrorLine = tok.line; 62 } 63 } 64 65 static bool isLineSeparator(dchar c) 66 { 67 import std.uni : lineSep, paraSep; 68 return c == lineSep || c == '\n' || c == '\v' || c == '\r' || c == paraSep; 69 } 70 71 size_t checkMultiLineToken()(auto ref const Token tok, size_t startColumn = 0) 72 { 73 import std.utf : byDchar; 74 75 auto col = startColumn; 76 foreach (c; tok.text.byDchar) 77 { 78 if (isLineSeparator(c)) 79 { 80 if (col > MAX_LINE_LENGTH) 81 triggerError(tok); 82 col = 1; 83 } 84 else 85 col += getEditorLength(c); 86 } 87 return col; 88 } 89 90 unittest 91 { 92 assert(new LineLengthCheck(null, null).checkMultiLineToken(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 8); 93 assert(new LineLengthCheck(null, null).checkMultiLineToken(Token(tok!"stringLiteral", " \na", 0, 0, 0)) == 2); 94 assert(new LineLengthCheck(null, null).checkMultiLineToken(Token(tok!"stringLiteral", " \n ", 0, 0, 0)) == 5); 95 } 96 97 static size_t tokenByteLength()(auto ref const Token tok) 98 { 99 return tok.text is null ? str(tok.type).length : tok.text.length; 100 } 101 102 unittest 103 { 104 assert(tokenByteLength(Token(tok!"stringLiteral", "aaa", 0, 0, 0)) == 3); 105 assert(tokenByteLength(Token(tok!"stringLiteral", "Дистан", 0, 0, 0)) == 12); 106 // tabs and whitespace 107 assert(tokenByteLength(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 1); 108 assert(tokenByteLength(Token(tok!"stringLiteral", " ", 0, 0, 0)) == 4); 109 } 110 111 // D Style defines tabs to have a width of four spaces 112 static size_t getEditorLength(C)(C c) 113 { 114 if (c == '\t') 115 return 4; 116 else 117 return 1; 118 } 119 120 alias TokenLength = Tuple!(size_t, "length", bool, "newLine", bool, "multiLine"); 121 static TokenLength tokenLength()(auto ref const Token tok, size_t prevLine) 122 { 123 import std.utf : byDchar; 124 125 size_t length; 126 const newLine = tok.line > prevLine; 127 bool multiLine; 128 if (tok.text is null) 129 length += str(tok.type).length; 130 else 131 foreach (c; tok.text.byDchar) 132 { 133 if (isLineSeparator(c)) 134 { 135 length = 1; 136 multiLine = true; 137 } 138 else 139 length += getEditorLength(c); 140 } 141 142 return TokenLength(length, newLine, multiLine); 143 } 144 145 unittest 146 { 147 assert(tokenLength(Token(tok!"stringLiteral", "aaa", 0, 0, 0), 0).length == 3); 148 assert(tokenLength(Token(tok!"stringLiteral", "Дистан", 0, 0, 0), 0).length == 6); 149 // tabs and whitespace 150 assert(tokenLength(Token(tok!"stringLiteral", " ", 0, 0, 0), 0).length == 4); 151 assert(tokenLength(Token(tok!"stringLiteral", " ", 0, 0, 0), 0).length == 4); 152 } 153 154 import std.conv : to; 155 156 enum string KEY = "dscanner.style.long_line"; 157 enum string MESSAGE = "Line is longer than " ~ to!string(MAX_LINE_LENGTH) ~ " characters"; 158 enum MAX_LINE_LENGTH = 120; 159 const(Token)[] tokens; 160 } 161 162 @system unittest 163 { 164 import dscanner.analysis.config : Check, StaticAnalysisConfig, disabledConfig; 165 import dscanner.analysis.helpers : assertAnalyzerWarnings; 166 import std.stdio : stderr; 167 168 StaticAnalysisConfig sac = disabledConfig(); 169 sac.long_line_check = Check.enabled; 170 171 assertAnalyzerWarnings(q{ 172 Window window = Platform.instance.createWindow("Дистанционное управление сварочным оборудованием ", null); 173 Window window = Platform.instance.createWindow("Дистанционное управление сварочным оборудованием ", null); // [warn]: Line is longer than 120 characters 174 unittest { 175 // with tabs 176 assert("foo" == "foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo1"); 177 assert("foo" == "fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo2"); // [warn]: Line is longer than 120 characters 178 // with whitespace (don't overwrite) 179 assert("foo" == "boooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo3"); 180 assert("foo" == "booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo4"); // [warn]: Line is longer than 120 characters 181 } 182 }c, sac); 183 184 // TODO: libdparse counts columns bytewise 185 //assert("foo" == "boooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo5"); 186 //assert("foo" == "booooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo6"); // [warn]: Line is longer than 120 characters 187 188 // reduced from std/regex/internal/thompson.d 189 assertAnalyzerWarnings(q{ 190 // whitespace on purpose, do not remove! 191 mixin(`case IR.`~e~`: 192 opCacheTrue[pc] = &Ops!(true).op!(IR.`~e~`); 193 opCacheBackTrue[pc] = &BackOps!(true).op!(IR.`~e~`); 194 `); 195 mixin(`case IR.`~e~`: 196 opCacheTrue[pc] = &Ops!(true).op!(IR.`~e~`); 197 opCacheTrue[pc] = &Ops!(true).op!(IR.`~e~`); 198 opCacheBackTrue[pc] = &BackOps!(true).op!(IR.`~e~`); // [warn]: Line is longer than 120 characters 199 `); 200 }c, sac); 201 202 stderr.writeln("Unittest for LineLengthCheck passed."); 203 }