1 module dsemver.compare; 2 3 import std.array : array, empty, front; 4 import std.algorithm.searching; 5 import std.algorithm.comparison : equal; 6 import std.typecons : nullable, Nullable; 7 import std.exception : enforce; 8 import std.format; 9 import std.stdio; 10 11 import dsemver.ast; 12 13 enum ResultValue { 14 equal, 15 minor, 16 major 17 } 18 19 ResultValue combine(const ResultValue on, const ResultValue no) 20 pure @safe 21 { 22 final switch(on) { 23 case ResultValue.equal: 24 final switch(no) { 25 case ResultValue.equal: return ResultValue.equal; 26 case ResultValue.minor: 27 throw new Exception(format("%s %s", on, no)); 28 case ResultValue.major: return ResultValue.minor; 29 } 30 case ResultValue.minor: assert(false); 31 case ResultValue.major: 32 final switch(no) { 33 case ResultValue.equal: return on; 34 case ResultValue.minor: 35 throw new Exception(format("%s %s", on, no)); 36 case ResultValue.major: return on; 37 } 38 } 39 } 40 41 struct Result { 42 ResultValue value; 43 string reason; 44 } 45 46 ResultValue summarize(const(Result)[] rslts) { 47 ResultValue rv; 48 foreach(ref r; rslts) { 49 if(r.value > rv) { 50 rv = r.value; 51 } 52 } 53 return rv; 54 } 55 56 Nullable!(const(Module)) findModule(ref const(Ast) toFindIn 57 , ref const(Module) mod) 58 { 59 auto f = mod.name.isNull() 60 ? toFindIn.modules.find!(m => m.file == mod.file) 61 : toFindIn.modules.find!(m => !m.name.isNull() 62 && m.name.get() == mod.name.get()); 63 64 return f.empty 65 ? Nullable!(const(Module)).init 66 : nullable(f.front); 67 } 68 69 Result[] compareOldNew(ref const(Ast) old, ref const(Ast) neu) { 70 Result[] ret; 71 foreach(ref mod; old.modules) { 72 Nullable!(const(Module)) fMod = findModule(neu, mod); 73 if(fMod.isNull()) { // Not found 74 ret ~= Result(ResultValue.major, format( 75 "module '%s' could no longer be found", mod.moduleToName())); 76 continue; 77 } else { // module name added 78 auto fModNN = fMod.get(); 79 if(!fModNN.name.isNull() && mod.name.isNull()) { 80 ret ~= Result(ResultValue.major, format( 81 "module '%s' added module name '%s'", mod.moduleToName() 82 , fModNN.name.get())); 83 continue; 84 } else { // recurse into module 85 foreach(ref mem; mod.members) { 86 // we can ignore private members 87 if(!mem.protection.isNull() 88 && mem.protection.get() == "private") 89 { 90 continue; 91 } 92 Nullable!(const(Member)) fm = findMember(fModNN, mem); 93 if(fm.isNull()) { 94 ret ~= Result(ResultValue.major, format( 95 "Ast Member '%s' of module '%s' couldn't be found" 96 , mem.name 97 , mod.moduleToName())); 98 continue; 99 } 100 const memsRslt = compareOldNew(mem, fm.get() 101 , [mod.moduleToName()]); 102 ret ~= memsRslt; 103 } 104 } 105 } 106 } 107 108 return ret; 109 } 110 111 Result[] compareOldNew(ref const(Member) old, ref const(Member) neu 112 , string[] path) 113 { 114 Result[] ret; 115 116 if(old.members.isNull()) { 117 return ret; 118 } 119 120 foreach(ref mem; old.members) { 121 Nullable!(const(Member)) f = neu.findMember(mem); 122 if(f.isNull()) { 123 ret ~= Result(ResultValue.major, format( 124 "%s of '%--(%s.%)' couldn't be found" 125 , mem.toString(), path)); 126 continue; 127 } 128 129 string[] np = path ~ mem.name; 130 if(mem.members.isNull()) { 131 continue; 132 } 133 foreach(ref const(Member) sub; mem.members) { 134 // we can ignore private members 135 if(!sub.protection.isNull() && sub.protection.get() == "private") { 136 continue; 137 } 138 Nullable!(const(Member)) fm = findMember(f.get(), sub); 139 if(fm.isNull()) { 140 ret ~= Result(ResultValue.major, format( 141 "%s of '%--(%s.%)' couldn't be found" 142 , sub.toString(), np)); 143 continue; 144 } 145 const memsRslt = compareOldNew(sub, fm.get(), np); 146 ret ~= memsRslt; 147 } 148 } 149 150 return ret; 151 } 152 153 private bool areEqualImpl(T)(ref const(T) a, ref const(T) b) { 154 import std.traits : isSomeString, isArray, FieldNameTuple, Unqual; 155 import std.range : ElementEncodingType; 156 157 static if(isSomeString!T) { 158 return a == b; 159 } else static if(isArray!T) { 160 if(a.length != b.length) { 161 return false; 162 } else { 163 alias ET = Unqual!(ElementEncodingType!T); 164 static if(is(ET == string)) { 165 return a.all!(i => canFind(b, i)); 166 } else { 167 return equal!((g,h) => areEqualImpl(g, h))(a, b); 168 } 169 } 170 } else static if(is(T == long)) { 171 return a == b; 172 } else static if(is(T == Nullable!F, F)) { 173 if(a.isNull() != b.isNull()) { 174 return false; 175 } else if(!a.isNull()) { 176 return areEqualImpl(a.get(), b.get()); 177 } else { 178 return true; 179 } 180 } else static if(is(T == struct)) { 181 static foreach(mem; FieldNameTuple!T) { 182 if(mem != "members" 183 && !areEqualImpl(__traits(getMember, a, mem) 184 , __traits(getMember, b, mem)) 185 ) 186 { 187 return false; 188 } 189 } 190 return true; 191 } else { 192 static assert(false, "Unhandled type " ~ T.stringof); 193 } 194 } 195 196 Nullable!(const(Member)) findMember(ref const(Member) toFindIn 197 , ref const(Member) mem) 198 { 199 import std.range : isForwardRange; 200 201 if(toFindIn.members.isNull()) { 202 return Nullable!(const(Member)).init; 203 } 204 205 auto n = toFindIn.members.get().find!(a => a.name == mem.name)().array; 206 auto f = n.find!(areEqualImpl)(mem); 207 return f.empty 208 ? Nullable!(const(Member)).init 209 : nullable(f.front); 210 } 211 212 Nullable!(const(Member)) findMember(ref const(Module) toFindIn 213 , ref const(Member) mem) 214 { 215 import std.range : isForwardRange; 216 217 static assert(isForwardRange!(typeof(cast()toFindIn.members))); 218 auto n = toFindIn.members.find!(a => a.name == mem.name)().array; 219 auto f = n.find!(areEqualImpl)(mem); 220 return f.empty 221 ? Nullable!(const(Member)).init 222 : nullable(f.front); 223 } 224 225 unittest { 226 import std.file : dirEntries, SpanMode, readText; 227 import std.algorithm.searching : canFind, startsWith, find; 228 import std.algorithm.iteration : splitter, map; 229 import std..string : strip; 230 231 struct ExpectedResult { 232 ResultValue old; 233 ResultValue neu; 234 ResultValue oldC; 235 ResultValue neuC; 236 } 237 238 ExpectedResult getExpected(string fn) { 239 import std.conv : to; 240 enum sstr = "// Result"; 241 string t = readText(fn); 242 auto s = t.splitter("\n").find!(l => l.startsWith(sstr)); 243 assert(!s.empty, fn); 244 245 string[] ws = s.front[sstr.length .. $].strip("()") 246 .splitter(",") 247 .map!(r => r.strip()) 248 .array; 249 250 enforce(ws.length == 4, fn); 251 return ExpectedResult 252 ( ws[0].to!ResultValue() 253 , ws[1].to!ResultValue() 254 , ws[2].to!ResultValue() 255 , ws[3].to!ResultValue() 256 ); 257 } 258 259 enum o = "old.d.json"; 260 foreach(fn; dirEntries("testdirgen/", "*old.d.json", SpanMode.depth)) { 261 auto a = parse(fn.name); 262 auto b = parse(fn.name[0 .. $ - o.length] ~ "new.d.json"); 263 264 string expected = fn.name[0 .. $ - 5]; 265 const ExpectedResult er = getExpected(expected); 266 267 auto cmpsON = compareOldNew(a, b); 268 auto sON = summarize(cmpsON); 269 assert(sON == er.old, format("\nwhich: old new\nfile: %s\ngot: %s\nexp: %s", fn.name 270 , sON, er.old)); 271 272 auto cmpsNO = compareOldNew(b, a); 273 auto sNO = summarize(cmpsNO); 274 assert(sNO == er.neu, format("\nwhich: neu old\nfile: %s\ngot: %s\nexp: %s", fn.name 275 , sNO, er.neu)); 276 277 auto compAB = combine(sON, sNO); 278 assert(compAB == er.oldC, format( 279 "\nwhich: old new comb\nfile: %s\ngot: %s\nexp: %s", fn.name 280 , compAB, er.oldC)); 281 282 auto compBA = combine(sNO, sON); 283 assert(compBA == er.neuC, format( 284 "\nwhich: new old comb\nfile: %s\ngot: %s\nexp: %s", fn.name 285 , compBA, er.neuC)); 286 } 287 }