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 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 			// 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 
81 	enum string KEY = "dscanner.suspicious.incomplete_operator_overloading";
82 }
83 
84 unittest
85 {
86 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
87 
88 	StaticAnalysisConfig sac = disabledConfig();
89 	sac.opequals_tohash_check = Check.enabled;
90 	assertAnalyzerWarnings(q{
91 		// Success because it has opEquals and toHash
92 		class Chimp
93 		{
94 			const bool opEquals(Object a, Object b)
95 			{
96 				return true;
97 			}
98 
99 			const override hash_t toHash()
100 			{
101 				return 0;
102 			}
103 		}
104 
105 		// AA would use default equal and default toHash
106 		struct Bee
107 		{
108 			int opCmp(Bee) const
109 			{
110 				return true;
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 	}c, sac);
150 
151 	stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed.");
152 }