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 Member[] emptyArray; 121 foreach(ref mem; old.members.get(emptyArray)) { 122 Nullable!(const(Member)) f = neu.findMember(mem); 123 if(f.isNull()) { 124 ret ~= Result(ResultValue.major, format( 125 "%s of '%--(%s.%)' couldn't be found" 126 , mem.toString(), path)); 127 continue; 128 } 129 130 string[] np = path ~ mem.name; 131 if(mem.members.isNull()) { 132 continue; 133 } 134 foreach(ref const(Member) sub; mem.members.get(emptyArray)) { 135 // we can ignore private members 136 if(!sub.protection.isNull() && sub.protection.get() == "private") { 137 continue; 138 } 139 Nullable!(const(Member)) fm = findMember(f.get(), sub); 140 if(fm.isNull()) { 141 ret ~= Result(ResultValue.major, format( 142 "%s of '%--(%s.%)' couldn't be found" 143 , sub.toString(), np)); 144 continue; 145 } 146 const memsRslt = compareOldNew(sub, fm.get(), np); 147 ret ~= memsRslt; 148 } 149 } 150 151 return ret; 152 } 153 154 private bool areEqualImpl(T)(ref const(T) a, ref const(T) b) { 155 import std.traits : isSomeString, isArray, FieldNameTuple, Unqual; 156 import std.range : ElementEncodingType; 157 158 static if(isSomeString!T) { 159 return a == b; 160 } else static if(isArray!T) { 161 if(a.length != b.length) { 162 return false; 163 } else { 164 alias ET = Unqual!(ElementEncodingType!T); 165 static if(is(ET == string)) { 166 return a.all!(i => canFind(b, i)); 167 } else { 168 return equal!((g,h) => areEqualImpl(g, h))(a, b); 169 } 170 } 171 } else static if(is(T == long)) { 172 return a == b; 173 } else static if(is(T == Nullable!F, F)) { 174 if(a.isNull() != b.isNull()) { 175 return false; 176 } else if(!a.isNull()) { 177 return areEqualImpl(a.get(), b.get()); 178 } else { 179 return true; 180 } 181 } else static if(is(T == struct)) { 182 static foreach(mem; FieldNameTuple!T) { 183 if(mem != "members" 184 && !areEqualImpl(__traits(getMember, a, mem) 185 , __traits(getMember, b, mem)) 186 ) 187 { 188 return false; 189 } 190 } 191 return true; 192 } else { 193 static assert(false, "Unhandled type " ~ T.stringof); 194 } 195 } 196 197 Nullable!(const(Member)) findMember(ref const(Member) toFindIn 198 , ref const(Member) mem) 199 { 200 import std.range : isForwardRange; 201 202 if(toFindIn.members.isNull()) { 203 return Nullable!(const(Member)).init; 204 } 205 206 auto n = toFindIn.members.get().find!(a => a.name == mem.name)().array; 207 auto f = n.find!(areEqualImpl)(mem); 208 return f.empty 209 ? Nullable!(const(Member)).init 210 : nullable(f.front); 211 } 212 213 Nullable!(const(Member)) findMember(ref const(Module) toFindIn 214 , ref const(Member) mem) 215 { 216 import std.range : isForwardRange; 217 218 static assert(isForwardRange!(typeof(cast()toFindIn.members))); 219 auto n = toFindIn.members.find!(a => a.name == mem.name)().array; 220 auto f = n.find!(areEqualImpl)(mem); 221 return f.empty 222 ? Nullable!(const(Member)).init 223 : nullable(f.front); 224 } 225 226 unittest { 227 import std.file : dirEntries, SpanMode, readText; 228 import std.algorithm.searching : canFind, startsWith, find; 229 import std.algorithm.iteration : splitter, map; 230 import std.string : strip; 231 232 struct ExpectedResult { 233 ResultValue old; 234 ResultValue neu; 235 ResultValue oldC; 236 ResultValue neuC; 237 } 238 239 ExpectedResult getExpected(string fn) { 240 import std.conv : to; 241 enum sstr = "// Result"; 242 string t = readText(fn); 243 auto s = t.splitter("\n").find!(l => l.startsWith(sstr)); 244 assert(!s.empty, fn); 245 246 string[] ws = s.front[sstr.length .. $].strip("()") 247 .splitter(",") 248 .map!(r => r.strip()) 249 .array; 250 251 enforce(ws.length == 4, fn); 252 return ExpectedResult 253 ( ws[0].to!ResultValue() 254 , ws[1].to!ResultValue() 255 , ws[2].to!ResultValue() 256 , ws[3].to!ResultValue() 257 ); 258 } 259 260 enum o = "old.d.json"; 261 foreach(fn; dirEntries("testdirgen/", "*old.d.json", SpanMode.depth)) { 262 auto a = parse(fn.name); 263 auto b = parse(fn.name[0 .. $ - o.length] ~ "new.d.json"); 264 265 string expected = fn.name[0 .. $ - 5]; 266 const ExpectedResult er = getExpected(expected); 267 268 auto cmpsON = compareOldNew(a, b); 269 auto sON = summarize(cmpsON); 270 assert(sON == er.old, format("\nwhich: old new\nfile: %s\ngot: %s\nexp: %s", fn.name 271 , sON, er.old)); 272 273 auto cmpsNO = compareOldNew(b, a); 274 auto sNO = summarize(cmpsNO); 275 assert(sNO == er.neu, format("\nwhich: neu old\nfile: %s\ngot: %s\nexp: %s", fn.name 276 , sNO, er.neu)); 277 278 auto compAB = combine(sON, sNO); 279 assert(compAB == er.oldC, format( 280 "\nwhich: old new comb\nfile: %s\ngot: %s\nexp: %s", fn.name 281 , compAB, er.oldC)); 282 283 auto compBA = combine(sNO, sON); 284 assert(compBA == er.neuC, format( 285 "\nwhich: new old comb\nfile: %s\ngot: %s\nexp: %s", fn.name 286 , compBA, er.neuC)); 287 } 288 }