using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using UnitTestSharp; namespace Vellum.Generator.UnitTests { public class SavedTypeAnalyzerTests : TestFixture { /// /// Holds a store with some generic methods registered as saving a type argument, and some not. /// public const string Store = "[assembly: RegisterSaveMethod(\"M:IStore.Put``1(System.String,``0)\", typeof(SaveMethodParameter0))]\n" + "[assembly: RegisterSaveMethod(\"M:IStore.Make``1\", typeof(SaveMethodParameter0))]\n" + "[assembly: RegisterSaveMethod(\"M:IStore.Pair``2(``0,``1)\", typeof(SaveMethodParameter1))]\n" + "public interface IStore\n{\n" + " void Put(string key, T value);\n" + " T Make();\n" + " void Put(object value);\n" + " void Pair(TKey key, T value);\n" + " void Note(string text);\n" + "}\n"; public static readonly MetadataReference StoreLibrary = VellumRun.CompileLibrary("Store", Store); /// /// Returns VEL0004's message for holding a . /// public static string Unsaveable(string name, string type, string inside = "") { return "VEL0004:'" + name + "' is saved, but a " + type + inside + " can't be saved. Mark it " + "[Saveable], or write a SaveConverter<" + type + "> for it."; } public class RegisteredTests : TestFixture { /// /// Returns each registration in , as "methodId=savedType". /// public static List RegisteredIncludingVellums(MetadataReference[] libraries, string source) { return SavedTypeAnalyzer.Registered(VellumRun.Compile(libraries, ("Source.cs", source))) .SelectMany(pair => pair.Value.Select(saved => pair.Key + "=" + saved.ToDisplayString())) .ToList(); } public static List Registered(MetadataReference[] libraries, string source) { return RegisteredIncludingVellums(libraries, source) .Where(name => !name.StartsWith("M:Vellum.", System.StringComparison.Ordinal)) .ToList(); } public static List Registered(string source) { return Registered(new MetadataReference[0], source); } public void Vellum_RegistersItsOwnSavingAndLoadingCalls() { List registered = RegisteredIncludingVellums(new MetadataReference[0], "public class Plain { }"); CheckTrue(registered.Contains("M:Vellum.SaveSession.ToNode``1(``0)=Vellum.SaveMethodParameter0")); CheckTrue(registered.Contains( "M:Vellum.SaveSession.FromNode``1(Vellum.SaveNode)=Vellum.SaveMethodParameter0")); CheckTrue(registered.Contains( "M:Vellum.SaveReader.Require``1(System.String)=Vellum.SaveMethodParameter0")); } public void FindsRegistrationsInTheCompilationAndItsReferences() { CheckEqualCommutative(new[] { "M:IStore.Put``1(System.String,``0)=Vellum.SaveMethodParameter0", "M:IStore.Make``1=Vellum.SaveMethodParameter0", "M:IStore.Pair``2(``0,``1)=Vellum.SaveMethodParameter1", "M:IOther.Put``1(``0)=Vellum.SaveMethodParameter0", }, Registered(new[] { StoreLibrary }, "[assembly: RegisterSaveMethod(\"M:IOther.Put``1(``0)\", " + "typeof(SaveMethodParameter0))]\npublic interface IOther { }")); } public void SavedTypeBuiltFromTheStandIns_IsKeptWhole() { CheckEqual(new[] { "M:Bag.Put``2(``0,``1)=System.Collections.Generic.Dictionary", }, Registered("[assembly: RegisterSaveMethod(\"M:Bag.Put``2(``0,``1)\", " + "typeof(Dictionary))]\npublic class Bag { }")); } public void RegistrationOfNoMethodOrNoType_IsIgnored() { CheckEqual(0, Registered("[assembly: RegisterSaveMethod(null, typeof(SaveMethodParameter0))]\n" + "[assembly: RegisterSaveMethod(\"M:Plain.Put``1(``0)\", null)]\npublic class Plain { }").Count); } public void RegistrationThatDoesntCompile_IsIgnored() { CheckEqual(0, Registered("[assembly: RegisterSaveMethod(\"M:Plain.Put``1(``0)\")]\n" + "public class Plain { }").Count); } public void AttributeNothingDefines_IsIgnored() { CheckEqual(0, Registered("[assembly: Missing]\npublic class Plain { }").Count); } public void OtherAssemblyAttributes_AreIgnored() { CheckEqual(0, Registered( "[assembly: System.Reflection.AssemblyTitle(\"Plain\")]\npublic class Plain { }").Count); } } public class FillTests : TestFixture { public static CSharpCompilation Compilation() { return VellumRun.Compile(("Source.cs", "public class Outer { public class Inner { public class Deep { } } }")); } public static ITypeSymbol Digit(CSharpCompilation compilation, int digit) { return compilation.GetTypeByMetadataName("Vellum.SaveMethodParameter" + digit); } public static ImmutableArray Arguments(CSharpCompilation compilation, params SpecialType[] types) { return types.Select(type => (ITypeSymbol)compilation.GetSpecialType(type)).ToImmutableArray(); } public void StandIn_IsTheArgumentOfItsNumber() { CSharpCompilation compilation = Compilation(); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, Digit(compilation, 1), Arguments(compilation, SpecialType.System_Int32, SpecialType.System_String)); CheckEqual("string", filled.ToDisplayString()); } public void ClassStandIn_IsTheClassArgumentOfItsNumber() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol wrapper = compilation.GetTypeByMetadataName("Vellum.SaveClassParameter`1"); ImmutableArray classArguments = Arguments(compilation, SpecialType.System_Int32, SpecialType.System_String); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, wrapper.Construct(Digit(compilation, 1)), Arguments(compilation, SpecialType.System_Double), classArguments); CheckEqual("string", filled.ToDisplayString()); } public void ClassStandInWithoutAClassArgument_IsNull() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol wrapper = compilation.GetTypeByMetadataName("Vellum.SaveClassParameter`1"); CheckNull(SavedTypeAnalyzer.Fill(compilation, wrapper.Construct(Digit(compilation, 0)), Arguments(compilation, SpecialType.System_Int32))); } public void ClassAndMethodStandInsInOneType_AreFilledFromTheirOwnArguments() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol wrapper = compilation.GetTypeByMetadataName("Vellum.SaveClassParameter`1"); INamedTypeSymbol open = compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2"); INamedTypeSymbol template = open.Construct(wrapper.Construct(Digit(compilation, 0)), Digit(compilation, 0)); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, template, Arguments(compilation, SpecialType.System_String), Arguments(compilation, SpecialType.System_Int32)); CheckEqual("System.Collections.Generic.Dictionary", filled.ToDisplayString()); } public void ClassArgumentsOfANestedType_ListTheOuterTypesFirst() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol outer = compilation.GetTypeByMetadataName("Outer`1") .Construct(compilation.GetSpecialType(SpecialType.System_Int32)); INamedTypeSymbol inner = outer.GetTypeMembers("Inner").Single() .Construct(compilation.GetSpecialType(SpecialType.System_String)); CheckEqual(new[] { "int", "string" }, SavedTypeAnalyzer.ClassArgumentsOf(inner).Select(type => type.ToDisplayString()).ToArray()); } public void ClassArgumentsOfATypeWithNone_AreEmpty() { CSharpCompilation compilation = Compilation(); CheckEqual(0, SavedTypeAnalyzer.ClassArgumentsOf( compilation.GetSpecialType(SpecialType.System_String)).Length); } public void StandInWithoutAnArgument_IsNull() { CSharpCompilation compilation = Compilation(); CheckNull(SavedTypeAnalyzer.Fill(compilation, Digit(compilation, 2), Arguments(compilation, SpecialType.System_Int32))); } public void TypeWithoutStandIns_IsItself() { CSharpCompilation compilation = Compilation(); ITypeSymbol closed = compilation.GetSpecialType(SpecialType.System_String); CheckTrue(ReferenceEquals(closed, SavedTypeAnalyzer.Fill(compilation, closed, Arguments(compilation)))); } public void ArrayOfAStandIn_IsAnArrayOfTheArgument() { CSharpCompilation compilation = Compilation(); IArrayTypeSymbol template = compilation.CreateArrayTypeSymbol(Digit(compilation, 0), rank: 2); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, template, Arguments(compilation, SpecialType.System_Int32)); CheckEqual("int[*,*]", filled.ToDisplayString()); } public void GenericOfStandIns_IsMadeFromTheArguments() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol open = compilation.GetTypeByMetadataName("System.Collections.Generic.Dictionary`2"); INamedTypeSymbol dictionary = open.Construct(Digit(compilation, 0), Digit(compilation, 1)); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, dictionary, Arguments(compilation, SpecialType.System_Int32, SpecialType.System_String)); CheckEqual("System.Collections.Generic.Dictionary", filled.ToDisplayString()); } public void TypeNestedInAGenericOne_KeepsTheOuterTypesArgument() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol outer = compilation.GetTypeByMetadataName("Outer`1").Construct(Digit(compilation, 0)); INamedTypeSymbol inner = outer.GetTypeMembers("Inner").Single().Construct(Digit(compilation, 1)); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, inner, Arguments(compilation, SpecialType.System_Int32, SpecialType.System_String)); CheckEqual("Outer.Inner", filled.ToDisplayString()); } public void TypeNestedInAClosedGenericOne_KeepsTheOuterTypesArgument() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol outer = compilation.GetTypeByMetadataName("Outer`1") .Construct(compilation.GetSpecialType(SpecialType.System_Int32)); INamedTypeSymbol inner = outer.GetTypeMembers("Inner").Single().Construct(Digit(compilation, 0)); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, inner, Arguments(compilation, SpecialType.System_String)); CheckEqual("Outer.Inner", filled.ToDisplayString()); } public void NonGenericTypeNestedInAGenericOne_KeepsTheOuterTypesArguments() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol outer = compilation.GetTypeByMetadataName("Outer`1").Construct(Digit(compilation, 0)); INamedTypeSymbol inner = outer.GetTypeMembers("Inner").Single().Construct(Digit(compilation, 1)); INamedTypeSymbol deep = inner.GetTypeMembers("Deep").Single(); ITypeSymbol filled = SavedTypeAnalyzer.Fill(compilation, deep, Arguments(compilation, SpecialType.System_Int32, SpecialType.System_String)); CheckEqual("Outer.Inner.Deep", filled.ToDisplayString()); } public void NestedTypeWhoseOuterTypeNeedsAMissingArgument_IsNull() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol outer = compilation.GetTypeByMetadataName("Outer`1").Construct(Digit(compilation, 3)); INamedTypeSymbol inner = outer.GetTypeMembers("Inner").Single().Construct(Digit(compilation, 0)); CheckNull(SavedTypeAnalyzer.Fill(compilation, inner, Arguments(compilation, SpecialType.System_Int32))); } public void NestingPastTheLimit_IsNull() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol list = compilation.GetTypeByMetadataName("System.Collections.Generic.List`1"); ITypeSymbol template = Digit(compilation, 0); for (int i = 0; i < SaveTemplate.MaxDepth; ++i) { template = list.Construct(template); } CheckNull(SavedTypeAnalyzer.Fill(compilation, template, Arguments(compilation, SpecialType.System_Int32))); } public void NestingAtTheLimit_IsFilled() { CSharpCompilation compilation = Compilation(); INamedTypeSymbol list = compilation.GetTypeByMetadataName("System.Collections.Generic.List`1"); ITypeSymbol template = Digit(compilation, 0); for (int i = 0; i < SaveTemplate.MaxDepth - 1; ++i) { template = list.Construct(template); } CheckNotNull(SavedTypeAnalyzer.Fill(compilation, template, Arguments(compilation, SpecialType.System_Int32))); } } public class InitializeTests : TestFixture { public void UnsaveableTypeArgument_IsReportedNamingWhatsSaved() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { " + "public void Save(IStore store) { store.Put(\"visited\", new System.Collections.ObjectModel.Collection()); } }"); CheckEqual(new[] { "VEL0004:store.Put(\"visited\", new System.Collections.ObjectModel.Collection())" }, run.Compiled); CheckEqual(new[] { Unsaveable("visited", "System.Collections.ObjectModel.Collection") }, run.CompiledMessages); } public void SaveableTypeArguments_AreAllowed() { VellumRun run = VellumRun.Of(StoreLibrary, ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point") + @" public class User { public void Save(IStore store) { store.Put(""count"", 1); store.Put(""name"", ""text""); store.Put(""day"", System.DayOfWeek.Monday); store.Put(""at"", new Point()); store.Put(""maybe"", (Point?)null); } }"); CheckEqual(0, run.Compiled.Count); } public void GenericSaveable_IsJudgedByItsSavedMembers() { VellumRun run = VellumRun.Of(StoreLibrary, @" [Saveable] public partial struct Tagged { public ValueType Value; [NoSave] public TagType Tag; } public class User { public void Save(IStore store) { store.Put(""fine"", new Tagged()); store.Put(""lost"", new Tagged()); } }"); CheckEqual(new[] { Unsaveable("lost", "object", " (inside Tagged)") }, run.CompiledMessages); } public void GenericTypeOfSaveableArguments_IsAllowed() { VellumRun run = VellumRun.Of(StoreLibrary, ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + @" public class User { public void Save(IStore store) { store.Put(""count"", new Box()); store.Put(""nested"", new Box>()); store.Put(""numbers"", new int[0]); store.Put(""grid"", new Box[0][]); store.Put(""stops"", new List>()); store.Put(""zones"", new Dictionary>>()); } }"); CheckEqual(0, run.Compiled.Count); } public void GenericTypeWithAnUnsaveableArgument_IsReportedNamingTheArgumentAndWhatItIsInside() { VellumRun run = VellumRun.Of(StoreLibrary, ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + @" public class User { public void Save(IStore store) { store.Put(""stops"", new Box>()); } }"); CheckEqual(new[] { Unsaveable("stops", "object", " (inside Box>)") }, run.CompiledMessages); } public void GenericTypeNoConverterHandles_IsReportedAsItself() { VellumRun run = VellumRun.Of(StoreLibrary, ConverterScanTests.Box + ConverterScanTests.Converter("BoxConverter", "Box") + @" public class User { public void Save(IStore store) { store.Put(""stops"", new Box>()); } }"); CheckEqual(new[] { Unsaveable("stops", "System.Collections.ObjectModel.Collection", " (inside Box>)"), }, run.CompiledMessages); } public void CollectionHoldingAnUnsaveableType_IsReportedNamingTheTypeAndWhatItIsInside() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public void Save(IStore store) " + "{ store.Put(\"stops\", new List>()); } }"); CheckEqual(new[] { Unsaveable("stops", "object", " (inside System.Collections.Generic.List<" + "System.Collections.Generic.Dictionary>)"), }, run.CompiledMessages); } public const string BagOfT = @" [assembly: RegisterSaveMethod(""M:Bag`1.Put(System.String,`0)"", typeof(SaveClassParameter))] public class Bag { public void Put(string key, T value) { } public void Other(string key, T value) { } public static void Make(string key, T value) { } } "; public void ClassParameterOfARegisteredMember_IsCheckedAtCallsOnTheClosedType() { VellumRun run = VellumRun.Of(BagOfT + @" public class User { public void Run(Bag bad, Bag good) { bad.Put(""stops"", null); good.Put(""fine"", 1); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void MemberNobodyRegistered_IsNotChecked() { VellumRun run = VellumRun.Of(BagOfT + @" public class User { public void Run(Bag bad) { bad.Other(""stops"", null); } }"); CheckEqual(0, run.Compiled.Count); } public void StaticMemberOfAGenericClass_IsChecked() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Bag`1.Make(System.String,`0)"", typeof(SaveClassParameter))] public class Bag { public static void Make(string key, T value) { } } public class User { public void Run() { Bag.Make(""stops"", null); Bag.Make(""fine"", 1); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void TypeNestedInAGenericOne_NumbersItsClassParametersFromTheOutermost() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Outer`1.Inner`1.Put(System.String,`1)"", typeof(SaveClassParameter))] [assembly: RegisterSaveMethod(""M:Outer`1.Inner`1.Keep(System.String,`0)"", typeof(SaveClassParameter))] public class Outer { public class Inner { public void Put(string key, B value) { } public void Keep(string key, A value) { } } } public class User { public void Run(Outer.Inner inner, Outer.Inner other) { inner.Put(""inner"", null); inner.Keep(""fine"", 1); other.Put(""fine"", 1); other.Keep(""outer"", null); } }"); CheckEqual(new[] { Unsaveable("inner", "object"), Unsaveable("outer", "object") }, run.CompiledMessages); } public void ClassAndMethodParametersInOneRegistration_AreBothChecked() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Bag`1.Put``1(System.String,`0,``0)"", typeof(Dictionary, SaveMethodParameter0>))] public class Bag { public void Put(string key, T first, U second) { } } public class User { public void Run(Bag ints, Bag objects) { ints.Put(""fine"", 1, 2); ints.Put(""second"", 1, new object()); objects.Put(""first"", null, 2); } }"); CheckEqual(2, run.CompiledMessages.Count); CheckEqual(new[] { "VEL0004:'second' is saved, but a object (inside System.Collections.Generic." + "Dictionary) can't be saved. Mark it [Saveable], or write a SaveConverter " + "for it.", "VEL0004:'first' is saved, but a object (inside System.Collections.Generic." + "Dictionary) can't be saved. Mark it [Saveable], or write a SaveConverter " + "for it." }, run.CompiledMessages); } public void MemberInheritedFromAClosedGenericBase_IsChecked() { VellumRun run = VellumRun.Of(BagOfT + @" public class ObjectBag : Bag { } public class IntBag : Bag { } public class User { public void Run(ObjectBag bad, IntBag good) { bad.Put(""stops"", null); good.Put(""fine"", 1); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void InterfaceWithItsParametersInAnotherOrder_IsCheckedWithTheClassesArguments() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag`1.Put(System.String,`0)"", typeof(SaveClassParameter))] public interface IBag { void Put(string key, X value); } public class Pair : IBag { public void Put(string key, B value) { } } public class User { public void Run(Pair bad, Pair good) { bad.Put(""stops"", null); good.Put(""fine"", 1); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void CallThroughTheInterface_IsChecked() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag`1.Put(System.String,`0)"", typeof(SaveClassParameter))] public interface IBag { void Put(string key, X value); } public class User { public void Run(IBag bad, IBag good) { bad.Put(""stops"", null); good.Put(""fine"", 1); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void OverrideOfARegisteredMemberInAGenericBase_IsCheckedWithTheBasesArguments() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Base`1.Put(System.String,`0)"", typeof(SaveClassParameter))] public class Base { public virtual void Put(string key, T value) { } } public class Derived : Base> { public override void Put(string key, List value) { } } public class User { public void Run(Derived bad, Derived good) { bad.Put(""stops"", null); good.Put(""fine"", null); } }"); CheckEqual(new[] { Unsaveable("stops", "object", " (inside System.Collections.Generic.List)") }, run.CompiledMessages); } public void ClassParameterBeyondTheTypesParameters_IsIgnored() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Bag`1.Put(System.String,`0)"", typeof(SaveClassParameter))] public class Bag { public void Put(string key, T value) { } } public class User { public void Run(Bag bad) { bad.Put(""stops"", null); } }"); CheckEqual(0, run.Compiled.Count); } public void ClassParameterOfATypeWithNone_IsIgnored() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Plain.Put(System.String,System.Object)"", typeof(SaveClassParameter))] public class Plain { public void Put(string key, object value) { } } public class User { public void Run(Plain plain) { plain.Put(""stops"", null); } }"); CheckEqual(0, run.Compiled.Count); } public void TypeParameterOfTheCaller_IsLeftToItsCallers() { VellumRun run = VellumRun.Of(BagOfT + @" public class User { public void Run(Bag bag, T value) { bag.Put(""any"", value); } }"); CheckEqual(0, run.Compiled.Count); } public void RegistrationNamingNothingTheCompilationCanSee_IsIgnored() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:Nothing`1.Put(`0)"", typeof(SaveClassParameter))] public class User { public void Run() { } }"); CheckEqual(0, run.Compiled.Count); } public void RegistrationInTheSameAssembly_Counts() { VellumRun run = VellumRun.Of(Store + "public class User { public void Save(IStore store) { store.Put(\"visited\", new object()); } }"); CheckEqual(new[] { "VEL0004:store.Put(\"visited\", new object())" }, run.Compiled); } public void UnregisteredType_IsIgnored() { VellumRun run = VellumRun.Of( "public interface IStore { void Put(string key, T value); }\n" + "public class User { public void Save(IStore store) { store.Put(\"visited\", new object()); } }"); CheckEqual(0, run.Compiled.Count); } public void GenericMethodOfAnUnregisteredType_IsIgnored() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public object[] Nothing() { return System.Array.Empty(); } }"); CheckEqual(0, run.Compiled.Count); } public void NonGenericMethodOfARegisteredType_IsIgnored() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public void Save(IStore store) { store.Put(new object()); } }"); CheckEqual(0, run.Compiled.Count); } public void UnregisteredGenericMethodOfARegisteredType_IsIgnored() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public void Save(IStore store) { store.Note(\"text\"); } }"); CheckEqual(0, run.Compiled.Count); } public void OnlyTheRegisteredTypeArgument_IsChecked() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public void Save(IStore store) " + "{ store.Pair(new object(), 1); store.Pair(1, new object()); } }"); CheckEqual(new[] { "VEL0004:store.Pair(1, new object())" }, run.Compiled); } public void RegistrationOutsideTheMethodsTypeParameters_IsIgnored() { VellumRun run = VellumRun.Of( "[assembly: RegisterSaveMethod(\"M:IOther.Put``1(``0)\", typeof(SaveMethodParameter1))]\n" + "[assembly: RegisterSaveMethod(\"M:IOther.Put``1(``0)\", typeof(SaveMethodParameter7))]\n" + "public interface IOther { void Put(T value); }\n" + "public class User { public void Save(IOther store) { store.Put(new object()); } }"); CheckEqual(0, run.Compiled.Count); } public void SavedTypeBuiltFromTheStandIns_IsFilledFromTheCallsTypeArguments() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``2(System.String,``0,``1)"", typeof(Dictionary))] public interface IBag { void Put(string name, TKey key, TItem item); } public class User { public void Save(IBag bag) { bag.Put(""fine"", 1, 2); bag.Put(""badKey"", new object(), 2); bag.Put(""badItem"", 1, new object()); } }"); CheckEqual(new[] { Unsaveable("badKey", "object", " (inside System.Collections.Generic.Dictionary)"), Unsaveable("badItem", "object", " (inside System.Collections.Generic.Dictionary)"), }, run.CompiledMessages); } public void OverloadWithAnotherId_IsIgnored() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(``0)"", typeof(SaveMethodParameter0))] public interface IBag { void Put(T value); void Put(T value, int count); } public class User { public void Save(IBag bag) { bag.Put(new object(), 2); } }"); CheckEqual(0, run.Compiled.Count); } public void CallThroughAClass_ReachesTheRegistrationOfItsInterface() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(SaveMethodParameter0))] public interface IBag { void Put(string name, T value); } public class Bag : IBag { public void Put(string name, T value) { } } public class User { public void Save(Bag bag) { bag.Put(""stops"", new object()); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void CallThroughAnOverride_ReachesTheRegistrationOfTheBaseMethod() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:BaseBag.Put``1(System.String,``0)"", typeof(SaveMethodParameter0))] public class BaseBag { public virtual void Put(string name, T value) { } } public class MiddleBag : BaseBag { public override void Put(string name, T value) { } } public class Bag : MiddleBag { public override void Put(string name, T value) { } } public class User { public void Save(Bag bag) { bag.Put(""stops"", new object()); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void CallThroughAnInheritedImplementation_ReachesTheRegistrationOfTheInterface() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(SaveMethodParameter0))] public interface IBag { void Put(string name, T value); } public class BaseBag { public void Put(string name, T value) { } } public class Bag : BaseBag, IBag { } public class User { public void Save(Bag bag) { bag.Put(""stops"", new object()); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void ClassAndInterfaceBothRegistered_ReportOnce() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(SaveMethodParameter0))] [assembly: RegisterSaveMethod(""M:Bag.Put``1(System.String,``0)"", typeof(SaveMethodParameter0))] public interface IBag { void Put(string name, T value); } public class Bag : IBag { public void Put(string name, T value) { } } public class User { public void Save(Bag bag) { bag.Put(""stops"", new object()); } }"); CheckEqual(new[] { Unsaveable("stops", "object") }, run.CompiledMessages); } public void InterfaceMethodThatIsntRegistered_IsNotReachedThroughAnotherOne() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(SaveMethodParameter0))] public interface IBag { void Put(string name, T value); void Note(string name, T value); } public class Bag : IBag { public void Put(string name, T value) { } public void Note(string name, T value) { } } public class User { public void Save(Bag bag) { bag.Note(""stops"", new object()); } }"); CheckEqual(0, run.Compiled.Count); } public void TemplateWithTheSameTypeTwice_ChecksEachPlace() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(Dictionary, List[]>))] public interface IBag { void Put(string name, T value); } public class User { public void Save(IBag bag) { bag.Put(""fine"", 1); bag.Put(""bad"", new object()); } }"); CheckEqual(new[] { Unsaveable("bad", "object", " (inside System.Collections.Generic.Dictionary<" + "System.Collections.Generic.List, System.Collections.Generic.List[]>)"), }, run.CompiledMessages); } public void TemplateFilledWithANullable_IsJudgedByTheTypeInsideIt() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(List))] public interface IBag { void Put(string name, T value); } public class User { public void Save(IBag bag) { bag.Put(""maybe"", (int?)null); } }"); CheckEqual(0, run.Compiled.Count); } public void TemplateFilledWithATypeParameter_IsLeftToTheGenericCodesCallers() { VellumRun run = VellumRun.Of(@" [assembly: RegisterSaveMethod(""M:IBag.Put``1(System.String,``0)"", typeof(List))] public interface IBag { void Put(string name, T value); } public class User { public void Save(IBag bag, T value) { bag.Put(""any"", value); } }"); CheckEqual(0, run.Compiled.Count); } public void InferredRegistration_IsChecked() { MetadataReference library = VellumRun.CompileLibrary("Saver", "public interface IStore { void Put(string key, T value); }\n" + "public class Store : IStore\n{\n" + " public SaveSession Session;\n" + " public void Put(string key, T value) { Session.ToNode(value, typeof(T)); }\n" + "}\n"); VellumRun run = VellumRun.Of(library, "public class User { " + "public void Save(IStore store) { store.Put(\"visited\", new System.Collections.ObjectModel.Collection()); } }"); CheckEqual(new[] { "VEL0004:store.Put(\"visited\", new System.Collections.ObjectModel.Collection())" }, run.Compiled); } public void InferredRegistrationThroughTypedToNode_IsChecked() { MetadataReference library = VellumRun.CompileLibrary("TypedSaver", "public interface IStore { void Put(T value); }\n" + "public class Store : IStore\n{\n" + " public SaveSession Session;\n" + " public void Put(T value) { Session.ToNode(value); }\n" + "}\n"); VellumRun run = VellumRun.Of(library, "public class User { " + "public void Save(IStore store, System.Collections.ObjectModel.Collection bag) " + "{ store.Put(bag); } }"); CheckEqual(new[] { "VEL0004:store.Put(bag)" }, run.Compiled); } public void TypedToNodeOfAnUnsaveableType_IsReported() { VellumRun run = VellumRun.Of("public class User { public void Save(SaveSession session, " + "System.Collections.ObjectModel.Collection bag) { session.ToNode(bag); } }"); CheckEqual(new[] { "VEL0004:session.ToNode(bag)" }, run.Compiled); CheckEqual(new[] { Unsaveable("bag", "System.Collections.ObjectModel.Collection") }, run.CompiledMessages); } public void TypedFromNodeOfAnUnsaveableType_IsReported() { VellumRun run = VellumRun.Of("public class User { public void Load(SaveSession session, " + "SaveNode node) { session.FromNode>(node); } }"); CheckEqual( new[] { "VEL0004:session.FromNode>(node)" }, run.Compiled); } public void TypedReadOfAnUnsaveableType_IsReported() { VellumRun run = VellumRun.Of("public class User { public void Load(SaveReader reader) " + "{ reader.Require>(\"bag\"); } }"); CheckEqual(new[] { Unsaveable("bag", "System.Collections.ObjectModel.Collection") }, run.CompiledMessages); } public void TryReadOfAnUnsaveableType_IsReported() { VellumRun run = VellumRun.Of("public class User { public void Load(SaveReader reader, " + "out System.Collections.ObjectModel.Collection bag) { reader.TryRead(\"bag\", out bag); } }"); CheckEqual(new[] { Unsaveable("bag", "System.Collections.ObjectModel.Collection") }, run.CompiledMessages); } public void ItemsOfAnUnsaveableType_AreReported() { VellumRun run = VellumRun.Of("public class User { public void Save(SaveSession session, " + "List> bags) " + "{ SavedItems.ToNode(bags, session); } }"); CheckEqual(1, run.Compiled.Count); } public void EntriesWithAnUnsaveableKeyOrValue_AreReportedOnce() { VellumRun run = VellumRun.Of("public class User { public void Save(SaveSession session, " + "Dictionary map, Dictionary keyed) " + "{ SavedEntries.ToNode(map, session); SavedEntries.ToNode(keyed, session); } }"); CheckEqual(2, run.Compiled.Count); } public void TypedCallsOfSaveableTypes_AreAllowed() { VellumRun run = VellumRun.Of("public class User { public void Save(SaveSession session, " + "SaveNode node) { session.ToNode(5); session.ToNode(\"text\"); session.FromNode(node); " + "session.FromNode(node); } }"); CheckEqual(0, run.Compiled.Count); } public void TypeParameter_IsLeftToTheGenericCodesCallers() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { " + "public void Save(IStore store, T value) { store.Put(\"value\", value); } }"); CheckEqual(0, run.Compiled.Count); } public void TypeThatDoesntExist_IsLeftToTheCompilersError() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public void Save(IStore store) { store.Put(\"value\", new Missing()); } }"); CheckEqual(new[] { "CS0246:Missing" }, run.Compiled); } public void NonConstantName_IsShownAsWritten() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { " + "public void Save(IStore store, string key) { store.Put(key, new object()); } }"); CheckEqual(new[] { Unsaveable("key", "object") }, run.CompiledMessages); } public void CallWithoutArguments_IsNamedByItsMethod() { VellumRun run = VellumRun.Of(StoreLibrary, "public class User { public void Load(IStore store) { store.Make(); } }"); CheckEqual(new[] { Unsaveable("Make", "object") }, run.CompiledMessages); } public void MethodOfAGenericRegisteredType_IsChecked() { VellumRun run = VellumRun.Of( "[assembly: RegisterSaveMethod(\"M:IKeyed`1.Put``1(`0,``0)\", typeof(SaveMethodParameter0))]\n" + "public interface IKeyed { void Put(TKey key, T value); }\n" + "public class User { public void Save(IKeyed store) { store.Put(1, new object()); } }"); CheckEqual(new[] { Unsaveable("1", "object") }, run.CompiledMessages); } public void ArcsUnsaveableFieldAndLocal_AreReportedOnTheScriptsOwnLines() { VellumRun run = VellumRun.OfArcs(@" using Ariadne; public class Gates { public HashSet Open = new HashSet(); } public class Waypoint { } public partial class Mission : Arc { public Waypoint Destination; public int Count; protected override void Run(Gates gates) { var visited = new System.Collections.ObjectModel.Collection(); wait: while (!gates.Open.Contains(""go"")) { visited.Add(""x""); Yield(); } } }"); CheckEqual(0, run.Reported.Count); CheckEqual(new[] { "VEL0004:Script.cs(13,21)", "VEL0004:Script.cs(19,13)" }, run.CompiledAt); CheckEqual(new[] { Unsaveable("Destination", "Waypoint"), Unsaveable("Run.visited", "System.Collections.ObjectModel.Collection"), }, run.CompiledMessages); } public void ArcHoldingAClassBelowASaveableOne_IsAllowed() { VellumRun run = VellumRun.OfArcs(@" [Saveable] public partial class Animal { public string Name; } public sealed partial class Dog : Animal { public bool Barks; } public class Gates { public HashSet Open = new HashSet(); } public partial class Mission : Ariadne.Arc { public Animal Pet; protected override void Run(Gates gates) { Animal start = new Dog(); wait: while (!gates.Open.Contains(""go"")) { Yield(); } Pet = start; } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void ArcHoldingASaveableType_IsAllowed() { VellumRun run = VellumRun.OfArcs(@" [Saveable] public partial struct Waypoint { public string Name; public double X; } public class Gates { public HashSet Open = new HashSet(); } public partial class Mission : Ariadne.Arc { public Waypoint Destination; protected override void Run(Gates gates) { var start = new Waypoint(); wait: while (!gates.Open.Contains(""go"")) { Yield(); } Destination = start; } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } public void ArcWithConvertedState_IsAllowed() { VellumRun run = VellumRun.OfArcs(ConverterScanTests.Point + ConverterScanTests.Converter("PointConverter", "Point") + @" public class Gates { public HashSet Open = new HashSet(); } public partial class Mission : Ariadne.Arc { public Point Target; protected override void Run(Gates gates) { var start = new Point(); wait: while (!gates.Open.Contains(""go"")) { Yield(); } Target = start; } }"); CheckEqual(0, run.Reported.Count); CheckEqual(0, run.Compiled.Count); } } } }