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 analysis.opequals_without_tohash;
7 
8 import std.stdio;
9 import dparse.ast;
10 import dparse.lexer;
11 import analysis.base;
12 import 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 class OpEqualsWithoutToHashCheck : BaseAnalyzer
20 {
21 	alias visit = BaseAnalyzer.visit;
22 
23 	this(string fileName, const(Scope)* sc)
24 	{
25 		super(fileName, sc);
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 = false;
43 		bool hasToHash = false;
44 		bool hasOpCmp = false;
45 
46 		// Just return if missing children
47 		if (!structBody
48 			|| !structBody.declarations
49 			|| 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
57 				|| !declaration.functionDeclaration)
58 				continue;
59 
60 			// Check if opEquals or toHash
61 			immutable string methodName = declaration.functionDeclaration.name.text;
62 			if (methodName == "opEquals")
63 				hasOpEquals = true;
64 			else if (methodName == "toHash")
65 				hasToHash = true;
66 			else if (methodName == "opCmp")
67 				hasOpCmp = true;
68 		}
69 
70 		// Warn if has opEquals, but not toHash
71 		if (hasOpEquals && !hasToHash)
72 		{
73 			string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'.";
74 			addErrorMessage(name.line, name.column, KEY, message);
75 		}
76 		// Warn if has toHash, but not opEquals
77 		else if (!hasOpEquals && hasToHash)
78 		{
79 			string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'.";
80 			addErrorMessage(name.line, name.column, KEY, message);
81 		}
82 
83 		if (hasOpCmp && !hasOpEquals)
84 		{
85 			addErrorMessage(name.line, name.column, KEY,
86 				"'" ~ name.text ~ "' has method 'opCmp', but not 'opEquals'.");
87 		}
88 	}
89 
90 	enum string KEY = "dscanner.suspicious.incomplete_operator_overloading";
91 }
92 
93 unittest
94 {
95 	import analysis.config : StaticAnalysisConfig;
96 
97 	StaticAnalysisConfig sac;
98 	sac.opequals_tohash_check = true;
99 	assertAnalyzerWarnings(q{
100 		// Success because it has opEquals and toHash
101 		class Chimp
102 		{
103 			const bool opEquals(Object a, Object b)
104 			{
105 				return true;
106 			}
107 
108 			const override hash_t toHash()
109 			{
110 				return 0;
111 			}
112 		}
113 
114 		// Fail on class opEquals
115 		class Rabbit // [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'.
116 		{
117 			const bool opEquals(Object a, Object b)
118 			{
119 				return true;
120 			}
121 		}
122 
123 		// Fail on class toHash
124 		class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'.
125 		{
126 			override const hash_t toHash()
127 			{
128 				return 0;
129 			}
130 		}
131 
132 		// Fail on struct opEquals
133 		struct Tarantula // [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'.
134 		{
135 			const bool opEquals(Object a, Object b)
136 			{
137 				return true;
138 			}
139 		}
140 
141 		// Fail on struct toHash
142 		struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'.
143 		{
144 			const nothrow @safe hash_t toHash()
145 			{
146 				return 0;
147 			}
148 		}
149 	}}, sac);
150 
151 	stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed.");
152 }
153