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