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