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 std.d.ast;
10 import std.d.lexer;
11 import analysis.base;
12 import analysis.helpers;
13 
14 /**
15  * Checks for when a class/struct has the method opEquals without toHash, or
16  * toHash without opEquals.
17  */
18 class OpEqualsWithoutToHashCheck : BaseAnalyzer
19 {
20 	alias visit = BaseAnalyzer.visit;
21 
22 	this(string fileName)
23 	{
24 		super(fileName);
25 	}
26 
27 	override void visit(const ClassDeclaration node)
28 	{
29 		actualCheck(node.name, node.structBody);
30 		node.accept(this);
31 	}
32 
33 	override void visit(const StructDeclaration node)
34 	{
35 		actualCheck(node.name, node.structBody);
36 		node.accept(this);
37 	}
38 
39 	private void actualCheck(const Token name, const StructBody structBody)
40 	{
41 		bool hasOpEquals = false;
42 		bool hasToHash = false;
43 		bool hasOpCmp = false;
44 
45 		// Just return if missing children
46 		if (!structBody
47 			|| !structBody.declarations
48 			|| name is Token.init)
49 			return;
50 
51 		// Check all the function declarations
52 		foreach (declaration; structBody.declarations)
53 		{
54 			// Skip if not a function declaration
55 			if (!declaration
56 				|| !declaration.functionDeclaration)
57 				continue;
58 
59 			// Check if opEquals or toHash
60 			immutable string methodName = declaration.functionDeclaration.name.text;
61 			if (methodName == "opEquals")
62 				hasOpEquals = true;
63 			else if (methodName == "toHash")
64 				hasToHash = true;
65 			else if (methodName == "opCmp")
66 				hasOpCmp = true;
67 		}
68 
69 		// Warn if has opEquals, but not toHash
70 		if (hasOpEquals && !hasToHash)
71 		{
72 			string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'.";
73 			addErrorMessage(name.line, name.column, KEY, message);
74 		}
75 		// Warn if has toHash, but not opEquals
76 		else if (!hasOpEquals && hasToHash)
77 		{
78 			string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'.";
79 			addErrorMessage(name.line, name.column, KEY, message);
80 		}
81 
82 		if (hasOpCmp && !hasOpEquals)
83 		{
84 			addErrorMessage(name.line, name.column, KEY,
85 				"'" ~ name.text ~ "' has method 'opCmp', but not 'opEquals'.");
86 		}
87 	}
88 
89 	enum string KEY = "dscanner.suspicious.incomplete_operator_overloading";
90 }
91 
92 unittest
93 {
94 	import analysis.config : StaticAnalysisConfig;
95 
96 	StaticAnalysisConfig sac;
97 	sac.opequals_tohash_check = true;
98 	assertAnalyzerWarnings(q{
99 		// Success because it has opEquals and toHash
100 		class Chimp
101 		{
102 			const bool opEquals(Object a, Object b)
103 			{
104 				return true;
105 			}
106 
107 			const override hash_t toHash()
108 			{
109 				return 0;
110 			}
111 		}
112 
113 		// Fail on class opEquals
114 		class Rabbit // [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'.
115 		{
116 			const bool opEquals(Object a, Object b)
117 			{
118 				return true;
119 			}
120 		}
121 
122 		// Fail on class toHash
123 		class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'.
124 		{
125 			override const hash_t toHash()
126 			{
127 				return 0;
128 			}
129 		}
130 
131 		// Fail on struct opEquals
132 		struct Tarantula // [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'.
133 		{
134 			const bool opEquals(Object a, Object b)
135 			{
136 				return true;
137 			}
138 		}
139 
140 		// Fail on struct toHash
141 		struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'.
142 		{
143 			const nothrow @safe hash_t toHash()
144 			{
145 				return 0;
146 			}
147 		}
148 	}}, sac);
149 
150 	stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed.");
151 }
152