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 }