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 || !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 			// Check if opEquals or toHash
58 			immutable string methodName = declaration.functionDeclaration.name.text;
59 			if (methodName == "opEquals")
60 				hasOpEquals = true;
61 			else if (methodName == "toHash")
62 				hasToHash = true;
63 			else if (methodName == "opCmp")
64 				hasOpCmp = true;
65 		}
66 
67 		// Warn if has opEquals, but not toHash
68 		if (hasOpEquals && !hasToHash)
69 		{
70 			string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'.";
71 			addErrorMessage(name.line, name.column, KEY, message);
72 		}
73 		// Warn if has toHash, but not opEquals
74 		else if (!hasOpEquals && hasToHash)
75 		{
76 			string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'.";
77 			addErrorMessage(name.line, name.column, KEY, message);
78 		}
79 
80 		if (hasOpCmp && !hasOpEquals)
81 		{
82 			addErrorMessage(name.line, name.column, KEY,
83 					"'" ~ name.text ~ "' has method 'opCmp', but not 'opEquals'.");
84 		}
85 	}
86 
87 	enum string KEY = "dscanner.suspicious.incomplete_operator_overloading";
88 }
89 
90 unittest
91 {
92 	import analysis.config : StaticAnalysisConfig;
93 
94 	StaticAnalysisConfig sac;
95 	sac.opequals_tohash_check = true;
96 	assertAnalyzerWarnings(q{
97 		// Success because it has opEquals and toHash
98 		class Chimp
99 		{
100 			const bool opEquals(Object a, Object b)
101 			{
102 				return true;
103 			}
104 
105 			const override hash_t toHash()
106 			{
107 				return 0;
108 			}
109 		}
110 
111 		// Fail on class opEquals
112 		class Rabbit // [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'.
113 		{
114 			const bool opEquals(Object a, Object b)
115 			{
116 				return true;
117 			}
118 		}
119 
120 		// Fail on class toHash
121 		class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'.
122 		{
123 			override const hash_t toHash()
124 			{
125 				return 0;
126 			}
127 		}
128 
129 		// Fail on struct opEquals
130 		struct Tarantula // [warn]: 'Tarantula' 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 struct toHash
139 		struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'.
140 		{
141 			const nothrow @safe hash_t toHash()
142 			{
143 				return 0;
144 			}
145 		}
146 	}}, sac);
147 
148 	stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed.");
149 }