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