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 }