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 final 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 bool containsDisable(A)(const A[] attribs) 58 { 59 import std.algorithm.searching : canFind; 60 return attribs.canFind!(a => a.atAttribute !is null && 61 a.atAttribute.identifier.text == "disable"); 62 } 63 64 const isDeclationDisabled = containsDisable(declaration.attributes) || 65 containsDisable(declaration.functionDeclaration.memberFunctionAttributes); 66 67 if (isDeclationDisabled) 68 continue; 69 70 // Check if opEquals or toHash 71 immutable string methodName = declaration.functionDeclaration.name.text; 72 if (methodName == "opEquals") 73 hasOpEquals = true; 74 else if (methodName == "toHash") 75 hasToHash = true; 76 else if (methodName == "opCmp") 77 hasOpCmp = true; 78 } 79 80 // Warn if has opEquals, but not toHash 81 if (hasOpEquals && !hasToHash) 82 { 83 string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'."; 84 addErrorMessage(name.line, name.column, KEY, message); 85 } 86 // Warn if has toHash, but not opEquals 87 else if (!hasOpEquals && hasToHash) 88 { 89 string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'."; 90 addErrorMessage(name.line, name.column, KEY, message); 91 } 92 } 93 94 enum string KEY = "dscanner.suspicious.incomplete_operator_overloading"; 95 } 96 97 unittest 98 { 99 import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 100 101 StaticAnalysisConfig sac = disabledConfig(); 102 sac.opequals_tohash_check = Check.enabled; 103 assertAnalyzerWarnings(q{ 104 // Success because it has opEquals and toHash 105 class Chimp 106 { 107 const bool opEquals(Object a, Object b) 108 { 109 return true; 110 } 111 112 const override hash_t toHash() 113 { 114 return 0; 115 } 116 } 117 118 // AA would use default equal and default toHash 119 struct Bee 120 { 121 int opCmp(Bee) const 122 { 123 return true; 124 } 125 } 126 127 // Fail on class opEquals 128 class Rabbit // [warn]: 'Rabbit' has method 'opEquals', but not 'toHash'. 129 { 130 const bool opEquals(Object a, Object b) 131 { 132 return true; 133 } 134 } 135 136 // Fail on class toHash 137 class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'. 138 { 139 override const hash_t toHash() 140 { 141 return 0; 142 } 143 } 144 145 // Fail on struct opEquals 146 struct Tarantula // [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'. 147 { 148 const bool opEquals(Object a, Object b) 149 { 150 return true; 151 } 152 } 153 154 // Fail on struct toHash 155 struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'. 156 { 157 const nothrow @safe hash_t toHash() 158 { 159 return 0; 160 } 161 } 162 163 // issue #659, do not warn if one miss and the other is not callable 164 struct Fox {const nothrow @safe hash_t toHash() @disable;} 165 struct Bat {@disable const nothrow @safe hash_t toHash();} 166 struct Rat {const bool opEquals(Object a, Object b) @disable;} 167 struct Cat {@disable const bool opEquals(Object a, Object b);} 168 169 }}, sac); 170 171 stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed."); 172 }