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 }