using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; using UnitTestSharp; namespace Vellum.Generator.UnitTests { public class SaveMethodsTests : TestFixture { public const string Header = "// \r\n" + "// The generic methods Vellum.Generator found saving their type parameters, so their callers are " + "checked.\r\n"; /// /// Returns the attributes generated, one per line. /// public static string[] Lines(VellumRun run) { return run.SaveMethodsFile.Split(new[] { "\r\n" }, System.StringSplitOptions.RemoveEmptyEntries) .Where(line => !line.StartsWith("//", System.StringComparison.Ordinal)) .ToArray(); } public static string Attribute(string methodId, int typeParameter) { return "[assembly: global::Vellum.RegisterSaveMethod(\"" + methodId + "\", typeof(global::Vellum.SaveMethodParameter" + typeParameter + "))]"; } public const string Marker0 = SaveTemplateTests.Marker0; public const string Marker1 = "global::Vellum.SaveMethodParameter1"; public const string ToNodeId = "M:Vellum.SaveSession.ToNode``1(``0)"; /// /// Returns what each call in hands on, as "callers => callees : arguments". /// public static List Edges(string source) { CSharpCompilation compilation = VellumRun.Compile(("Source.cs", source)); SyntaxTree tree = compilation.SyntaxTrees[0]; SemanticModel model = compilation.GetSemanticModel(tree); var returnMe = new List(); foreach (InvocationExpressionSyntax call in tree.GetRoot().DescendantNodes() .OfType()) { foreach (SaveEdge edge in SaveMethods.EdgesOf(model, call, CancellationToken.None)) { string arguments = string.Join(", ", edge.Arguments.Items.Select( argument => argument == null ? "null" : argument.HasUnspellable ? "unspellable" : argument.ToSource())); returnMe.Add(string.Join("|", edge.CallerIds.Items) + " => " + string.Join("|", edge.CalleeIds.Items) + " : " + arguments); } } return returnMe; } public class MightHoldEdgesTests : TestFixture { /// /// Returns whether each method declared in might hold edges. /// public static List Might(string source) { SyntaxTree tree = CSharpSyntaxTree.ParseText(source); return tree.GetRoot().DescendantNodes().OfType() .Select(method => SaveMethods.MightHoldEdges(method, CancellationToken.None)) .ToList(); } public void GenericMethod_Might() { CheckEqual(new[] { true }, Might("class C { void M(T value) { Save(value); } }")); } public void PlainMethod_Mightnt() { CheckEqual(new[] { false }, Might("class C { void M(int value) { Save(value); } }")); } public void MethodOfAGenericClass_Might() { CheckEqual(new[] { true }, Might("class C { void M(T value) { Save(value); } }")); } public void MethodOfAGenericStructRecordOrInterface_Might() { CheckEqual(new[] { true }, Might("struct S { void M(T value) { Save(value); } }")); CheckEqual(new[] { true }, Might("record R { void M(T value) { Save(value); } }")); CheckEqual(new[] { true }, Might("interface I { void M(T value) { Save(value); } }")); } public void MethodOfATypeNestedInAGenericOne_Might() { CheckEqual(new[] { true }, Might("class Outer { class Inner { void M(int value) { Save(value); } } }")); } public void MethodOfAGenericTypeNestedInAPlainOne_Might() { CheckEqual(new[] { true }, Might("class Outer { class Inner { void M(T value) { Save(value); } } }")); } public void MethodInANamespaceOfPlainTypes_Mightnt() { CheckEqual(new[] { false }, Might("namespace N { class Outer { class Inner { void M() { Save(1); } } } }")); } public void MethodNamingNothing_Mightnt() { CheckEqual(new[] { false }, Might("class C { void M(T first, U second) { } }")); CheckEqual(new[] { false }, Might("class C { int M(T first) => 1; }")); CheckEqual(new[] { false }, Might("abstract class C { public abstract void M(T first); }")); } public void MethodNamingAProperty_Might() { CheckEqual(new[] { true }, Might("class C { int M(C other) { return other.Count; } }")); CheckEqual(new[] { true }, Might("class C { int M(C other) => other[0]; }")); CheckEqual(new[] { true }, Might("class C { int M(C other) => other?[0] ?? 0; }")); CheckEqual(new[] { true }, Might("class C { int M() => this[0]; }")); } public void CallInALambdaOrLocalFunction_Counts() { CheckEqual(new[] { true }, Might("class C { void M(T value) { System.Action act = () => Save(value); } }")); CheckEqual(new[] { true }, Might("class C { void M(T value) { void L() { Save(value); } } }")); } public void ExpressionBodiedCall_Counts() { CheckEqual(new[] { true }, Might("class C { int M(T value) => Save(value); }")); } public void LocalFunction_Mightnt() { CSharpSyntaxNode local = CSharpSyntaxTree.ParseText("class C { void M() { void L() { } } }") .GetRoot().DescendantNodes().OfType().Single(); CheckFalse(SaveMethods.MightHoldEdges(local, CancellationToken.None)); } public void NodeThatIsNotAMethod_Mightnt() { CSharpSyntaxNode node = CSharpSyntaxTree.ParseText("class C { int field; C() { } }").GetRoot() .DescendantNodes().OfType().Single(); CheckFalse(SaveMethods.MightHoldEdges(node, CancellationToken.None)); CheckFalse(SaveMethods.MightHoldEdges(node.Parent, CancellationToken.None)); } /// /// Returns whether each accessor, and each expression-bodied property or indexer, in /// might hold edges. /// public static List MightAccessors(string source) { return CSharpSyntaxTree.ParseText(source).GetRoot().DescendantNodes() .Where(node => node is AccessorDeclarationSyntax || node is ArrowExpressionClauseSyntax) .Select(node => SaveMethods.MightHoldEdges(node, CancellationToken.None)) .ToList(); } public void AccessorOfAGenericClass_Might() { CheckEqual(new[] { true, true }, MightAccessors( "class C { T P { get { return Save(1); } set { Save(value); } } }")); CheckEqual(new[] { true, true }, MightAccessors( "class C { T this[int i] { get { return Save(i); } set { Save(value); } } }")); CheckEqual(new[] { true, true }, MightAccessors( "class C { event System.Action E { add { Save(1); } remove { Save(1); } } }")); } public void ExpressionBodiedPropertyOrIndexerOfAGenericClass_Might() { CheckEqual(new[] { true }, MightAccessors("class C { int P => Save(1); }")); CheckEqual(new[] { true }, MightAccessors("class C { int this[int i] => Save(i); }")); } public void AccessorOfAPlainClass_Mightnt() { CheckEqual(new[] { false, false }, MightAccessors( "class C { int P { get { return Save(1); } set { Save(value); } } }")); CheckEqual(new[] { false }, MightAccessors("class C { int P => Save(1); }")); } public void AccessorWithNoBody_Mightnt() { CheckEqual(new[] { false, false }, MightAccessors("class C { T P { get; set; } }")); CheckEqual(new[] { false }, MightAccessors("interface I { T P { get; } }")); } public void AccessorNamingNothing_Mightnt() { CheckEqual(new[] { false }, MightAccessors("class C { int P { get { return 1; } } }")); CheckEqual(new[] { false }, MightAccessors("class C { int P => 1; }")); } public void ExpressionBodyOfAnAccessor_BelongsToTheAccessor() { CheckEqual(new[] { true, false }, MightAccessors("class C { int P { get => Save(1); } }")); } public void ExpressionBodyOfAMethod_IsNotAnAccessor() { CheckEqual(new[] { false }, MightAccessors("class C { int M() => Save(1); }")); } } public class EdgesOfTests : TestFixture { public const string SaverPrefix = "public class Store { public SaveSession Session; "; public void TypedToNodeOfTheParameter_HandsItOn() { CheckEqual(new[] { "M:Store.Put``1(``0) => " + ToNodeId + " : " + Marker0 }, Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(value); } }")); } public void TypeArgumentSpelled_HandsItOn() { CheckEqual(new[] { "M:Store.Put``1(``0) => " + ToNodeId + " : " + Marker0 }, Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(value); } }")); } public void TypeofOfTheParameter_HandsItOn() { CheckEqual(new[] { "M:Store.Put``1(``0) => M:Vellum.SaveSession.ToNode(System.Object,System.Type) : " + Marker0, }, Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(value, typeof(T)); } }")); } public void StaticCallee_HasNoReceiver() { CheckEqual(new[] { "M:Store.Put``1(``0) => M:Vellum.SavedValue.ToNode(System.Object,System.Type) : " + Marker0, }, Edges("public class Store { public void Put(T value) { " + "SavedValue.ToNode(value, typeof(T)); } }")); } public void SecondTypeParameter_IsItsOwnStandIn() { CheckEqual(new[] { "M:Store.Pair``2(``0,``1) => " + ToNodeId + " : " + Marker1 }, Edges(SaverPrefix + "public void Pair(TKey key, T value) { Session.ToNode(value); } }")); } public void ParameterInsideAGenericType_IsKeptInside() { CheckEqual(new[] { "M:Store.Put``1(System.Collections.Generic.List{``0}) => " + ToNodeId + " : global::System.Collections.Generic.List<" + Marker0 + ">", }, Edges(SaverPrefix + "public void Put(List value) { Session.ToNode(value); } }")); } public void ParameterInsideArrays_IsKeptInside() { CheckEqual(new[] { "M:Store.Put``1(``0[0:,0:][]) => " + ToNodeId + " : " + Marker0 + "[][,]", }, Edges(SaverPrefix + "public void Put(T[][,] value) { Session.ToNode(value); } }")); } public void ParameterNextToAClosedType_KeepsTheClosedType() { CheckEqual(new[] { "M:Store.Put``1(System.Collections.Generic.Dictionary{``0,System.String}) => " + ToNodeId + " : global::System.Collections.Generic.Dictionary<" + Marker0 + ", global::System.String>", }, Edges(SaverPrefix + "public void Put(Dictionary value) { Session.ToNode(value); } }")); } public void ParameterInsideATypeNestedInAGenericOne_IsKeptInside() { CheckEqual(new[] { "M:Store.Put``1(Outer{System.Int32}.Inner{``0}) => " + ToNodeId + " : global::Outer.Inner<" + Marker0 + ">", }, Edges("public class Outer { public class Inner { } }\n" + SaverPrefix + "public void Put(Outer.Inner value) { Session.ToNode(value); } }")); } public void CalleeGivenTwoTypeArguments_HandsOnEach() { CheckEqual(new[] { "M:Store.Put``1(``0) => M:Saver.Pair``2(``0,``1) : global::System.Int32, " + Marker0, }, Edges("public class Saver { public void Pair(A a, B b) { } }\n" + "public class Store { public Saver Saver; " + "public void Put(T value) { Saver.Pair(5, value); } }")); } public void ClosedTypesOnly_HandOnNothing() { CheckEqual(0, Edges(SaverPrefix + "public void Put(T value) { Session.ToNode(5); " + "Session.ToNode(\"text\"); } }").Count); } public void CallOutsideAGenericMethod_HandsOnNothing() { CheckEqual(0, Edges(SaverPrefix + "public void Put(int value) { Session.ToNode(value); } }").Count); } public const string ClassMarker0 = "global::Vellum.SaveClassParameter"; public const string ClassMarker1 = "global::Vellum.SaveClassParameter"; public void ParameterOfTheClass_IsHandedOn() { CheckEqual(new[] { "M:Store`1.Put``1(`0) => " + ToNodeId + " : " + ClassMarker0 }, Edges("public class Store { public SaveSession Session; " + "public void Put(TValue value) { Session.ToNode(value); } }")); } public void ParameterOfAnOuterClass_IsNumberedBeforeThoseOfTheInnerOne() { CheckEqual(new[] { "M:Outer`1.Inner`1.Keep(`0) => " + ToNodeId + " : " + ClassMarker0, "M:Outer`1.Inner`1.Put(`1) => " + ToNodeId + " : " + ClassMarker1, }, Edges("public class Outer { public class Inner { public SaveSession Session; " + "public void Keep(A value) { Session.ToNode(value); } " + "public void Put(B value) { Session.ToNode(value); } } }")); } public void ParameterOfAnInnerClassOfAPlainOne_IsNumberedFromZero() { CheckEqual(new[] { "M:Outer.Inner`1.Put(`0) => " + ToNodeId + " : " + ClassMarker0 }, Edges("public class Outer { public class Inner { public SaveSession Session; " + "public void Put(B value) { Session.ToNode(value); } } }")); } public void ClassAndMethodParametersTogether_AreEachSpelledByTheirOwnStandIn() { CheckEqual(new[] { "M:Store`1.Put``1(`0,``0) => " + ToNodeId + " : global::System.Collections.Generic.Dictionary<" + ClassMarker0 + ", " + Marker0 + ">", }, Edges("public class Store { public SaveSession Session; " + "public void Put(TValue key, T value) { " + "Session.ToNode(new Dictionary()); } }")); } public void ParameterOfTheClassInsideAType_IsKeptInside() { CheckEqual(new[] { "M:Store`1.Put(System.Collections.Generic.List{`0}) => " + ToNodeId + " : global::System.Collections.Generic.List<" + ClassMarker0 + ">", }, Edges("public class Store { public SaveSession Session; " + "public void Put(List value) { Session.ToNode(value); } }")); } public void ParameterOfTheClassNextToAnUnspellableType_IsUnspellable() { CheckEqual(new[] { "M:Store`1.Put(`0) => " + ToNodeId + " : unspellable" }, Edges("public class Store { private class Hidden { } public SaveSession Session; " + "public void Put(TValue value) { Session.ToNode(new Dictionary { public class Inner : IBag { public SaveSession Session; public void Put(B value) { Session.ToNode(value); } } }"); CheckEqual(new[] { ClassAttribute("M:IBag`1.Put(`0)", ClassStandIn0), ClassAttribute("M:Outer`1.Inner`1.Put(`1)", ClassStandIn1), }, Lines(run)); } public void InterfaceThatTakesTheSameParameterTwice_IsRegisteredWithItsFirstNumber() { VellumRun run = VellumRun.Of(@" public interface IBag { void Put(X value); } public class Twin : IBag { public SaveSession Session; public void Put(A value) { Session.ToNode(value); } }"); CheckEqual(new[] { ClassAttribute("M:IBag`2.Put(`0)", ClassStandIn0), ClassAttribute("M:Twin`1.Put(`0)", ClassStandIn0), }, Lines(run)); } public void InterfaceThatTakesTheParameterInsideAnotherType_IsNotRegisteredAndWarns() { VellumRun run = VellumRun.Of(@" public interface IBag { void Put(X value); } public class Wrap : IBag> { public SaveSession Session; public void Put(List value) { Session.ToNode(value); } }"); CheckEqual(new[] { "[assembly: global::Vellum.RegisterSaveMethod(" + "\"M:Wrap`1.Put(System.Collections.Generic.List{`0})\", " + "typeof(global::System.Collections.Generic.List>))]", }, Lines(run)); CheckEqual(new[] { "VEL0016" }, run.Reported); } public void MethodImplementingAnInterfaceExplicitly_IsRegisteredUnderTheInterfaceOnly() { VellumRun run = VellumRun.Of(@" public interface IBag { void Put(X value); } public class Pair : IBag { public SaveSession Session; void IBag.Put(B value) { Session.ToNode(value); } }"); CheckEqual(new[] { ClassAttribute("M:IBag`1.Put(`0)", ClassStandIn0) }, Lines(run)); } public void CallThroughTheInterface_IsCheckedWithTheInterfacesArgument() { MetadataReference library = VellumRun.CompileLibrary("Saver", @" public interface IBag { void Put(string key, X value); } public class Pair : IBag { public SaveSession Session; public void Put(string key, B value) { Session.ToNode(value); } }"); VellumRun run = VellumRun.Of(library, @" public class User { public void Run(IBag> bad, IBag good, Pair fine, Pair alsoBad) { bad.Put(""bag"", null); good.Put(""fine"", 1); fine.Put(""fine"", 1); alsoBad.Put(""other"", null); } }"); CheckEqual(new[] { "VEL0004:bad.Put(\"bag\", null)", "VEL0004:alsoBad.Put(\"other\", null)" }, run.Compiled); } public void MethodThatCallsAMemberOfAGenericClass_IsRegisteredByTheMethodsParameter() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; public void Put(string key, T value) { Session.ToNode(value); } } public class Front { public void Save(U value) { new Store().Put(""k"", value); } }"); CheckEqual(new[] { Attribute("M:Front.Save``1(``0)", 0), ClassAttribute("M:Store`1.Put(System.String,`0)", ClassStandIn0), }, Lines(run)); } public void MethodOfAGenericClassThatCallsAMemberOfAnother_IsRegisteredByItsClassParameter() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; public void Put(string key, T value) { Session.ToNode(value); } } public class Holder { public Store Inner = new Store(); public void Hold(U value) { Inner.Put(""k"", value); } }"); CheckEqual(new[] { ClassAttribute("M:Holder`1.Hold(`0)", ClassStandIn0), ClassAttribute("M:Store`1.Put(System.String,`0)", ClassStandIn0), }, Lines(run)); } public void ClassArgumentBuiltFromTheCallersParameter_IsSpelledInTheTemplate() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; public void Put(string key, T value) { Session.ToNode(value); } } public class Holder { public Store> Inner = new Store>(); public void Hold(U value) { Inner.Put(""k"", null); } }"); CheckEqual(new[] { "[assembly: global::Vellum.RegisterSaveMethod(\"M:Holder`1.Hold(`0)\", typeof(global::System." + "Collections.Generic.List>))]", ClassAttribute("M:Store`1.Put(System.String,`0)", ClassStandIn0), }, Lines(run)); } public void StaticMemberOfAGenericClass_IsHandedOnToo() { VellumRun run = VellumRun.Of(@" public class Store { public static SaveSession Session; public static void Make(T value) { Session.ToNode(value); } } public class Front { public void Use(U value) { Store.Make(value); } }"); CheckEqual(new[] { Attribute("M:Front.Use``1(``0)", 0), ClassAttribute("M:Store`1.Make(`0)", ClassStandIn0), }, Lines(run)); } public void MethodThatCallsAnotherOfItsOwnClass_IsRegisteredToo() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; void Keep(T value) { Session.ToNode(value); } public void Put(T value) { Keep(value); } }"); CheckEqual(new[] { ClassAttribute("M:Store`1.Keep(`0)", ClassStandIn0), ClassAttribute("M:Store`1.Put(`0)", ClassStandIn0), }, Lines(run)); } public void CallThroughAChainOfGenericClasses_IsCheckedForSaveableTypes() { MetadataReference library = VellumRun.CompileLibrary("Saver", @" public class Store { public SaveSession Session; public void Put(string key, T value) { Session.ToNode(value); } } public class Holder { public Store Inner = new Store(); public void Hold(string key, U value) { Inner.Put(key, value); } }"); VellumRun run = VellumRun.Of(library, @" public class User { public void Run(Holder> bad, Holder good) { bad.Hold(""bag"", null); good.Hold(""fine"", 1); } }"); CheckEqual(new[] { "VEL0004:bad.Hold(\"bag\", null)" }, run.Compiled); } public void ClassArgumentOfACallThatIsClosed_HandsOnNothing() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; public void Put(T value) { Session.ToNode(value); } } public class Front { public void Save(U value) { new Store().Put(1); } }"); CheckEqual(new[] { ClassAttribute("M:Store`1.Put(`0)", ClassStandIn0) }, Lines(run)); } public void MethodParameterTemplate_IsStillRegisteredUnderAnInterfaceOfAGenericClass() { VellumRun run = VellumRun.Of(@" public interface IStore { void Put(T value); } public class Store : IStore { public SaveSession Session; public void Put(T value) { Session.ToNode(value); } }"); CheckEqual(new[] { Attribute("M:IStore`1.Put``1(``0)", 0), Attribute("M:Store`1.Put``1(``0)", 0), }, Lines(run)); } public void CallThroughAGenericClassesRegisteredMethod_IsCheckedForSaveableTypes() { MetadataReference library = VellumRun.CompileLibrary("Saver", @" public class Store { public SaveSession Session; public void Put(string key, T value) { Session.ToNode(value); } }"); VellumRun run = VellumRun.Of(library, @" public class User { public void Run(Store> bad, Store good) { bad.Put(""bag"", null); good.Put(""fine"", 1); } }"); CheckEqual(new[] { "VEL0004:bad.Put(\"bag\", null)" }, run.Compiled); } public void ClassAndMethodParametersTogether_AreBothCheckedAtTheCall() { MetadataReference library = VellumRun.CompileLibrary("Saver", @" public class Store { public SaveSession Session; public void Put(string key, U value) { Session.ToNode(new Dictionary()); } }"); VellumRun run = VellumRun.Of(library, @" public class User { public void Run(Store ints, Store objects) { ints.Put(""fine"", 1); ints.Put(""second"", new object()); objects.Put(""first"", 1); } }"); CheckEqual(new[] { "VEL0004:ints.Put(\"second\", new object())", "VEL0004:objects.Put(\"first\", 1)" }, run.Compiled); } public void MethodsThatCallEachOther_AreRegistered() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; public void A(T value, int count) { if (count > 0) { B(value, count - 1); } Session.ToNode(value); } public void B(T value, int count) { A(value, count); } }"); CheckEqual(0, run.Compiled.Count); CheckEqual(new[] { Attribute("M:Store.A``1(``0,System.Int32)", 0), Attribute("M:Store.B``1(``0,System.Int32)", 0), }, Lines(run)); } public void WrapperSavingATypeBuiltFromItsParameter_SpellsThatType() { VellumRun run = VellumRun.Of(Store + @" public class Front { public Store Store; public void Save(T value) { Store.Put(new List()); } }"); CheckEqual(new[] { "[assembly: global::Vellum.RegisterSaveMethod(\"M:Front.Save``1(``0)\", typeof(global::System." + "Collections.Generic.List))]", Attribute("M:Store.Put``1(``0)", 0), }, Lines(run)); } public void WrapperSavingItsSecondParameter_NamesTheSecondStandIn() { VellumRun run = VellumRun.Of(Store + @" public class Front { public Store Store; public void Save(TKey key, T value) { Store.Put(value); } }"); CheckEqual(new[] { Attribute("M:Front.Save``2(``0,``1)", 1), Attribute("M:Store.Put``1(``0)", 0), }, Lines(run)); } public void MethodThatDoesntSave_IsNotRegistered() { VellumRun run = VellumRun.Of(Store + @" public class Front { public Store Store; public void Note(T value) { Other(value); } void Other(T value) { } }"); CheckEqual(new[] { Attribute("M:Store.Put``1(``0)", 0) }, Lines(run)); } public void RequireOfTheParameter_IsRegistered() { VellumRun run = VellumRun.Of(@" public class Store { public T Load(SaveReader reader) { return reader.Require(""key""); } }"); CheckEqual(new[] { Attribute("M:Store.Load``1(Vellum.SaveReader)", 0) }, Lines(run)); } public void TryReadOfTheParameter_IsRegistered() { VellumRun run = VellumRun.Of(@" public class Store { public bool Load(SaveReader reader, out T value) { return reader.TryRead(""key"", out value); } }"); CheckEqual(new[] { Attribute("M:Store.Load``1(Vellum.SaveReader,``0@)", 0) }, Lines(run)); } public void TypeofFromNode_IsRegistered() { VellumRun run = VellumRun.Of(@" public class Store { public SaveSession Session; public T Read(SaveNode node) { return (T)Session.FromNode(node, typeof(T)); } }"); CheckEqual(new[] { Attribute("M:Store.Read``1(Vellum.SaveNode)", 0) }, Lines(run)); } public void SavedItemsOfTheParameter_AreRegistered() { VellumRun run = VellumRun.Of(@" public class Store { public SaveNode Put(IEnumerable items, SaveSession session) { return SavedItems.ToNode(items, session); } public T[] Get(SaveNode node, SaveSession session) { return SavedItems.FromNode(node, session); } }"); CheckEqual(new[] { Attribute("M:Store.Get``1(Vellum.SaveNode,Vellum.SaveSession)", 0), Attribute("M:Store.Put``1(System.Collections.Generic.IEnumerable{``0},Vellum.SaveSession)", 0), }, Lines(run)); } public void SavedEntriesOfTheParameters_RegisterBoth() { VellumRun run = VellumRun.Of(@" public class Store { public SaveNode Put(IEnumerable> entries, SaveSession session) { return SavedEntries.ToNode(entries, session); } }"); string id = "M:Store.Put``2(System.Collections.Generic.IEnumerable" + "{System.Collections.Generic.KeyValuePair{``0,``1}},Vellum.SaveSession)"; CheckEqual(new[] { Attribute(id, 0), Attribute(id, 1) }, Lines(run)); } public void SavedEntriesWithOneParameterClosed_RegistersTheOpenOne() { VellumRun run = VellumRun.Of(@" public class Store { public SaveNode Put(IEnumerable> entries, SaveSession session) { return SavedEntries.ToNode(entries, session); } }"); CheckEqual(new[] { Attribute("M:Store.Put``1(System.Collections.Generic.IEnumerable" + "{System.Collections.Generic.KeyValuePair{System.String,``0}},Vellum.SaveSession)", 0), }, Lines(run)); } public void WrapperThroughAnInterface_IsRegistered() { VellumRun run = VellumRun.Of(@" public interface IStore { void Put(T value); } public class Store : IStore { public SaveSession Session; public void Put(T value) { Session.ToNode(value); } } public class Front { public IStore Store; public void Save(T value) { Store.Put(value); } }"); CheckEqual(new[] { Attribute("M:Front.Save``1(``0)", 0), Attribute("M:IStore.Put``1(``0)", 0), Attribute("M:Store.Put``1(``0)", 0), }, Lines(run)); } public void WrapperInAReference_IsRegisteredForItsCallers() { MetadataReference library = VellumRun.CompileLibrary("Saver", Store + "public class Front { public Store Store; public void Save(T value) { Store.Put(value); } }"); VellumRun run = VellumRun.Of(library, @" public class Back { public Front Front; public void Keep(T value) { Front.Save(value); } }"); CheckEqual(new[] { Attribute("M:Back.Keep``1(``0)", 0) }, Lines(run)); } public void CallThroughAWrapper_IsCheckedForSaveableTypes() { MetadataReference library = VellumRun.CompileLibrary("Saver", Store + "public class Front { public Store Store; public void Save(T value) { Store.Put(value); } }"); VellumRun run = VellumRun.Of(library, "public class User { public void Run(Front front, " + "System.Collections.ObjectModel.Collection bag) { front.Save(bag); } }"); CheckEqual(new[] { "VEL0004:front.Save(bag)" }, run.Compiled); } public void CallThroughAWrapperOfAGenericType_IsCheckedInsideThatType() { MetadataReference library = VellumRun.CompileLibrary("Saver", Store + "public class Front { public Store Store; " + "public void Save(T value) { Store.Put(new List()); } }"); VellumRun run = VellumRun.Of(library, "public class User { public void Run(Front front) " + "{ front.Save(5); front.Save(new object()); } }"); CheckEqual(new[] { "VEL0004:front.Save(new object())" }, run.Compiled); } public void CallOnAnOverrideInAPlainClass_IsCheckedByTheBaseClassesRegistration() { MetadataReference library = VellumRun.CompileLibrary("Saver", @" public class Base { public SaveSession Session; public virtual void Put(T value) { Session.ToNode(value); } }"); VellumRun run = VellumRun.Of(library, "public class Derived : Base> { public override void Put(System.Collections.ObjectModel.Collection " + "value) { } }\npublic class User { public void Run(Derived derived, " + "System.Collections.ObjectModel.Collection bag) { derived.Put(bag); } }"); CheckEqual(new[] { "VEL0004:derived.Put(bag)" }, run.Compiled); } public void WrapperSavingAPrivateTypeAroundItsParameter_WarnsAndIsNotRegistered() { VellumRun run = VellumRun.Of(Store + @" public class Front { private class Hidden { } public Store Store; public void Save(T value) { Store.Put(new Dictionary