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 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 // 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 81 enum string KEY = "dscanner.suspicious.incomplete_operator_overloading"; 82 } 83 84 unittest 85 { 86 import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 87 88 StaticAnalysisConfig sac = disabledConfig(); 89 sac.opequals_tohash_check = Check.enabled; 90 assertAnalyzerWarnings(q{ 91 // Success because it has opEquals and toHash 92 class Chimp 93 { 94 const bool opEquals(Object a, Object b) 95 { 96 return true; 97 } 98 99 const override hash_t toHash() 100 { 101 return 0; 102 } 103 } 104 105 // AA would use default equal and default toHash 106 struct Bee 107 { 108 int opCmp(Bee) const 109 { 110 return true; 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 }c, sac); 150 151 stderr.writeln("Unittest for OpEqualsWithoutToHashCheck passed."); 152 }