1 // Copyright (c) 2014, Matthew Brennan Jones <matthew.brennan.jones@gmail.com>
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.opequals_without_tohash;
7 
8 import std.stdio;
9 import dparse.ast;
10 import dparse.lexer;
11 import dscanner.analysis.base;
12 import dscanner.analysis.helpers;
13 import dsymbol.scope_ : Scope;
14 
15 /**
16  * Checks for when a class/struct has the method opEquals without toHash, or
17  * toHash without opEquals.
18  */
19 final class OpEqualsWithoutToHashCheck : BaseAnalyzer
20 {
21 	alias visit = BaseAnalyzer.visit;
22 
23 	mixin AnalyzerInfo!"opequals_tohash_check";
24 
25 	this(string fileName, const(Scope)* sc, bool skipTests = false)
26 	{
27 		super(fileName, sc, skipTests);
28 	}
29 
30 	override void visit(const ClassDeclaration node)
31 	{
32 		actualCheck(node.name, node.structBody);
33 		node.accept(this);
34 	}
35 
36 	override void visit(const StructDeclaration node)
37 	{
38 		actualCheck(node.name, node.structBody);
39 		node.accept(this);
40 	}
41 
42 	private void actualCheck(const Token name, const StructBody structBody)
43 	{
44 		bool hasOpEquals;
45 		bool hasToHash;
46 		bool hasOpCmp;
47 
48 		// Just return if missing children
49 		if (!structBody || !structBody.declarations || name is Token.init)
50 			return;
51 
52 		// Check all the function declarations
53 		foreach (declaration; structBody.declarations)
54 		{
55 			// Skip if not a function declaration
56 			if (!declaration || !declaration.functionDeclaration)
57 				continue;
58 
59 			bool containsDisable(A)(const A[] attribs)
60 			{
61 				import std.algorithm.searching : canFind;
62 				return attribs.canFind!(a => a.atAttribute !is null &&
63 					a.atAttribute.identifier.text == "disable");
64 			}
65 
66 			const isDeclationDisabled = containsDisable(declaration.attributes) ||
67 				containsDisable(declaration.functionDeclaration.memberFunctionAttributes);
68 
69 			if (isDeclationDisabled)
70 				continue;
71 
72 			// Check if opEquals or toHash
73 			immutable string methodName = declaration.functionDeclaration.name.text;
74 			if (methodName == "opEquals")
75 				hasOpEquals = true;
76 			else if (methodName == "toHash")
77 				hasToHash = true;
78 			else if (methodName == "opCmp")
79 				hasOpCmp = true;
80 		}
81 
82 		// Warn if has opEquals, but not toHash
83 		if (hasOpEquals && !hasToHash)
84 		{
85 			string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'.";
86 			addErrorMessage(name.line, name.column, KEY, message);
87 		}
88 		// Warn if has toHash, but not opEquals
89 		else if (!hasOpEquals && hasToHash)
90 		{
91 			string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'.";
92 			addErrorMessage(name.line, name.column, KEY, message);
93 		}
94 	}
95 
96 	enum string KEY = "dscanner.suspicious.incomplete_operator_overloading";
97 }
98 
99 unittest
100 {
101 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
102 
103 	StaticAnalysisConfig sac = disabledConfig();
104 	sac.opequals_tohash_check = Check.enabled;
105 	assertAnalyzerWarnings(q{
106 		// Success because it has opEquals and toHash
107 		class Chimp
108 		{
109 			const bool opEquals(Object a, Object b)
110 			{
111 				return true;
112 			}
113 
114 			const override hash_t toHash()
115 			{
116 				return 0;
117 			}
118 		}
119 
120 		// AA would use default equal and default toHash
121 		struct Bee
122 		{
123 			int opCmp(Bee) const
124 			{
125 				return true;
126 			}
127 		}
128 
129 		// Fail on class opEquals
130 		class Rabbit // [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'.
131 		{
132 			const bool opEquals(Object a, Object b)
133 			{
134 				return true;
135 			}
136 		}
137 
138 		// Fail on class toHash
139 		class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'.
140 		{
141 			override const hash_t toHash()
142 			{
143 				return 0;
144 			}
145 		}
146 
147 		// Fail on struct opEquals
148 		struct Tarantula // [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'.
149 		{
150 			const bool opEquals(Object a, Object b)
151 			{
152 				return true;
153 			}
154 		}
155 
156 		// Fail on struct toHash
157 		struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'.
158 		{
159 			const nothrow @safe hash_t toHash()
160 			{
161 				return 0;
162 			}
163 		}
164 
165 		// issue #659, do not warn if one miss and the other is not callable
166 		struct Fox {const nothrow @safe hash_t toHash() @disable;}
167 		struct Bat {@disable const nothrow @safe hash_t toHash();}
168 		struct Rat {const bool opEquals(Object a, Object b) @disable;}
169 		struct Cat {@disable const bool opEquals(Object a, Object b);}
170 
171 	}}, sac);
172 
173 	stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed.");
174 }