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