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.assert_without_msg;
6 
7 import dscanner.analysis.base : BaseAnalyzer;
8 import dsymbol.scope_ : Scope;
9 import dparse.lexer;
10 import dparse.ast;
11 
12 import std.stdio;
13 import std.algorithm;
14 
15 /**
16  * Check that all asserts have an explanatory message.
17  */
18 class AssertWithoutMessageCheck : BaseAnalyzer
19 {
20 	enum string KEY = "dscanner.style.assert_without_msg";
21 	enum string MESSAGE = "An assert should have an explanatory message";
22 
23 	///
24 	this(string fileName, const(Scope)* sc, bool skipTests = false)
25 	{
26 		super(fileName, sc, skipTests);
27 	}
28 
29 	override void visit(const AssertExpression expr)
30 	{
31 		if (expr.message is null)
32 			addErrorMessage(expr.line, expr.column, KEY, MESSAGE);
33 	}
34 
35 	override void visit(const FunctionCallExpression expr)
36 	{
37 		if (!isStdExceptionImported)
38 			return;
39 
40 		if (expr.unaryExpression !is null &&
41 			expr.unaryExpression.primaryExpression !is null &&
42 			expr.unaryExpression.primaryExpression.identifierOrTemplateInstance !is null)
43 		{
44 			auto ident = expr.unaryExpression.primaryExpression.identifierOrTemplateInstance.identifier;
45 			if (ident.text == "enforce" && expr.arguments !is null && expr.arguments.argumentList !is null &&
46 					expr.arguments.argumentList.items.length < 2)
47 				addErrorMessage(ident.line, ident.column, KEY, MESSAGE);
48 		}
49 	}
50 
51 	override void visit(const SingleImport sImport)
52 	{
53 		static immutable stdException = ["std", "exception"];
54 		if (sImport.identifierChain.identifiers.map!(a => a.text).equal(stdException))
55 			isStdExceptionImported = true;
56 	}
57 
58 	// revert the stack after new scopes
59 	override void visit(const Declaration decl)
60 	{
61 		// be careful - ImportDeclarations don't introduce a new scope
62 		if (decl.importDeclaration is null)
63 		{
64 			bool tmp = isStdExceptionImported;
65 			scope(exit) isStdExceptionImported = tmp;
66 			decl.accept(this);
67 		}
68 		else
69 			decl.accept(this);
70 	}
71 
72 	mixin ScopedVisit!IfStatement;
73 	mixin ScopedVisit!BlockStatement;
74 
75 	alias visit = BaseAnalyzer.visit;
76 
77 private:
78 	bool isStdExceptionImported;
79 
80 	template ScopedVisit(NodeType)
81 	{
82 		override void visit(const NodeType n)
83 		{
84 			bool tmp = isStdExceptionImported;
85 			scope(exit) isStdExceptionImported = tmp;
86 			n.accept(this);
87 		}
88 	}
89 }
90 
91 unittest
92 {
93 	import std.stdio : stderr;
94 	import std.format : format;
95 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
96 	import dscanner.analysis.helpers : assertAnalyzerWarnings;
97 
98 	StaticAnalysisConfig sac = disabledConfig();
99 	sac.assert_without_msg = Check.enabled;
100 
101 	assertAnalyzerWarnings(q{
102 	unittest {
103 		assert(0, "foo bar");
104 		assert(0); // [warn]: %s
105 	}
106 	}}.format(
107 		AssertWithoutMessageCheck.MESSAGE,
108 	), sac);
109 
110 	assertAnalyzerWarnings(q{
111 	unittest {
112 		static assert(0, "foo bar");
113 		static assert(0); // [warn]: %s
114 	}
115 	}}.format(
116 		AssertWithoutMessageCheck.MESSAGE,
117 	), sac);
118 
119 	// check for std.exception.enforce
120 	assertAnalyzerWarnings(q{
121 	unittest {
122 		enforce(0); // std.exception not imported yet - could be a user-defined symbol
123 		import std.exception;
124 		enforce(0, "foo bar");
125 		enforce(0); // [warn]: %s
126 	}
127 	}}.format(
128 		AssertWithoutMessageCheck.MESSAGE,
129 	), sac);
130 
131 	// check for std.exception.enforce
132 	assertAnalyzerWarnings(q{
133 	unittest {
134 		import exception;
135 		class C {
136 			import std.exception;
137 		}
138 		enforce(0); // std.exception not imported yet - could be a user-defined symbol
139 		struct S {
140 			import std.exception;
141 		}
142 		enforce(0); // std.exception not imported yet - could be a user-defined symbol
143 		if (false) {
144 			import std.exception;
145 		}
146 		enforce(0); // std.exception not imported yet - could be a user-defined symbol
147 		{
148 			import std.exception;
149 		}
150 		enforce(0); // std.exception not imported yet - could be a user-defined symbol
151 	}
152 	}}, sac);
153 
154 	stderr.writeln("Unittest for AssertWithoutMessageCheck passed.");
155 }