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 mixin AnalyzerInfo!"opequals_tohash_check"; 24 25 this(string fileName, const(Scope)* sc, bool skipTests = false) 26 { 27 super(fileName, sc, skipTests); 28 } 29 30 override void visit(const ClassDeclaration node) 31 { 32 actualCheck(node.name, node.structBody); 33 node.accept(this); 34 } 35 36 override void visit(const StructDeclaration node) 37 { 38 actualCheck(node.name, node.structBody); 39 node.accept(this); 40 } 41 42 private void actualCheck(const Token name, const StructBody structBody) 43 { 44 bool hasOpEquals; 45 bool hasToHash; 46 bool hasOpCmp; 47 48 // Just return if missing children 49 if (!structBody || !structBody.declarations || 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 || !declaration.functionDeclaration) 57 continue; 58 59 bool containsDisable(A)(const A[] attribs) 60 { 61 import std.algorithm.searching : canFind; 62 return attribs.canFind!(a => a.atAttribute !is null && 63 a.atAttribute.identifier.text == "disable"); 64 } 65 66 const isDeclationDisabled = containsDisable(declaration.attributes) || 67 containsDisable(declaration.functionDeclaration.memberFunctionAttributes); 68 69 if (isDeclationDisabled) 70 continue; 71 72 // Check if opEquals or toHash 73 immutable string methodName = declaration.functionDeclaration.name.text; 74 if (methodName == "opEquals") 75 hasOpEquals = true; 76 else if (methodName == "toHash") 77 hasToHash = true; 78 else if (methodName == "opCmp") 79 hasOpCmp = true; 80 } 81 82 // Warn if has opEquals, but not toHash 83 if (hasOpEquals && !hasToHash) 84 { 85 string message = "'" ~ name.text ~ "' has method 'opEquals', but not 'toHash'."; 86 addErrorMessage(name.line, name.column, KEY, message); 87 } 88 // Warn if has toHash, but not opEquals 89 else if (!hasOpEquals && hasToHash) 90 { 91 string message = "'" ~ name.text ~ "' has method 'toHash', but not 'opEquals'."; 92 addErrorMessage(name.line, name.column, KEY, message); 93 } 94 } 95 96 enum string KEY = "dscanner.suspicious.incomplete_operator_overloading"; 97 } 98 99 unittest 100 { 101 import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 102 103 StaticAnalysisConfig sac = disabledConfig(); 104 sac.opequals_tohash_check = Check.enabled; 105 assertAnalyzerWarnings(q{ 106 // Success because it has opEquals and toHash 107 class Chimp 108 { 109 const bool opEquals(Object a, Object b) 110 { 111 return true; 112 } 113 114 const override hash_t toHash() 115 { 116 return 0; 117 } 118 } 119 120 // AA would use default equal and default toHash 121 struct Bee 122 { 123 int opCmp(Bee) const 124 { 125 return true; 126 } 127 } 128 129 // Fail on class opEquals 130 class Rabbit // [warn]: 'Rabbit' 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 class toHash 139 class Kangaroo // [warn]: 'Kangaroo' has method 'toHash', but not 'opEquals'. 140 { 141 override const hash_t toHash() 142 { 143 return 0; 144 } 145 } 146 147 // Fail on struct opEquals 148 struct Tarantula // [warn]: 'Tarantula' has method 'opEquals', but not 'toHash'. 149 { 150 const bool opEquals(Object a, Object b) 151 { 152 return true; 153 } 154 } 155 156 // Fail on struct toHash 157 struct Puma // [warn]: 'Puma' has method 'toHash', but not 'opEquals'. 158 { 159 const nothrow @safe hash_t toHash() 160 { 161 return 0; 162 } 163 } 164 165 // issue #659, do not warn if one miss and the other is not callable 166 struct Fox {const nothrow @safe hash_t toHash() @disable;} 167 struct Bat {@disable const nothrow @safe hash_t toHash();} 168 struct Rat {const bool opEquals(Object a, Object b) @disable;} 169 struct Cat {@disable const bool opEquals(Object a, Object b);} 170 171 }}, sac); 172 173 stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed."); 174 }