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