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 }