1 //          Copyright Brian Schott (Hackerpilot) 2014.
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.objectconst;
7 
8 import std.stdio;
9 import std.regex;
10 import dparse.ast;
11 import dparse.lexer;
12 import dscanner.analysis.base;
13 import dscanner.analysis.helpers;
14 import dsymbol.scope_ : Scope;
15 
16 /**
17  * Checks that opEquals, opCmp, toHash, 'opCast', and toString are either const,
18  * immutable, or inout.
19  */
20 final class ObjectConstCheck : BaseAnalyzer
21 {
22 	alias visit = BaseAnalyzer.visit;
23 
24 	///
25 	this(string fileName, const(Scope)* sc, bool skipTests = false)
26 	{
27 		super(fileName, sc, skipTests);
28 	}
29 
30 	mixin visitTemplate!ClassDeclaration;
31 	mixin visitTemplate!InterfaceDeclaration;
32 	mixin visitTemplate!UnionDeclaration;
33 	mixin visitTemplate!StructDeclaration;
34 
35 	override void visit(const AttributeDeclaration d)
36 	{
37 		if (d.attribute.attribute == tok!"const" && inAggregate)
38 		{
39 			constColon = true;
40 		}
41 		d.accept(this);
42 	}
43 
44 	override void visit(const Declaration d)
45 	{
46 		import std.algorithm : any;
47 		bool setConstBlock;
48 		if (inAggregate && d.attributes && d.attributes.any!(a => a.attribute == tok!"const"))
49 		{
50 			setConstBlock = true;
51 			constBlock = true;
52 		}
53 
54 		bool containsDisable(A)(const A[] attribs)
55 		{
56 			import std.algorithm.searching : canFind;
57 			return attribs.canFind!(a => a.atAttribute !is null &&
58 				a.atAttribute.identifier.text == "disable");
59 		}
60 
61 		if (const FunctionDeclaration fd = d.functionDeclaration)
62 		{
63 			const isDeclationDisabled = containsDisable(d.attributes) ||
64 				containsDisable(fd.memberFunctionAttributes);
65 
66 			if (inAggregate && !constColon && !constBlock && !isDeclationDisabled
67 					&& isInteresting(fd.name.text) && !hasConst(fd.memberFunctionAttributes))
68 			{
69 				addErrorMessage(d.functionDeclaration.name.line,
70 						d.functionDeclaration.name.column, "dscanner.suspicious.object_const",
71 						"Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const.");
72 			}
73 		}
74 
75 		d.accept(this);
76 
77 		if (!inAggregate)
78 			constColon = false;
79 		if (setConstBlock)
80 			constBlock = false;
81 	}
82 
83 	private static bool hasConst(const MemberFunctionAttribute[] attributes)
84 	{
85 		import std.algorithm : any;
86 
87 		return attributes.any!(a => a.tokenType == tok!"const"
88 				|| a.tokenType == tok!"immutable" || a.tokenType == tok!"inout");
89 	}
90 
91 	private static bool isInteresting(string name)
92 	{
93 		return name == "opCmp" || name == "toHash" || name == "opEquals"
94 			|| name == "toString" || name == "opCast";
95 	}
96 
97 	private bool constBlock;
98 	private bool constColon;
99 
100 }
101 
102 unittest
103 {
104 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
105 
106 	StaticAnalysisConfig sac = disabledConfig();
107 	sac.object_const_check = Check.enabled;
108 	assertAnalyzerWarnings(q{
109 		void testConsts()
110 		{
111 			// Will be ok because all are declared const/immutable
112 			class Cat
113 			{
114 				const bool opEquals(Object a, Object b) // ok
115 				{
116 					return true;
117 				}
118 
119 				const int opCmp(Object o) // ok
120 				{
121 					return 1;
122 				}
123 
124 				const hash_t toHash() // ok
125 				{
126 					return 0;
127 				}
128 
129 				const string toString() // ok
130 				{
131 					return "Cat";
132 				}
133 			}
134 
135 			class Bat
136 			{
137 				const: override string toString() { return "foo"; } // ok
138 			}
139 
140 			class Fox
141 			{
142 				const{ override string toString() { return "foo"; }} // ok
143 			}
144 
145 			class Rat
146 			{
147 				bool opEquals(Object a, Object b) @disable; // ok
148 			}
149 
150 			class Ant
151 			{
152 				@disable bool opEquals(Object a, Object b); // ok
153 			}
154 
155 			// Will warn, because none are const
156 			class Dog
157 			{
158 				bool opEquals(Object a, Object b) // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const.
159 				{
160 					return true;
161 				}
162 
163 				int opCmp(Object o) // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const.
164 				{
165 					return 1;
166 				}
167 
168 				hash_t toHash() // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const.
169 				{
170 					return 0;
171 				}
172 
173 				string toString() // [warn]: Methods 'opCmp', 'toHash', 'opEquals', 'opCast', and/or 'toString' are non-const.
174 				{
175 					return "Dog";
176 				}
177 			}
178 		}
179 	}}, sac);
180 
181 	stderr.writeln("Unittest for ObjectConstCheck passed.");
182 }