1 // Distributed under the Boost Software License, Version 1.0.
2 //	  (See accompanying file LICENSE_1_0.txt or copy at
3 //			http://www.boost.org/LICENSE_1_0.txt)
4 
5 module dscanner.analysis.if_constraints_indent;
6 
7 import dparse.lexer;
8 import dparse.ast;
9 import dscanner.analysis.base;
10 import dsymbol.scope_ : Scope;
11 
12 import std.algorithm.iteration : filter;
13 import std.range;
14 
15 /**
16 Checks whether all if constraints have the same indention as their declaration.
17 */
18 final class IfConstraintsIndentCheck : BaseAnalyzer
19 {
20 	mixin AnalyzerInfo!"if_constraints_indent";
21 
22 	///
23 	this(string fileName, const(Token)[] tokens, bool skipTests = false)
24 	{
25 		super(fileName, null, skipTests);
26 
27 		// convert tokens to a list of token starting positions per line
28 
29 		// libdparse columns start at 1
30 		foreach (t; tokens)
31 		{
32 			// pad empty positions if we skip empty token-less lines
33 			// t.line (unsigned) may be 0 if the token is uninitialized/broken, so don't subtract from it
34 			// equivalent to: firstSymbolAtLine.length < t.line - 1
35 			while (firstSymbolAtLine.length + 1 < t.line)
36 				firstSymbolAtLine ~= Pos(1);
37 
38 			// insert a new line with positions if new line is reached
39 			// (previous while pads skipped lines)
40 			if (firstSymbolAtLine.length < t.line)
41 				firstSymbolAtLine ~= Pos(t.column, t.type == tok!"if");
42 		}
43 	}
44 
45 	override void visit(const FunctionDeclaration decl)
46 	{
47 		if (decl.constraint !is null)
48 			checkConstraintSpace(decl.constraint, decl.name);
49 	}
50 
51 	override void visit(const InterfaceDeclaration decl)
52 	{
53 		if (decl.constraint !is null)
54 			checkConstraintSpace(decl.constraint, decl.name);
55 	}
56 
57 
58 	override void visit(const ClassDeclaration decl)
59 	{
60 		if (decl.constraint !is null)
61 			checkConstraintSpace(decl.constraint, decl.name);
62 	}
63 
64 	override void visit(const TemplateDeclaration decl)
65 	{
66 		if (decl.constraint !is null)
67 			checkConstraintSpace(decl.constraint, decl.name);
68 	}
69 
70 	override void visit(const UnionDeclaration decl)
71 	{
72 		if (decl.constraint !is null)
73 			checkConstraintSpace(decl.constraint, decl.name);
74 	}
75 
76 	override void visit(const StructDeclaration decl)
77 	{
78 		if (decl.constraint !is null)
79 			checkConstraintSpace(decl.constraint, decl.name);
80 	}
81 
82 	override void visit(const Constructor decl)
83 	{
84 		if (decl.constraint !is null)
85 			checkConstraintSpace(decl.constraint, decl.line);
86 	}
87 
88 	alias visit = ASTVisitor.visit;
89 
90 private:
91 
92 	enum string KEY = "dscanner.style.if_constraints_indent";
93 	enum string MESSAGE = "If constraints should have the same indentation as the function";
94 
95 	Pos[] firstSymbolAtLine;
96 	static struct Pos
97 	{
98 		size_t column;
99 		bool isIf;
100 	}
101 
102 	/**
103 	Check indentation of constraints
104 	*/
105 	void checkConstraintSpace(const Constraint constraint, const Token token)
106 	{
107 		checkConstraintSpace(constraint, token.line);
108 	}
109 
110 	void checkConstraintSpace(const Constraint constraint, size_t line)
111 	{
112 		// dscanner lines start at 1
113 		auto pDecl = firstSymbolAtLine[line - 1];
114 
115 		// search for constraint if (might not be on the same line as the expression)
116 		auto r = firstSymbolAtLine[line .. constraint.expression.line].retro.filter!(s => s.isIf);
117 
118 		// no hit = constraint is on the same line
119 		if (r.empty)
120 			addErrorMessage(constraint.expression.line, constraint.expression.column, KEY, MESSAGE);
121 		else if (pDecl.column != r.front.column)
122 			addErrorMessage(constraint.expression.line, constraint.expression.column, KEY, MESSAGE);
123 	}
124 }
125 
126 unittest
127 {
128 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
129 	import dscanner.analysis.helpers : assertAnalyzerWarnings;
130 	import std.format : format;
131 	import std.stdio : stderr;
132 
133 	StaticAnalysisConfig sac = disabledConfig();
134 	sac.if_constraints_indent = Check.enabled;
135 
136 	assertAnalyzerWarnings(q{
137 void foo(R)(R r)
138 if (R == null)
139 {}
140 
141 void foo(R)(R r)
142 	if (R == null) // [warn]: %s
143 {}
144 	}}.format(
145 		IfConstraintsIndentCheck.MESSAGE,
146 	), sac);
147 
148 	assertAnalyzerWarnings(q{
149 	void foo(R)(R r)
150 	if (R == null)
151 	{}
152 
153 	void foo(R)(R r)
154 if (R == null) // [warn]: %s
155 	{}
156 
157 	void foo(R)(R r)
158 		if (R == null) // [warn]: %s
159 	{}
160 	}}.format(
161 		IfConstraintsIndentCheck.MESSAGE,
162 		IfConstraintsIndentCheck.MESSAGE,
163 	), sac);
164 
165 	assertAnalyzerWarnings(q{
166 	struct Foo(R)
167 	if (R == null)
168 	{}
169 
170 	struct Foo(R)
171 if (R == null) // [warn]: %s
172 	{}
173 
174 	struct Foo(R)
175 		if (R == null) // [warn]: %s
176 	{}
177 	}}.format(
178 		IfConstraintsIndentCheck.MESSAGE,
179 		IfConstraintsIndentCheck.MESSAGE,
180 	), sac);
181 
182 	// test example from Phobos
183 	assertAnalyzerWarnings(q{
184 Num abs(Num)(Num x) @safe pure nothrow
185 if (is(typeof(Num.init >= 0)) && is(typeof(-Num.init)) &&
186 	!(is(Num* : const(ifloat*)) || is(Num* : const(idouble*))
187 	|| is(Num* : const(ireal*))))
188 {
189 	static if (isFloatingPoint!(Num))
190 		return fabs(x);
191 	else
192 		return x >= 0 ? x : -x;
193 }
194 	}, sac);
195 
196 	// weird constraint formatting
197 	assertAnalyzerWarnings(q{
198 	struct Foo(R)
199 	if
200 	(R == null)
201 	{}
202 
203 	struct Foo(R)
204 	if
205 		(R == null)
206 	{}
207 
208 	struct Foo(R)
209 if
210 	(R == null) // [warn]: %s
211 	{}
212 
213 	struct Foo(R)
214 	if (
215 	R == null)
216 	{}
217 
218 	struct Foo(R)
219 	if (
220 		R == null
221 	)
222 	{}
223 
224 	struct Foo(R)
225 		if (
226 		R == null // [warn]: %s
227 	) {}
228 	}}.format(
229 		IfConstraintsIndentCheck.MESSAGE,
230 		IfConstraintsIndentCheck.MESSAGE,
231 	), sac);
232 
233 	// constraint on the same line
234 	assertAnalyzerWarnings(q{
235 	struct CRC(uint N, ulong P) if (N == 32 || N == 64) // [warn]: %s
236 	{}
237 	}}.format(
238 		IfConstraintsIndentCheck.MESSAGE,
239 	), sac);
240 
241 	stderr.writeln("Unittest for IfConstraintsIndentCheck passed.");
242 }
243 
244 @("issue #829")
245 unittest
246 {
247 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
248 	import dscanner.analysis.helpers : assertAnalyzerWarnings;
249 	import std.format : format;
250 	import std.stdio : stderr;
251 
252 	StaticAnalysisConfig sac = disabledConfig();
253 	sac.if_constraints_indent = Check.enabled;
254 
255 	assertAnalyzerWarnings(`void foo() {
256 	''
257 }`, sac);
258 }