275 changes: 275 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/MSBuild.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System.Collections.Immutable;
using System.Text;

// Note that ProjectElementContainer-derived types lack any properties that expose
// the specific types of children they support. And the Children member that is exposed
// allows any type of ProjectElement.
// So an enhancement we should consider is to optionally hide the Children member
// from the public API and allow more constrained types of children via the public API.

// class ElementLocation {
// int column;
// int line;
// string file;
// string locationString;
// }

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
abstract partial class ProjectElement
{
readonly string condition;
// readonly ElementLocation conditionLocation;

readonly string label;
// readonly ElementLocation labelLocation;

// readonly ElementLocation location;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
abstract partial class ProjectElementContainer : ProjectElement
{
readonly ImmutableList<ProjectElement> children;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectRootElement : ProjectElementContainer
{
readonly string fullPath;
readonly Encoding encoding;

readonly string toolsVersion;
// readonly ElementLocation toolsVersionLocation;

readonly string defaultTargets;
// readonly ElementLocation defaultTargetsLocation;

readonly string initialTargets;
// readonly ElementLocation initialTargetsLocation;

readonly bool treatAsLocalProperty;
// readonly ElementLocation treatAsLocalPropertylocation;

static partial void CreateDefaultTemplate(ref Template template)
{
template.Children = ImmutableList.Create<ProjectElement>();
}
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectPropertyGroupElement : ProjectElementContainer
{
static partial void CreateDefaultTemplate(ref Template template)
{
template.Children = ImmutableList.Create<ProjectElement>();
}
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectItemGroupElement : ProjectElementContainer
{
static partial void CreateDefaultTemplate(ref Template template)
{
template.Children = ImmutableList.Create<ProjectElement>();
}
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectChooseElement : ProjectElementContainer
{
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectOtherwiseElement : ProjectElementContainer
{
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectWhenElement : ProjectElementContainer
{
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectPropertyElement : ProjectElement
{
readonly string name;
readonly string value;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectItemElement : ProjectElementContainer
{
readonly string exclude;
// readonly ElementLocation ExcludeLocation;

readonly string include;
// readonly ElementLocation IncludeLocation;

readonly string itemType;

readonly string keepDuplicates;
// readonly ElementLocation KeepDuplicatesLocation;

readonly string keepMetadata;
// readonly ElementLocation KeepMetadataLocation;

readonly string remove;
// readonly ElementLocation RemoveLocation;

readonly string removeMetadata;
// readonly ElementLocation RemoveMetadataLocation;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectMetadataElement : ProjectElement
{
readonly string name;
readonly string value;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectExtensionsElement : ProjectElement
{
readonly string content;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectImportElement : ProjectElement
{
readonly string project;
// readonly ElementLocation projectLocation;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectImportGroupElement : ProjectElementContainer
{
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectItemDefinitionElement : ProjectElementContainer
{
readonly string itemType;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectItemDefinitionGroupElement : ProjectElementContainer
{
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectOnErrorElement : ProjectElement
{
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectOutputElement : ProjectElement
{
readonly bool isOutputItem;
readonly bool isOutputProperty;

readonly string itemType;
// readonly ElementLocation itemTypeLocation;

readonly string propertyName;
// readonly ElementLocation propertyNameLocation;

readonly string taskParameter;
// readonly ElementLocation TaskParameterLocation;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectTargetElement : ProjectElementContainer
{
readonly string afterTargets;
// readonly ElementLocation AfterTargetsLocation;

readonly string beforeTargets;
// readonly ElementLocation BeforeTargetsLocation;

readonly string dependsOnTargets;
// readonly ElementLocation DependsOnTargetsLocation;

readonly string inputs;
// readonly ElementLocation InputsLocation;

readonly string keepDuplicateOutputs;
// readonly ElementLocation KeepDuplicateOutputsLocation;

readonly string name;
// readonly ElementLocation NameLocation;

readonly string outputs;
// readonly ElementLocation OutputsLocation;

readonly string returns;
// readonly ElementLocation ReturnsLocation;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectTaskElement : ProjectElementContainer
{
readonly string continueOnError;
// readonly ElementLocation ContinueOnErrorLocation;

readonly string msbuildArchitecture;
// readonly ElementLocation MSBuildArchitectureLocation;

readonly string msbuildRuntime;
// readonly ElementLocation MSBuildRuntimeLocation;

readonly string name;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectUsingTaskBodyElement : ProjectElement
{
readonly string evaluate;
// readonly ElementLocation evaluateLocation;

readonly string taskBody;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectUsingTaskElement : ProjectElementContainer
{
readonly string architecture;
// readonly ElementLocation ArchitectureLocation;

readonly string assemblyFile;
// readonly ElementLocation AssemblyFileLocation;

readonly string assemblyName;
// readonly ElementLocation AssemblyNameLocation;

readonly string runtime;
// readonly ElementLocation RuntimeLocation;

readonly string taskFactory;
// readonly ElementLocation TaskFactoryLocation;

readonly string taskName;
// readonly ElementLocation TaskNameLocation;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class ProjectUsingTaskParameterElement : ProjectElement
{
readonly string name;

readonly string output;
// readonly ElementLocation outputLocation;

readonly string parameterType;
// readonly ElementLocation parameterTypeLocation;

readonly string required;
// readonly ElementLocation requiredLocation;
}

[GenerateImmutable(DefineRootedStruct = true, Delta = true, DefineWithMethodsPerProperty = true)]
partial class UsingTaskParameterGroupElement : ProjectElementContainer
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
public class NestedTests
{
[Fact]
public void NestedCreateBuilder()
{
var nestedBuilder = Nested.NestedClass.CreateBuilder();
Assert.NotNull(nestedBuilder);
}

[Fact]
public void NestedConstruction()
{
var nested = Nested.NestedClass.Create("a name");
Assert.NotNull(nested);
}
}
}
17 changes: 17 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/Nested.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
partial class Nested
{
[GenerateImmutable(GenerateBuilder = true, DefineWithMethodsPerProperty = true)]
public partial class NestedClass
{
readonly string name;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class Empty { }

[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class EmptyDerived : Empty
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class Empty
{
}

[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class NotSoEmptyDerived : Empty
{
readonly bool oneField;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

[GenerateImmutable]
abstract partial class RootRecursive
{
}

[GenerateImmutable]
abstract partial class RecursiveContainer : RootRecursive
{
readonly ImmutableList<RootRecursive> children;
}

[GenerateImmutable]
partial class ContainerOfNonRecursiveCollection : RootRecursive
{
[NotRecursive]
readonly ImmutableList<NonRecursiveElement> metadata;
}

[GenerateImmutable]
partial class NonRecursiveElement : RootRecursive
{
readonly string name;
readonly string value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[ImmutableObjectGraph.Generation.GenerateImmutable]
partial class One
{
readonly Other buddy;
}

[ImmutableObjectGraph.Generation.GenerateImmutable]
partial class Other
{
readonly int count;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[ImmutableObjectGraph.Generation.GenerateImmutable]
partial class Apple
{
readonly int seeds;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class NonEmptyBase
{
readonly bool oneField;
}

[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class EmptyDerivedFromNonEmptyBase : NonEmptyBase
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[ImmutableObjectGraph.Generation.GenerateImmutable(GenerateBuilder = true)]
partial class Apple
{
readonly int seeds;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xunit;

public class PersonBuilderTests
{
[Fact]
public void ToBuilderReturnsSimilarObject()
{
var person = Person.Create("bill", age: 10);

var personBuilder = person.ToBuilder();
Assert.NotNull(personBuilder);
Assert.Equal(person.Name, personBuilder.Name);
Assert.Equal(person.Age, personBuilder.Age);
}

[Fact]
public void ToBuilderToImmutableRoundtripReusesInstance()
{
var person = Person.Create("bill", age: 10);
var personBuilder = person.ToBuilder();
var roundTripPerson = personBuilder.ToImmutable();
Assert.Same(person, roundTripPerson);
}

[Fact]
public void MutablePropertiesRetainChanges()
{
var person = Person.Create("bill", age: 10);
var personBuilder = person.ToBuilder();

personBuilder.Name = "billy";
personBuilder.Age = 8;

Assert.Equal("billy", personBuilder.Name);
Assert.Equal(8, personBuilder.Age);
}

[Fact]
public void ToImmutableReturnsSimilarObject()
{
var person = Person.Create("bill", age: 10);
var personBuilder = person.ToBuilder();

personBuilder.Name = "billy";
personBuilder.Age = 8;

var recreatedPerson = personBuilder.ToImmutable();
Assert.Equal(personBuilder.Name, recreatedPerson.Name);
Assert.Equal(personBuilder.Age, recreatedPerson.Age);
}

[Fact]
public void ToImmutableCalledRepeatedlyAfterChangesReusesInstance()
{
var person = Person.Create(null);
var builder = person.ToBuilder();
Assert.Same(person, builder.ToImmutable());
builder.Name = "bill";
var bill1 = builder.ToImmutable();
var bill2 = builder.ToImmutable();
Assert.NotSame(person, bill1);
Assert.Same(bill1, bill2);
}

[Fact]
public void PropertiesAreAlsoBuilders()
{
var person = Person.Create("bill", watch: Watch.Create());
var personBuilder = person.ToBuilder();
personBuilder.Watch.Color = "Red";
var modifiedPerson = personBuilder.ToImmutable();
Assert.Equal("Red", modifiedPerson.Watch.Color);

personBuilder.Watch = null;
var personWithoutWatch = personBuilder.ToImmutable();
Assert.Null(personWithoutWatch.Watch);
}

[Fact]
public void PropertyBuildersAreNullIfImmutableIsNull()
{
var person = Person.Create("bill");
var builder = person.ToBuilder();
Assert.Null(builder.Watch);
}

[Fact]
public void CreateBuilder()
{
var builder = Person.CreateBuilder();
builder.Name = "name";
var immutable = builder.ToImmutable();
Assert.Equal("name", immutable.Name);
}
}
}
204 changes: 204 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/Person.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using Xunit;

public class PersonTests
{
/// <summary>
/// Immutable types should have 1 public constructor to support deserialization.
/// </summary>
[Fact]
public void PublicConstructor()
{
#pragma warning disable CS0618
var p = new Person(Name: "Andrew", Age: 15, Watch: null);
#pragma warning restore CS0618
Assert.Equal("Andrew", p.Name);
Assert.Equal(15, p.Age);
Assert.Null(p.Watch);
}

[Fact]
public void DefaultInstance()
{
var defaultPerson = Person.Create(null);
Assert.NotNull(defaultPerson);
Assert.Equal(0, defaultPerson.Age);
Assert.Null(defaultPerson.Name);
}

[Fact]
public void CreateWithArguments()
{
var billyAge10 = Person.Create(name: "billy", age: 10);
Assert.Equal("billy", billyAge10.Name);
Assert.Equal(10, billyAge10.Age);
}

[Fact]
public void SetScalarReferenceTypeProperty()
{
var original = Person.Create(null);
var modified = original.WithName("bill");
Assert.Equal("bill", modified.Name);
Assert.Null(original.Name);
}

[Fact]
public void SetScalarValueTypeProperty()
{
var original = Person.Create(null);
var modified = original.WithAge(8);
Assert.Equal(8, modified.Age);
Assert.Equal(0, original.Age);
}

[Fact]
public void SetScalarReferenceTypePropertyToSameValueReturnsSameInstance()
{
var expected = Person.Create(null).WithName("bill");
var actual = expected.WithName(expected.Name);
Assert.Same(expected, actual);
}

[Fact]
public void SetScalarValueTypePropertyToSameValueReturnsSameInstance()
{
var expected = Person.Create(null).WithAge(8);
var actual = expected.WithAge(expected.Age);
Assert.Same(expected, actual);
}

[Fact]
public void WithSetsNonDefaultValues()
{
// Initialize
var billAge10 = Person.Create(null).With(name: "bill", age: 10);
Assert.Equal("bill", billAge10.Name);
Assert.Equal(10, billAge10.Age);

// Full modification
var jillAge9 = billAge10.With(name: "jill", age: 9);
Assert.Equal("jill", jillAge9.Name);
Assert.Equal(9, jillAge9.Age);

// Partial modification
var billAge12 = billAge10.With(age: 12);
Assert.Equal("bill", billAge12.Name);
Assert.Equal(12, billAge12.Age);

var billyAge10 = billAge10.With(name: "billy");
Assert.Equal("billy", billyAge10.Name);
Assert.Equal(10, billyAge10.Age);
}

[Fact]
public void WithRevertsToDefaultValues()
{
var billAge10 = Person.Create(name: "bill", age: 10);

var billAge0 = billAge10.With(age: 0);
Assert.Equal(0, billAge0.Age);
Assert.Equal("bill", billAge0.Name);

var age10 = billAge10.With(name: null);
Assert.Equal(10, age10.Age);
Assert.Null(age10.Name);
}

[Fact]
public void ReferenceToOtherImmutable()
{
var blackWatch = Watch.Create(color: "black", size: 10);
var personWithBlackWatch = Person.Create(null, watch: blackWatch);

var silverWatch = blackWatch.WithColor("silver");
var personWithSilverWatch = personWithBlackWatch.WithWatch(silverWatch);
Assert.Equal(silverWatch, personWithSilverWatch.Watch);
}

[Fact]
public void WithPreservesInstanceWhenNoChangesMade()
{
var bill = Person.Create(name: "bill");
Assert.Same(bill, bill.With());
Assert.Same(bill, bill.With(name: "bill"));
}

[Fact]
public void DefaultValues()
{
// We expect Members to be non-null because we have a partial class defined that specifies that.
var family = Family.Create();
Assert.NotNull(family.Members);
Assert.Empty(family.Members);
}

[Fact]
public void DefaultValuesCanBeOverriddenWithTypeDefaults()
{
Assert.NotNull(Family.Create().Members); // the test is only valid if the default value is non-null
Assert.Null(Family.Create(members: null).Members);
Assert.Null(Family.Create().WithMembers((ImmutableSortedSet<Person>)null).Members);
}

[Fact]
public void DefaultValuesCanBeOverriddenWithOtherValue()
{
Assert.NotNull(Family.Create().Members); // the test is only valid if the default value is non-null

var otherMembers = ImmutableSortedSet.Create(Person.Create("bill"));
Assert.Same(otherMembers, Family.Create(members: otherMembers).Members);
Assert.Same(otherMembers, Family.Create().WithMembers(otherMembers).Members);
}

[Fact]
public void ImmutableCollection()
{
var members = ImmutableSortedSet.Create<Person>();
var family = Family.Create(members: members);
Assert.Same(members, family.Members);
var newMembers = family.Members.Add(Person.Create("bill"));
Assert.Same(members, family.Members);
Assert.NotSame(newMembers, members);

var newFamily = family.WithMembers(newMembers);
Assert.Same(newMembers, newFamily.Members);
}

[Fact]
public void CollectionsAlternateMutationMethods()
{
var family = Family.Create();
var familyAdd1 = family.AddMembers(Person.Create("billy", age: 5));
Assert.Empty(family.Members);
Assert.Single(familyAdd1.Members);

var familyAdd1More = familyAdd1.AddMembers(Person.Create("sally", age: 8));
Assert.Equal(2, familyAdd1More.Members.Count);

var familyRemove1 = familyAdd1More.RemoveMembers(familyAdd1.Members[0]);
Assert.Equal(2, familyAdd1More.Members.Count);
Assert.Single(familyRemove1.Members);

var familyAddMany = familyAdd1.AddMembers(
Person.Create("sally", age: 8),
Person.Create("sam", age: 4));
Assert.Equal(3, familyAddMany.Members.Count);

var familyRemoveMany = familyAddMany.RemoveMembers(familyAdd1More.Members);
Assert.Single(familyRemoveMany.Members);

var familyCleared = familyAddMany.RemoveMembers();
Assert.Empty(familyCleared.Members);
}
}
}
41 changes: 41 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/Person.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System.Collections.Generic;
using System.Collections.Immutable;
using ImmutableObjectGraph;

[GenerateImmutable(DefineWithMethodsPerProperty = true, GenerateBuilder = true)]
partial class Family
{
readonly ImmutableSortedSet<Person> members;

static partial void CreateDefaultTemplate(ref Template template)
{
template.Members = ImmutableSortedSet.Create<Person>(new FamilyMemberComparer());
}

private class FamilyMemberComparer : IComparer<Person>
{
public int Compare(Person x, Person y)
{
return x.Age.CompareTo(y.Age);
}
}
}

[GenerateImmutable(DefineWithMethodsPerProperty = true, GenerateBuilder = true)]
partial class Person
{
[Required]
readonly string name;
readonly int age;
readonly Watch watch;
}

[GenerateImmutable(DefineWithMethodsPerProperty = true, GenerateBuilder = true)]
partial class Watch
{
readonly string color;
readonly int size;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System.Collections.Immutable;

public interface IRule { }

public interface IProjectPropertiesContext { }

public interface IPropertySheet { }

public class ProjectPropertiesContext : IProjectPropertiesContext
{
}

[GenerateImmutable(DefineInterface = true, GenerateBuilder = true, DefineWithMethodsPerProperty = true, DefineRootedStruct = true, Delta = true)]
partial class ProjectTree
{
[Required]
readonly string caption;
readonly string filePath;
readonly System.Drawing.Image icon;
readonly System.Drawing.Image expandedIcon;
readonly bool visible;
readonly IRule browseObjectProperties;
readonly ImmutableHashSet<string> capabilities;
readonly ImmutableSortedSet<ProjectTree> children;
}

[GenerateImmutable(DefineInterface = true, GenerateBuilder = true, DefineWithMethodsPerProperty = true, DefineRootedStruct = true, Delta = true)]
partial class ProjectItemTree : ProjectTree
{
[Required]
readonly IProjectPropertiesContext projectPropertiesContext;
readonly IPropertySheet propertySheet;
readonly bool isLinked;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//-----------------------------------------------------------------------
// <copyright file="ProjectTreeCapabilities.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;

/// <summary>
/// String constants that may appear in <see cref="IProjectTree.Capabilities"/>.
/// </summary>
public static class ProjectTreeCapabilities
{
/// <summary>
/// An empty set of project tree capabilities with the case-insensitive comparer.
/// </summary>
[SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "Is indeed immutable.")]
public static readonly ImmutableHashSet<string> EmptyCapabilities = ImmutableHashSet.Create<string>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Indicates that this node is the root project node.
/// </summary>
/// <remarks>
/// This is useful for <see cref="IProjectTreeModifier"/> extensions so they know which node is actually the root one,
/// since all nodes look like root nodes at the time they are being modified.
/// </remarks>
public const string ProjectRoot = "ProjectRoot";

/// <summary>
/// Indicates that this node is the well-known "References" folder.
/// </summary>
public const string ReferencesFolder = "ReferencesFolder";

/// <summary>
/// Indicates that this node is the special project Properties folder.
/// </summary>
public const string AppDesignerFolder = "AppDesignerFolder";

/// <summary>
/// Indicates that this item represents a reference (e.g. Assembly, COM, or Project reference).
/// </summary>
/// <remarks>
/// This capability should only appear on <see cref="IProjectItemTree"/> instances.
/// </remarks>
public const string Reference = "Reference";

/// <summary>
/// Indicates that this item is a reference that has been successfully resolved.
/// </summary>
/// <remarks>
/// This capability should only appear on <see cref="IProjectItemTree"/> instances.
/// </remarks>
public const string ResolvedReference = "ResolvedReference";

/// <summary>
/// Indicates that this item is a reference that failed to resolve.
/// </summary>
/// <remarks>
/// This capability should only appear on <see cref="IProjectItemTree"/> instances.
/// </remarks>
public const string BrokenReference = "BrokenReference";

/// <summary>
/// Indicates that this item represents a source file (e.g. *.cs, *.resx, *.bmp) that is included in the build.
/// </summary>
/// <remarks>
/// This capability should only appear on <see cref="IProjectItemTree"/> instances.
/// </remarks>
public const string SourceFile = "SourceFile";

/// <summary>
/// Indicates that this item represents a folder on disk, and may contain sub-items that can be manipulated by the user.
/// </summary>
/// <remarks>
/// This capability may appear on any <see cref="IProjectTree"/> instance.
/// </remarks>
public const string Folder = "Folder";

/// <summary>
/// Indicates that this item represents a file on disk (not a folder, and not a virtual node).
/// </summary>
public const string FileOnDisk = "FileOnDisk";

/// <summary>
/// Indicates that this item represents a file or folder on disk (not a virtual node).
/// </summary>
public const string FileSystemEntity = "FileSystemEntity";

/// <summary>
/// Indicates that this item should appear near the top of its containing list with other similarly tagged nodes.
/// </summary>
public const string BubbleUp = "BubbleUp";

/// <summary>
/// Indicates that this item does not exist in the project, but does exist on disk and might be included in the project later.
/// </summary>
public const string IncludeInProjectCandidate = "IncludeInProjectCandidate";

/// <summary>
/// Indicates that this item does not exist in the project, but does exist on disk and might be included in the project later,
/// but that the file entity on disk is marked as hidden in the file system.
/// </summary>
public const string HiddenIncludeInProjectCandidate = "HiddenIncludeInProjectCandidate";

/// <summary>
/// Indicates that this item may not exist in the project but implies nothing about includability into a project.
/// </summary>
/// <remarks>
/// Node providers can use this capability to claim responsibility for resolving a node to a path
/// if path resolution fails because the node is not a project item. SDK references are an example.
/// </remarks>
[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "NonMember", Justification = "Ignored")]
public const string NonMemberItem = "NonMemberItem";

/// <summary>
/// Indicates that this item should always be copyable.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Copyable", Justification = "Ignored")]
public const string AlwaysCopyable = "AlwaysCopyable";

/// <summary>
/// Indicates that this item should be implicitly copied, moved, or dragged whenever its immediate parent is.
/// </summary>
public const string FollowsParent = "FollowsParent";
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
//-----------------------------------------------------------------------
// <copyright file="UnattachedProjectTreeNodeTest2.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

/// <summary>
/// This is a test class for ImmutableNodeTest and is intended
/// to contain all ImmutableNodeTest Unit Tests
/// </summary>
public class UnattachedProjectTreeNodeTest2 : ProjectTreeNodeTestBase
{
private ProjectTree node;

public UnattachedProjectTreeNodeTest2()
{
this.node = (ProjectTree)this.NewTree(Caption, children: this.Children);
}

protected override void Cleanup()
{
RecursiveTypeExtensions.LookupTable<ProjectTree, ProjectTree>.ValidateInternalIntegrity(this.node);
base.Cleanup();
}

/// <summary>
/// A test for ImmutableNode Constructor
/// </summary>
[Fact]
public void ImmutableNodeConstructorTest()
{
this.Children = this.Children.Add(this.NewNode()); // children must be non-empty for the collection to be used in the node.
this.node = (ProjectTree)this.NewTree(Caption, children: this.Children);

Assert.Same(Caption, this.node.Caption);
Assert.Same(Children, this.node.Children);
}

[Fact]
public void GetSelfAndDescendentsBreadthFirst_GreenNode()
{
var grandChild1 = this.NewNode();
var grandChild2 = this.NewNode();
var child1 = this.NewNode(grandChild1);
var child2 = this.NewNode(grandChild2);
var parent = this.NewNode(child1, child2);

var array = parent.GetSelfAndDescendentsBreadthFirst().ToArray();
Assert.Same(parent, array[0]);
Assert.Same(child1, array[1]);
Assert.Same(child2, array[2]);
Assert.Same(grandChild1, array[3]);
Assert.Same(grandChild2, array[4]);
}

[Fact]
public void GetSelfAndDescendentsBreadthFirst_RedNode()
{
var grandChild1 = this.NewNode();
var grandChild2 = this.NewNode();
var child1 = this.NewNode(grandChild1);
var child2 = this.NewNode(grandChild2);
var parent = this.NewNode(child1, child2);

var array = parent.AsRoot.GetSelfAndDescendentsBreadthFirst().ToArray();
Assert.Same(parent, array[0].ProjectTree);
Assert.Same(child1, array[1].ProjectTree);
Assert.Same(child2, array[2].ProjectTree);
Assert.Same(grandChild1, array[3].ProjectTree);
Assert.Same(grandChild2, array[4].ProjectTree);
}

[Fact]
public void AddChild()
{
this.Children = this.Children.Add(this.NewNode()); // children must be non-empty for the collection to be used in the node.
this.node = this.NewTree(Caption, children: this.Children);

var newChild = this.NewNode();
var newNode = this.node.AddChildren(newChild);
Assert.Same(this.Children, this.node.Children);
Assert.Equal(2, newNode.Children.Count);
Assert.Same(newChild, newNode.Children[1]);
}

[Fact]
public void RemoveChild()
{
var newChild = this.NewNode();
var nodeWithChildren = this.node.AddChildren(newChild);
var nodeWithoutChildren = nodeWithChildren.RemoveChildren(newChild);
Assert.Single(nodeWithChildren.Children);
Assert.Empty(nodeWithoutChildren.Children);
}

[Fact]
public void RemoveChildrenThrowsOnMissingChild()
{
var newChild = this.NewNode();
Assert.Throws<ArgumentException>(() => this.node.RemoveChildren(new[] { newChild }));
}

[Fact]
public void RemoveChildThrowsOnMissingChild()
{
var newChild = this.NewNode();
Assert.Throws<ArgumentException>(() => this.node.RemoveChild(newChild));
}

[Fact]
public void RemoveChildDeep()
{
var grandChild1 = this.NewNode();
var child = this.NewNode(grandChild1);
var parent = this.NewNode(child);

var newParent = parent.RemoveDescendent(grandChild1);
Assert.Single(parent.Children[0].Children);
Assert.Empty(newParent.Children[0].Children);
}

[Fact]
public void ReplaceChild()
{
var newChild1 = this.NewNode();
var newChild2 = this.NewNode();
var nodeWithChildren = this.node.AddChildren(newChild1);
var newNodeWithChildren = nodeWithChildren.ReplaceDescendent(newChild1, newChild2);
Assert.Single(nodeWithChildren.Children);
Assert.Single(newNodeWithChildren.Children);
Assert.Same(newChild1, nodeWithChildren.Children[0]);
Assert.Same(newChild2, newNodeWithChildren.Children[0]);
}

[Fact]
public void ReplaceChildThrowsOnMissingChild()
{
var newChild1 = this.NewNode();
var newChild2 = this.NewNode();
Assert.Throws<ArgumentException>(() => this.node.ReplaceDescendent(newChild1, newChild2));
}

[Fact]
public void ReplaceChildDeep()
{
var grandChild1 = this.NewNode();
var grandChild2 = this.NewNode();
var child = this.NewNode(grandChild1);
var parent = this.NewNode(child);

var newParent = parent.ReplaceDescendent(grandChild1, grandChild2);
Assert.Same(grandChild1, parent.Children[0].Children[0]);
Assert.Single(newParent.Children[0].Children);
Assert.Same(grandChild2, newParent.Children[0].Children[0]);
}

[Fact]
public void ReplaceNodeWithChildrenAndChangeIdentity()
{
var grandchild = this.NewTree("grandchild");
var child = this.NewTree("child", new[] { grandchild });
this.node = this.node.AddChildren(child);

// Ensure we exercise our lookup table update code by filling the tree with enough nodes.
for (int i = 0; i < RecursiveTypeExtensions.InefficiencyLoadThreshold; i++)
{
this.node = this.node.AddChildren(this.NewTree("child " + i));
}

// Verify that we can find the interesting child.
var spine = this.node.GetSpine(child.Identity);
Assert.Same(this.node, spine.Peek());
Assert.Same(child, spine.Pop().Peek());

// Now replace the child with one of a different identity.
var newChild = this.NewTree("newChild", child.Children);
this.node = this.node.ReplaceDescendent(child, newChild);

spine = this.node.GetSpine(newChild.Identity);
Assert.Same(this.node, spine.Peek());
Assert.Same(newChild, spine.Last());

spine = this.node.GetSpine(grandchild.Identity);
Assert.Same(this.node, spine.Peek());
Assert.Same(newChild, spine.Pop().Peek());
Assert.Same(grandchild, spine.Last());
}

[Fact]
public void Contains()
{
var leaf = (ProjectTree)this.NewTree("leaf");
var expectedChain = new List<ProjectTree>();
var head = leaf;
expectedChain.Add(head);
for (int i = 0; i < RecursiveTypeExtensions.InefficiencyLoadThreshold * 3; i++)
{
head = (ProjectTree)this.NewTree("step " + (i + 1), children: new[] { head });
expectedChain.Insert(0, head);
}

for (int i = 0; i < expectedChain.Count - 1; i++)
{
Assert.True(expectedChain[i].HasDescendent(expectedChain.Last().Identity));
Assert.False(expectedChain[i].HasDescendent(this.NewTree("missing").Identity));
}
}

[Fact]
public void Validate()
{
var node1 = this.NewNode();
var node2 = this.NewNode(node1);
RecursiveTypeExtensions.LookupTable<ProjectTree, ProjectTree>.ValidateInternalIntegrity(node1);
RecursiveTypeExtensions.LookupTable<ProjectTree, ProjectTree>.ValidateInternalIntegrity(node2);
Assert.Throws<RecursiveChildNotUniqueException>(() =>
{
var cycle = node1.AddChildren(node2);
RecursiveTypeExtensions.LookupTable<ProjectTree, ProjectTree>.ValidateInternalIntegrity(cycle);
});
}

[Fact]
public void GetSpine()
{
var path = this.node.GetSpine(this.node.Identity).ToList();
Assert.Single(path);
Assert.Same(this.node, path[0]);

ProjectTree child;
var parent = this.node.AddChildren(child = this.NewTree("child"));
path = parent.GetSpine(child.Identity).ToList();
Assert.Equal(2, path.Count);
Assert.Same(parent, path[0]);
Assert.Same(child, path[1]);

var leaf = this.NewTree("leaf");
for (int steps = 0; steps < RecursiveTypeExtensions.InefficiencyLoadThreshold * 3; steps++)
{
var expectedChain = new List<ProjectTree>(steps + 1);
var head = leaf;
expectedChain.Add(head);
for (int i = 0; i < steps; i++)
{
head = (ProjectTree)this.NewTree("step " + (i + 1), head);
expectedChain.Insert(0, head);
}

// Find every single node along the chain from the head to the tail.
for (int i = 0; i <= steps; i++)
{
var actualChain = head.GetSpine(expectedChain[i].Identity).ToList();
Assert.Equal(expectedChain.Take(i + 1).Select(n => n.Identity).ToList(), actualChain.Select(n => n.Identity).ToList());
}

// Now find the tail from every node, starting at the tail to get code coverage on the inner-nodes' lazy search-building capabilities.
for (int i = steps; i >= 0; i--)
{
var actualChain = expectedChain[i].GetSpine(expectedChain.Last().Identity).ToList();
Assert.Equal(expectedChain.Skip(i).Select(n => n.Identity).ToList(), actualChain.Select(n => n.Identity).ToList());
}

// And test searches for non-related nodes.
Assert.True(head.GetSpine(this.node.Identity).IsEmpty);
}
}

[Fact]
public void AsProjectItemTree_OnNonItem()
{
var root = ProjectTree.Create("hi").AsRoot;
var item = root.AsProjectItemTree;
Assert.True(item.IsDefault);
}

[Fact]
public void FindThrowsOnMissingNode()
{
var child = this.NewTree("child");
Assert.Throws<KeyNotFoundException>(() => this.node.Find(child.Identity));
}

[Fact]
public void EqualsTest()
{
Assert.False(this.node.Equals((object)null));
Assert.False(this.node.Equals((ProjectTree)null));
Assert.True(this.node.Equals((object)this.node));
Assert.True(this.node.Equals((ProjectTree)this.node));
}

[Fact]
public void GetHashCodeTest()
{
var newNode = this.node.WithCaption("some caption");
Assert.NotEqual(this.node.GetHashCode(), newNode.GetHashCode());
}

[Fact]
public void EqualsIdentityComparerTest()
{
var newNode = this.node.WithCaption("some caption");
Assert.NotEqual(this.node, newNode, EqualityComparer<ProjectTree>.Default);
Assert.Equal(this.node, newNode, ProjectTree.Comparers.Identity);
}

[Fact]
public void GetHashCodeIdentityComparerTest()
{
var newNode = this.node.WithCaption("some caption");
Assert.Equal(ProjectTree.Comparers.Identity.GetHashCode(this.node), ProjectTree.Comparers.Identity.GetHashCode(newNode));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//-----------------------------------------------------------------------
// <copyright file="ProjectTreeNodeTestBase.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;

/// <summary>
/// A base class for testing of red and green trees.
/// </summary>
public abstract class ProjectTreeNodeTestBase : IDisposable
{
internal const string Caption = "some caption.txt";

internal const string ModifiedCaption = "some other caption.txt";

internal const string ItemType = "itemType";

internal const string ItemName = "some item name.txt";

private int nodeCounter;

internal ImmutableSortedSet<ProjectTree> Children { get; set; }

public ProjectTreeNodeTestBase()
{
this.nodeCounter = 0;
this.Children = ImmutableSortedSet.Create(ProjectTreeSort.Default);
}

public void Dispose()
{
this.Cleanup();
}

protected virtual void Cleanup()
{
}

internal ProjectTree NewNode(params ProjectTree[] children)
{
this.nodeCounter++;
var tree = ProjectTree.Create(Caption + this.nodeCounter);
if (children != null)
{
tree = tree.WithChildren(children);
}

return tree;
}

internal ProjectTree NewTree(string caption, ProjectTree singleChild)
{
return this.NewTree(caption, new[] { singleChild });
}

internal ProjectTree NewTree(string caption, IEnumerable<ProjectTree> children = null)
{
var tree = ProjectTree.Create(caption);
if (children != null)
{
tree = tree.WithChildren(children);
}

return tree;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Validation;

[DebuggerDisplay("{Caption} ({Identity})")]
partial class ProjectTree
{
public static IReadOnlyList<DiffGram> GetDelta(ProjectTree before, ProjectTree after)
{
return after.ChangesSince(before);
}

static partial void CreateDefaultTemplate(ref ProjectTree.Template template)
{
template.Children = ImmutableSortedSet.Create(ProjectTreeSort.Default);
template.Capabilities = ImmutableHashSet.Create<string>(StringComparer.OrdinalIgnoreCase);
template.Visible = true;
}

public override string ToString()
{
return this.Caption;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//-----------------------------------------------------------------------
// <copyright file="ProjectTreeSort.cs" company="Microsoft">
// Copyright (c) Microsoft. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Validation;

/// <summary>
/// Standard sort routine(s) for project tree nodes.
/// </summary>
internal class ProjectTreeSort : IComparer<ProjectTree>
{
/// <summary>
/// Backing field for the <see cref="Default"/> property.
/// </summary>
private static IComparer<ProjectTree> defaultInstance = new ProjectTreeSort();

/// <summary>
/// Initializes a new instance of the <see cref="ProjectTreeSort"/> class.
/// </summary>
private ProjectTreeSort()
{
}

/// <summary>
/// Gets the default sorting algorithm, which sorts alphabetically, puts folders on top, and any special folders or items above that.
/// </summary>
internal static IComparer<ProjectTree> Default
{
get { return defaultInstance; }
}

/// <summary>
/// Compares two objects and returns a value indicating whether one is less than,
/// equal to, or greater than the other.
/// </summary>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
/// <returns>Standard -1, 0, 1 sorting responses.</returns>
public int Compare(ProjectTree x, ProjectTree y)
{
Requires.NotNull(x, "x");
Requires.NotNull(y, "y");

// Start by putting "special" folders on top.
bool xUp = x.Capabilities.Contains(ProjectTreeCapabilities.BubbleUp);
bool yUp = y.Capabilities.Contains(ProjectTreeCapabilities.BubbleUp);
if (xUp ^ yUp)
{
return xUp ? -1 : 1;
}

// Then folders should appear above files.
bool xFolder = x.Capabilities.Contains(ProjectTreeCapabilities.Folder);
bool yFolder = y.Capabilities.Contains(ProjectTreeCapabilities.Folder);
if (xFolder ^ yFolder)
{
return xFolder ? -1 : 1;
}

// Finally, sort alphabetically. Notice we use CurrentCulture here to get the right effect when sorting.
int cultureIgnoreCaseSort = StringComparer.CurrentCultureIgnoreCase.Compare(x.Caption, y.Caption);
if (cultureIgnoreCaseSort == 0)
{
// The rule is that two items are equivalent in this tree only if they are ordinal-equivalent,
// not just culture-equivalent. For instance if one inserted an item with a Turkish ı and English I
// (for instance fıle.txt and fIle.txt), such items should be considered distinct, but a culture
// comparison would return equivalence. This causes problems as code to check for existence of items
// in the tree uses explicit ordinal comparisons, but when an item is inserted, the comparison in this
// method is used to validate the tree's state.
return StringComparer.OrdinalIgnoreCase.Compare(x.Caption, y.Caption);
}

return cultureIgnoreCaseSort;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Validation;
using Xunit;
using Xunit.Abstractions;

public class ProjectTreeTests
{
private readonly ITestOutputHelper logger;

public ProjectTreeTests(ITestOutputHelper logger)
{
Requires.NotNull(logger, nameof(logger));
this.logger = logger;
}

[Fact]
public void ProjectTreeValueComparer()
{
var tree = ProjectTree.Create("root");
Assert.Equal(tree, tree, ProjectTree.Comparers.Identity);
Assert.Equal(tree, tree, ProjectTree.Comparers.ByValue);

var newPath = tree.WithFilePath("c:\\some\\path");
Assert.Equal(tree, newPath, ProjectTree.Comparers.Identity);
Assert.NotEqual(tree, newPath, ProjectTree.Comparers.ByValue);

var changedBackToOriginal = newPath.WithFilePath(tree.FilePath);
Assert.Equal(tree, changedBackToOriginal, ProjectTree.Comparers.Identity);
Assert.Equal(tree, changedBackToOriginal, ProjectTree.Comparers.ByValue);

var derivedType = tree.ToProjectItemTree(new ProjectPropertiesContext());
Assert.Equal(tree, derivedType, ProjectTree.Comparers.Identity);
Assert.NotEqual(tree, derivedType, ProjectTree.Comparers.ByValue);
}

[Fact]
public void ProjectTreeChangesSinceLargeTreeTest()
{
int seed = Environment.TickCount;
this.logger.WriteLine("Random seed: {0}", seed);
var random = new Random(seed);
var largeTree = ConstructVeryLargeTree(random, 4, 500, 10000);
int actualSize = largeTree.ProjectTree.GetSelfAndDescendents().Count();
this.logger.WriteLine("Total tree size: {0} nodes", actualSize);
IRecursiveParent lt = largeTree.ProjectTree;
//lt.Write(Console.Out);

// Pick one random node to change.
var changedNodeIdentity = ((uint)random.Next(actualSize)) + largeTree.Identity;
var originalNode = largeTree.Find(changedNodeIdentity);
var changedNode = originalNode.WithCaption("Changed!");

// Now diff the two versions.
var changes = largeTree.ChangesSince(changedNode.Root);
Assert.Equal(1, changes.Count);
Assert.Equal(ChangeKind.Replaced, changes[0].Kind);
Assert.Equal(ProjectTreeChangedProperties.Caption, changes[0].Changes & ~ProjectTreeChangedProperties.PositionUnderParent);
Assert.Equal(changedNodeIdentity, changes[0].Identity);
}

[Fact]
public void ConstructLargeTreeOptimal()
{
this.ConstructLargeTreeOptimalHelper(null, 1000);
}

/// <summary>
/// Constructs a tree based on a template in top down order (root to leaf),
/// instead of the optimal leaf to roof order.
/// </summary>
[Fact]
public void ConstructLargeTreeSuboptimalWithoutBuilder()
{
this.CloneProjectTreeRootToLeafWithoutBuilders(ConstructLargeTreeOptimalHelper());
}

/// <summary>
/// Constructs a tree based on a template in top down order (root to leaf),
/// instead of the optimal leaf to roof order.
/// </summary>
[Fact]
public void ConstructLargeTreeSuboptimalUsingBuilder()
{
this.CloneProjectTreeRootToLeafWithBuilders(ConstructLargeTreeOptimalHelper());
}

internal RootedProjectTree ConstructLargeTreeOptimalHelper(int? seed = null, int maxSize = 1000)
{
int randSeed = seed.HasValue ? seed.Value : Environment.TickCount;
this.logger.WriteLine("Random seed: {0}", randSeed);
var random = new Random(randSeed);
return ConstructVeryLargeTree(random, 4, 100, maxSize);
}

internal RootedProjectTree CloneProjectTreeLeafToRoot(RootedProjectTree templateTree)
{
var clone = ProjectTree.Create(templateTree.Caption).AddChildren(templateTree.ProjectTree.Children.Select(this.CloneProjectTreeLeafToRoot));
return clone.AsRoot;
}

internal ProjectTree CloneProjectTreeLeafToRoot(ProjectTree templateTree)
{
var clone = ProjectTree.Create(templateTree.Caption).AddChildren(templateTree.Children.Select(this.CloneProjectTreeLeafToRoot));
return clone;
}

internal RootedProjectTree CloneProjectTreeRootToLeafWithoutBuilders(RootedProjectTree templateTree)
{
var root = RootedProjectTree.Create(templateTree.Caption);
var rootWithChildren = RecursiveAddChildren(templateTree.ProjectTree, root);
return rootWithChildren;
}

internal RootedProjectTree CloneProjectTreeRootToLeafWithBuilders(RootedProjectTree templateTree)
{
var rootBuilder = ProjectTree.Create(templateTree.Caption).ToBuilder();
RecursiveAddChildren(templateTree.ProjectTree, rootBuilder);
var root = rootBuilder.ToImmutable();
return root.AsRoot;
}

private static void RecursiveAddChildren(ProjectTree template, ProjectTree.Builder receiver)
{
foreach (var templateChild in template)
{
var clonedTemplateChild = ProjectTree.Create(templateChild.Caption).ToBuilder();
RecursiveAddChildren(templateChild, clonedTemplateChild);
receiver.Children.Add(clonedTemplateChild.ToImmutable());
}
}

private static RootedProjectTree RecursiveAddChildren(ProjectTree template, RootedProjectTree receiver)
{
RootedProjectTree latest = receiver;
foreach (var templateChild in template)
{
var clonedTemplateChild = ProjectTree.Create(templateChild.Caption);
var asChild = latest.AddChild(clonedTemplateChild).Value;
var childWithChildren = RecursiveAddChildren(templateChild, asChild);
latest = childWithChildren.Parent;
}

return latest;
}

internal static RootedProjectTree ConstructVeryLargeTree(Random random, int depth, int maxImmediateChildrenCount, int totalNodeCount, Func<string> counter = null)
{
Requires.NotNull(random, "random");
Requires.Range(depth > 0, "maxDepth");
Requires.Range(totalNodeCount > 0, "totalNodeCount");

if (counter == null)
{
int counterPosition = 0;
counter = () => "Node " + ++counterPosition;
}

var tree = RootedProjectTree.Create(counter());
int nodesAllocated = 1;

int maxChildrenCount = Math.Min(maxImmediateChildrenCount, totalNodeCount - nodesAllocated);
if (depth == 1)
{
tree = tree.AddChildren(Enumerable.Range(1, maxChildrenCount).Select(n => ProjectTree.Create(counter())));
nodesAllocated += maxChildrenCount;
}
else
{
int childrenCount = random.Next(maxChildrenCount) + 1;
int sizePerBranch = (totalNodeCount - nodesAllocated) / childrenCount;
if (sizePerBranch > 0)
{
tree = tree.AddChildren(Enumerable.Range(1, childrenCount).Select(n => ConstructVeryLargeTree(random, depth - 1, maxImmediateChildrenCount, sizePerBranch, counter).ProjectTree));
}
}

return tree;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xunit;

public class RequiresAndHierarchyTests
{
[Fact]
public void RequiredFieldNotDeclaredFirst()
{
ReqAndHierL1 value = ReqAndHierL1.Create("value2");
Assert.Null(value.L1Field1);
Assert.Equal("value2", value.L1Field2);
}

[Fact]
public void RequiredFieldsAppearInMultipleRelatedTypes()
{
ReqAndHierL2 value = ReqAndHierL2.Create("l1f2Value", "l2f2Value");
Assert.Equal("l1f2Value", value.L1Field2);
Assert.Equal("l2f2Value", value.L2Field2);
Assert.Null(value.L1Field1);
Assert.Null(value.L2Field1);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
[GenerateImmutable]
partial class ReqAndHierL1
{
readonly string l1Field1;
[Required]
readonly string l1Field2;
}

[GenerateImmutable]
partial class ReqAndHierL2 : ReqAndHierL1
{
readonly string l2Field1;
[Required]
readonly string l2Field2;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

[GenerateImmutable(DefineRootedStruct = true)]
public partial class Tree
{
readonly ImmutableSortedSet<Tree> children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
public class SealedTests
{
[Fact]
public void SealedCreateBuilder()
{
Sealed.CreateBuilder();
}

[Fact]
public void SealedWithBaseCreateBuilder()
{
Sealed_WithBase.CreateBuilder();
}
}
}
20 changes: 20 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/Sealed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
[GenerateImmutable(DefineInterface = true, DefineWithMethodsPerProperty = true, GenerateBuilder = true)]
sealed partial class Sealed
{
readonly string name;
}

[GenerateImmutable(DefineInterface = true, DefineWithMethodsPerProperty = true, GenerateBuilder = true)]
partial class Sealed_UnsealedBase
{
readonly string name;
}

[GenerateImmutable(DefineInterface = true, DefineWithMethodsPerProperty = true, GenerateBuilder = true)]
sealed partial class Sealed_WithBase : Sealed_UnsealedBase
{
readonly int age;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

public class TreeNodeTests
{
[Fact]
public void TreeConstruction()
{
var root = TreeNode.Create("temp", @"c:\temp\").WithChildren(
TreeNode.Create("a.cs", @"c:\temp\a.cs"),
TreeNode.Create("subfolder", @"c:\temp\subfolder\").WithChildren(
TreeNode.Create("g.h", @"c:\temp\subfolder\g.h")));

Assert.Equal("temp", root.Caption);
Assert.Equal("a.cs", root.Children[0].Caption);
Assert.Equal("subfolder", root.Children[1].Caption);
Assert.Equal("g.h", root.Children[1].Children[0].Caption);

var root2 = root.AddChildren(TreeNode.Create("b.cs", @"c:\temp\b.cs"));
Assert.Equal(3, root2.Children.Count);

var root3 = root2.RemoveChildren(root2.Children[0]);
Assert.Equal(2, root3.Children.Count);
Assert.Equal(root2.Children[1], root3.Children[0]);
}
}

[DebuggerDisplay("{FilePath}")]
partial class TreeNode
{
static partial void CreateDefaultTemplate(ref TreeNode.Template template)
{
template.Children = ImmutableList.Create<TreeNode>();
template.Visible = true;
template.Attributes = ImmutableHashSet.Create<string>(StringComparer.OrdinalIgnoreCase);
}

[DebuggerDisplay("{FilePath}")]
partial class Builder
{
}
}
}
14 changes: 14 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/TreeNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System.Collections.Immutable;

[GenerateImmutable(DefineWithMethodsPerProperty = true)]
partial class TreeNode
{
readonly string caption;
readonly string filePath;
readonly bool visible;
readonly ImmutableHashSet<string> attributes;
readonly ImmutableList<TreeNode> children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using ImmutableObjectGraph; // this Using statement should remain
using ImmutableObjectGraph.Generation;

[GenerateImmutable]
partial class SomeClass
{
ImmutableDeque<int> deque; // something to use the ImmutableObjectGraph namespace
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

public class XmlNodeTests
{
[Fact]
public void XmlTreeConstruction()
{
XmlElement root = XmlElement.Create("Root").WithChildren(
XmlElement.Create("Child1"),
XmlElement.Create("Child2"));

XmlElement xe = XmlElement.Create("l1", "n1");
XmlElement xel2 = xe.With("l2");
Assert.Equal("l2", xel2.LocalName);
Assert.Equal("n1", xel2.NamespaceName);
XmlElement xel3n3 = xe.With("l3", "n3");
Assert.Equal("l3", xel3n3.LocalName);
Assert.Equal("n3", xel3n3.NamespaceName);
}

/// <summary>
/// Verifies that the With overloads don't cause compiler errors due to ambiguity.
/// </summary>
[Fact]
public void WithNoArguments()
{
XmlElement e = XmlElement.Create("ln1", "ns1");
XmlElement e2 = e.With();
Assert.Equal("ln1", e2.LocalName);
Assert.Equal("ns1", e2.NamespaceName);
}

[Fact]
public void Polymorphism()
{
XmlElement e = XmlElement.Create("ln1", "ns1");
XmlNode n = e;
XmlNode n2 = n.WithLocalName("newName");
Assert.Equal("newName", n2.LocalName);
XmlElement e2 = (XmlElement)n2;
Assert.Equal(n2.LocalName, e2.LocalName);
Assert.Equal(e.NamespaceName, e2.NamespaceName);
}

[Fact]
public void TypeConversion()
{
XmlElement ordinaryElement = XmlElement.Create("TagName");
Assert.IsNotType<XmlElementWithContent>(ordinaryElement);

// Switch to derived type, without extra data.
XmlElementWithContent elementWithContent = ordinaryElement.ToXmlElementWithContent();
Assert.Equal(ordinaryElement.LocalName, elementWithContent.LocalName);

// Switch to derived type, including extra data.
elementWithContent = ordinaryElement.ToXmlElementWithContent("SomeContent");
Assert.Equal(ordinaryElement.LocalName, elementWithContent.LocalName);
Assert.Equal("SomeContent", elementWithContent.Content);

// Switch back to base type.
XmlElement backAgain = elementWithContent.ToXmlElement();
Assert.IsNotType<XmlElementWithContent>(backAgain);
Assert.Equal(ordinaryElement.LocalName, backAgain.LocalName);
}
}

[DebuggerDisplay("<{LocalName,nq}>")]
partial class XmlElement
{
static partial void CreateDefaultTemplate(ref Template template)
{
template.Children = ImmutableList.Create<XmlNode>();
}
}

[DebuggerDisplay("<{LocalName,nq}>{Content}</{LocalName,nq}>")]
partial class XmlElementWithContent
{
static partial void CreateDefaultTemplate(ref Template template)
{
template.Children = ImmutableList.Create<XmlNode>();
}
}
}
35 changes: 35 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/TestSources/XmlNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace ImmutableObjectGraph.Generation.Tests.TestSources
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

[GenerateImmutable(DefineWithMethodsPerProperty = true)]
abstract partial class XmlNode
{
readonly string localName;
}

[GenerateImmutable(DefineWithMethodsPerProperty = true)]
partial class XmlElement : XmlNode
{
readonly string namespaceName;
readonly ImmutableList<XmlNode> children;
}

[GenerateImmutable(DefineWithMethodsPerProperty = true)]
partial class XmlElementWithContent : XmlElement
{
readonly string content;
}

[GenerateImmutable(DefineWithMethodsPerProperty = true)]
partial class XmlAttribute : XmlNode
{
readonly string namespaceName;
readonly string value;
}
}
6 changes: 6 additions & 0 deletions src/ImmutableObjectGraph.Generation.Tests/app.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="xunit.methodDisplay" value="method" />
</appSettings>
</configuration>
395 changes: 395 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+BuilderGen.cs

Large diffs are not rendered by default.

323 changes: 323 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+CollectionHelpersGen.cs

Large diffs are not rendered by default.

315 changes: 315 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+DeepMutationGen.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
namespace ImmutableObjectGraph.Generation
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data.Entity.Design.PluralizationServices;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Validation;
using LookupTableHelper = RecursiveTypeExtensions.LookupTable<IRecursiveType, IRecursiveParentWithLookupTable<IRecursiveType>>;

public partial class CodeGen
{
protected class DefineWithMethodsPerPropertyGen : FeatureGenerator
{
internal const string WithPropertyMethodPrefix = "With";

public DefineWithMethodsPerPropertyGen(CodeGen generator)
: base(generator)
{
}

public override bool IsApplicable
{
get { return this.generator.options.DefineWithMethodsPerProperty; }
}

protected override void GenerateCore()
{
var valueParameterName = SyntaxFactory.IdentifierName("value");

foreach (var field in this.generator.applyToMetaType.LocalFields)
{
var withPropertyMethod = SyntaxFactory.MethodDeclaration(
GetFullyQualifiedSymbolName(this.generator.applyToSymbol),
WithPropertyMethodPrefix + field.Name.ToPascalCase())
.WithAdditionalAnnotations()
.AddModifiers(
SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAttributeLists(PureAttributeList)
.AddParameterListParameters(
SyntaxFactory.Parameter(valueParameterName.Identifier)
.WithType(GetFullyQualifiedSymbolName(field.Type)))
.WithBody(SyntaxFactory.Block(
SyntaxFactory.IfStatement(
SyntaxFactory.BinaryExpression(
SyntaxKind.EqualsExpression,
valueParameterName,
SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ThisExpression(), field.NameAsField)),
SyntaxFactory.Block(
SyntaxFactory.ReturnStatement(SyntaxFactory.ThisExpression()))),
SyntaxFactory.ReturnStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.ThisExpression(),
WithMethodName),
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(
SyntaxFactory.NameColon(field.Name),
NoneToken,
Syntax.OptionalFor(valueParameterName))))))));

this.innerMembers.Add(withPropertyMethod);
}

foreach (var field in this.generator.applyToMetaType.InheritedFields)
{
string withMethodName = WithPropertyMethodPrefix + field.Name.ToPascalCase();
var withPropertyMethod = SyntaxFactory.MethodDeclaration(
GetFullyQualifiedSymbolName(this.generator.applyToSymbol),
withMethodName)
.AddModifiers(
SyntaxFactory.Token(SyntaxKind.NewKeyword),
SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAttributeLists(PureAttributeList)
.AddParameterListParameters(
SyntaxFactory.Parameter(valueParameterName.Identifier)
.WithType(GetFullyQualifiedSymbolName(field.Type)))
.WithBody(SyntaxFactory.Block(
SyntaxFactory.ReturnStatement(
SyntaxFactory.CastExpression(
GetFullyQualifiedSymbolName(this.generator.applyToSymbol),
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.BaseExpression(),
SyntaxFactory.IdentifierName(withMethodName)),
SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(valueParameterName))))))));

this.innerMembers.Add(withPropertyMethod);
}
}
}
}
}
594 changes: 594 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+DeltaGen.cs

Large diffs are not rendered by default.

Large diffs are not rendered by default.

452 changes: 452 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+FastSpineGen.cs

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+InterfacesGen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace ImmutableObjectGraph.Generation
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data.Entity.Design.PluralizationServices;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Validation;
using LookupTableHelper = RecursiveTypeExtensions.LookupTable<IRecursiveType, IRecursiveParentWithLookupTable<IRecursiveType>>;

public partial class CodeGen
{
protected class InterfacesGen : FeatureGenerator
{
public InterfacesGen(CodeGen generator)
: base(generator)
{
}

public override bool IsApplicable
{
get { return this.generator.options.DefineInterface; }
}

protected override BaseTypeSyntax[] AdditionalApplyToBaseTypes
{
get
{
return new BaseTypeSyntax[] { SyntaxFactory.SimpleBaseType(
SyntaxFactory.IdentifierName("I" + this.generator.applyTo.Identifier.Text)) };
}
}

protected override void GenerateCore()
{
var iface = SyntaxFactory.InterfaceDeclaration(
"I" + this.generator.applyTo.Identifier.Text)
.AddModifiers(GetModifiersForAccessibility(this.generator.applyToSymbol))
.WithMembers(
SyntaxFactory.List<MemberDeclarationSyntax>(
from field in this.generator.applyToMetaType.LocalFields
select SyntaxFactory.PropertyDeclaration(
GetFullyQualifiedSymbolName(field.Type),
field.Name.ToPascalCase())
.WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.SingletonList(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)))))));
if (this.generator.applyToMetaType.HasAncestor)
{
iface = iface.WithBaseList(SyntaxFactory.BaseList(
SyntaxFactory.SingletonSeparatedList<BaseTypeSyntax>(SyntaxFactory.SimpleBaseType(
SyntaxFactory.IdentifierName("I" + this.generator.applyToMetaType.Ancestor.TypeSymbol.Name)))));
}

this.siblingMembers.Add(iface);
}
}
}
}
51 changes: 51 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+RecursiveTypeGen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace ImmutableObjectGraph.Generation
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data.Entity.Design.PluralizationServices;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Validation;
using LookupTableHelper = RecursiveTypeExtensions.LookupTable<IRecursiveType, IRecursiveParentWithLookupTable<IRecursiveType>>;

public partial class CodeGen
{
protected class RecursiveTypeGen : FeatureGenerator
{
public RecursiveTypeGen(CodeGen generator)
: base(generator)
{
}

public override bool IsApplicable
{
get { return this.generator.applyToMetaType.IsRecursiveType; }
}

protected override void GenerateCore()
{
this.baseTypes.Add(SyntaxFactory.SimpleBaseType(Syntax.GetTypeSyntax(typeof(IRecursiveType))));

//// uint IRecursiveType.Identity => this.Identity;
this.innerMembers.Add(SyntaxFactory.PropertyDeclaration(
IdentityFieldTypeSyntax,
nameof(IRecursiveType.Identity))
.WithExplicitInterfaceSpecifier(
SyntaxFactory.ExplicitInterfaceSpecifier(Syntax.GetTypeSyntax(typeof(IRecursiveType))))
.WithExpressionBody(
SyntaxFactory.ArrowExpressionClause(Syntax.ThisDot(IdentityPropertyName)))
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(DebuggerBrowsableNeverAttribute))));
}
}
}
}
1,218 changes: 1,218 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+RootedStructGen.cs

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+StyleCopCompliance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
namespace ImmutableObjectGraph.Generation
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data.Entity.Design.PluralizationServices;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Validation;

public partial class CodeGen
{
protected class StyleCopCompliance : FeatureGenerator
{
public StyleCopCompliance(CodeGen generator) : base(generator)
{
}

public override bool IsApplicable
{
get { return true; }
}

protected override void GenerateCore()
{
}

public override SyntaxList<MemberDeclarationSyntax> ProcessFinalGeneratedResult(SyntaxList<MemberDeclarationSyntax> applyToAndOtherTypes)
{
var result = base.ProcessFinalGeneratedResult(applyToAndOtherTypes);

for (int i = 0; i < result.Count; i++)
{
var member = result[i];
var types = member.DescendantNodesAndSelf(n => n is ClassDeclarationSyntax || n is StructDeclarationSyntax)
.OfType<TypeDeclarationSyntax>()
.ToArray();
var trackingMember = member.TrackNodes(types);

foreach (var type in types)
{
var currentMember = trackingMember.GetCurrentNode(type);
var updatedMember = currentMember;
var cl = currentMember as ClassDeclarationSyntax;
if (cl != null)
{
updatedMember = SortMembers(cl);
}

var str = currentMember as StructDeclarationSyntax;
if (str != null)
{
updatedMember = SortMembers(str);
}

trackingMember = trackingMember.ReplaceNode(currentMember, updatedMember);
}

result = result.Replace(member, trackingMember);
}

return result;
}

private static ClassDeclarationSyntax SortMembers(ClassDeclarationSyntax type)
{
var innerMembers = type.Members.ToList();
innerMembers.Sort(StyleCop.Sort);
type = type.WithMembers(SyntaxFactory.List(innerMembers));
return type;
}

private static StructDeclarationSyntax SortMembers(StructDeclarationSyntax type)
{
var innerMembers = type.Members.ToList();
innerMembers.Sort(StyleCop.Sort);
type = type.WithMembers(SyntaxFactory.List(innerMembers));
return type;
}
}
}
}
261 changes: 261 additions & 0 deletions src/ImmutableObjectGraph.Generation/CodeGen+TypeConversionGen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
namespace ImmutableObjectGraph.Generation
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Data.Entity.Design.PluralizationServices;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Validation;
using LookupTableHelper = RecursiveTypeExtensions.LookupTable<IRecursiveType, IRecursiveParentWithLookupTable<IRecursiveType>>;

public partial class CodeGen
{
protected class TypeConversionGen : FeatureGenerator
{
private static readonly IdentifierNameSyntax CreateWithIdentityMethodName = SyntaxFactory.IdentifierName("CreateWithIdentity");

public TypeConversionGen(CodeGen generator)
: base(generator)
{
}

public override bool IsApplicable
{
get { return true; }
}

protected override void GenerateCore()
{
if (!this.generator.applyToSymbol.IsAbstract && (this.generator.applyToMetaType.HasAncestor || this.generator.applyToMetaType.Descendents.Any()))
{
this.innerMembers.Add(this.CreateCreateWithIdentityMethod());
}

if (this.generator.applyToMetaType.HasAncestor && !this.generator.applyToMetaType.Ancestor.TypeSymbol.IsAbstract)
{
this.innerMembers.Add(this.CreateToAncestorTypeMethod());
}

// Only generate derived type conversion methods if we have something to add:
// either because we're the most base class, or because we have fields to add.
if (this.generator.applyToMetaType.LocalFields.Any() || !this.generator.applyToMetaType.Ancestors.Any())
{
foreach (MetaType derivedType in this.generator.applyToMetaType.Descendents.Where(d => !d.TypeSymbol.IsAbstract))
{
this.innerMembers.Add(this.CreateToDerivedTypeMethod(derivedType));

if (this.generator.applyToMetaType.LocalFields.Any())
{
foreach (MetaType ancestor in this.generator.applyToMetaType.Ancestors.Where(a => a.LocalFields.Any()))
{
this.innerMembers.Add(this.CreateToDerivedTypeOverrideMethod(derivedType, ancestor));
}
}
}
}
}

internal static IdentifierNameSyntax GetToTypeMethodName(string typeName)
{
return SyntaxFactory.IdentifierName("To" + typeName);
}

private MemberDeclarationSyntax CreateCreateWithIdentityMethod()
{
ExpressionSyntax returnExpression = DefaultInstanceFieldName;
if (this.generator.applyToMetaType.LocalFields.Any())
{
returnExpression = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
returnExpression,
WithFactoryMethodName),
this.generator.CreateArgumentList(this.generator.applyToMetaType.AllFields, ArgSource.OptionalArgumentOrTemplate, OptionalStyle.Always)
.AddArguments(OptionalIdentityArgument));
}

var method = SyntaxFactory.MethodDeclaration(
this.generator.applyToTypeName,
CreateWithIdentityMethodName.Identifier)
.AddModifiers(
SyntaxFactory.Token(SyntaxKind.InternalKeyword),
SyntaxFactory.Token(SyntaxKind.StaticKeyword))
.WithParameterList(
this.generator.CreateParameterList(
this.generator.applyToMetaType.AllFields,
ParameterStyle.OptionalOrRequired)
.AddParameters(OptionalIdentityParameter))
.WithBody(SyntaxFactory.Block(
SyntaxFactory.IfStatement(
SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, Syntax.OptionalIsDefined(IdentityParameterName)),
SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentityParameterName,
SyntaxFactory.InvocationExpression(NewIdentityMethodName, SyntaxFactory.ArgumentList())))),
SyntaxFactory.ReturnStatement(returnExpression)));

// BUG: the condition should be if there are local fields on *any* ancestor
// from the closest non-abstract ancestor (exclusive) to this type (inclusive).
if (!this.generator.applyToMetaType.LocalFields.Any() && this.generator.applyToMetaType.Ancestors.Any(a => !a.TypeSymbol.IsAbstract))
{
method = Syntax.AddNewKeyword(method);
}

return method;
}

private MemberDeclarationSyntax CreateToAncestorTypeMethod()
{
var ancestor = this.generator.applyToMetaType.Ancestor;
var ancestorType = GetFullyQualifiedSymbolName(ancestor.TypeSymbol);
return SyntaxFactory.MethodDeclaration(
ancestorType,
GetToTypeMethodName(this.generator.applyToMetaType.Ancestor.TypeSymbol.Name).Identifier)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAttributeLists(PureAttributeList)
.WithBody(SyntaxFactory.Block(
SyntaxFactory.ReturnStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ancestorType,
CreateWithIdentityMethodName),
this.generator.CreateArgumentList(ancestor.AllFields, asOptional: OptionalStyle.WhenNotRequired)
.AddArguments(RequiredIdentityArgumentFromProperty)))));
}

private MemberDeclarationSyntax CreateToDerivedTypeMethod(MetaType derivedType)
{
var derivedTypeName = GetFullyQualifiedSymbolName(derivedType.TypeSymbol);
var thatLocal = SyntaxFactory.IdentifierName("that");
var body = new List<StatementSyntax>();

// var that = this as DerivedType;
body.Add(SyntaxFactory.LocalDeclarationStatement(
SyntaxFactory.VariableDeclaration(
varType,
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.VariableDeclarator(thatLocal.Identifier)
.WithInitializer(SyntaxFactory.EqualsValueClause(
SyntaxFactory.BinaryExpression(
SyntaxKind.AsExpression,
SyntaxFactory.ThisExpression(),
derivedTypeName)))))));

// this.GetType()
var thisDotGetType = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.ThisExpression(), SyntaxFactory.IdentifierName("GetType")),
SyntaxFactory.ArgumentList());

// {0}.Equals(typeof(derivedType))
var thisTypeIsEquivalentToDerivedType =
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
thisDotGetType,
SyntaxFactory.IdentifierName(nameof(Type.Equals))),
SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(
SyntaxFactory.TypeOfExpression(derivedTypeName)))));

var ifEquivalentTypeBlock = new List<StatementSyntax>();
var fieldsBeyond = derivedType.GetFieldsBeyond(this.generator.applyToMetaType);
if (fieldsBeyond.Any())
{
Func<MetaField, ExpressionSyntax> isUnchanged = v =>
SyntaxFactory.ParenthesizedExpression(
v.IsRequired
? // ({0} == that.{1})
SyntaxFactory.BinaryExpression(
SyntaxKind.EqualsExpression,
v.NameAsField,
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
thatLocal,
v.NameAsProperty))
: // (!{0}.IsDefined || {0}.Value == that.{1})
SyntaxFactory.BinaryExpression(
SyntaxKind.LogicalOrExpression,
SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, Syntax.OptionalIsDefined(v.NameAsField)),
SyntaxFactory.BinaryExpression(
SyntaxKind.EqualsExpression,
Syntax.OptionalValue(v.NameAsField),
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
thatLocal,
v.NameAsProperty))));
var noChangesExpression = fieldsBeyond.Select(isUnchanged).ChainBinaryExpressions(SyntaxKind.LogicalAndExpression);

ifEquivalentTypeBlock.Add(SyntaxFactory.IfStatement(
noChangesExpression,
SyntaxFactory.ReturnStatement(thatLocal)));
}
else
{
ifEquivalentTypeBlock.Add(SyntaxFactory.ReturnStatement(thatLocal));
}

// if (that != null && this.GetType().IsEquivalentTo(typeof(derivedType))) { ... }
body.Add(SyntaxFactory.IfStatement(
SyntaxFactory.BinaryExpression(
SyntaxKind.LogicalAndExpression,
SyntaxFactory.BinaryExpression(SyntaxKind.NotEqualsExpression, thatLocal, SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)),
thisTypeIsEquivalentToDerivedType),
SyntaxFactory.Block(ifEquivalentTypeBlock)));

// return DerivedType.CreateWithIdentity(...)
body.Add(SyntaxFactory.ReturnStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
derivedTypeName,
CreateWithIdentityMethodName),
this.generator.CreateArgumentList(this.generator.applyToMetaType.AllFields, asOptional: OptionalStyle.WhenNotRequired)
.AddArguments(RequiredIdentityArgumentFromProperty)
.AddArguments(this.generator.CreateArgumentList(fieldsBeyond, ArgSource.Argument).Arguments.ToArray()))));

return SyntaxFactory.MethodDeclaration(
derivedTypeName,
GetToTypeMethodName(derivedType.TypeSymbol.Name).Identifier)
.AddModifiers(
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.VirtualKeyword))
.AddAttributeLists(PureAttributeList)
.WithParameterList(this.generator.CreateParameterList(fieldsBeyond, ParameterStyle.OptionalOrRequired))
.WithBody(SyntaxFactory.Block(body));
}

private MemberDeclarationSyntax CreateToDerivedTypeOverrideMethod(MetaType derivedType, MetaType ancestor)
{
var derivedTypeName = GetFullyQualifiedSymbolName(derivedType.TypeSymbol);
return SyntaxFactory.MethodDeclaration(
derivedTypeName,
GetToTypeMethodName(derivedType.TypeSymbol.Name).Identifier)
.AddModifiers(
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
SyntaxFactory.Token(SyntaxKind.OverrideKeyword))
.AddAttributeLists(PureAttributeList)
.WithParameterList(
this.generator.CreateParameterList(derivedType.GetFieldsBeyond(ancestor), ParameterStyle.OptionalOrRequired))
.WithBody(SyntaxFactory.Block(
// return base.ToDerivedType(args);
SyntaxFactory.ReturnStatement(
SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.BaseExpression(),
GetToTypeMethodName(derivedType.TypeSymbol.Name)),
this.generator.CreateArgumentList(this.generator.applyToMetaType.GetFieldsBeyond(ancestor), ArgSource.OptionalArgumentOrPropertyExceptWhenRequired, OptionalStyle.WhenNotRequired)
.AddArguments(this.generator.CreateArgumentList(derivedType.GetFieldsBeyond(this.generator.applyToMetaType), ArgSource.Argument).Arguments.ToArray())))));
}
}
}
}