diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 0dd8a9b..9468742 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -22,7 +22,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore .NET dependencies working-directory: ./src @@ -48,7 +48,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - uses: actions/download-artifact@v4 with: @@ -68,7 +68,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - uses: actions/download-artifact@master with: diff --git a/.gitignore b/.gitignore index 59589dc..12d6472 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,6 @@ /src/NodeDev.Core.Types.Tests/bin /src/NodeDev.Core.Types.Tests/obj /src/NodeDev.Core.Types/bin -/src/NodeDev.EndToEndTests/bin/Debug/net8.0 +/src/NodeDev.EndToEndTests/bin /src/NodeDev.EndToEndTests/obj /src/NodeDev.Blazor.Server/project_backup.json \ No newline at end of file diff --git a/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs b/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs index b89a934..a7f3355 100644 --- a/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs +++ b/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs @@ -15,10 +15,11 @@ using System.Reactive.Linq; using NodeDev.Core.Class; using NodeDev.Blazor.Services; +using NodeDev.Blazor.Services.GraphManager; namespace NodeDev.Blazor.Components; -public partial class GraphCanvas : ComponentBase, IDisposable +public partial class GraphCanvas : ComponentBase, IDisposable, IGraphCanvas { [Parameter, EditorRequired] public Graph Graph { get; set; } = null!; @@ -29,6 +30,9 @@ public partial class GraphCanvas : ComponentBase, IDisposable [Inject] internal DebuggedPathService DebuggedPathService { get; set; } = null!; + private GraphManagerService? _GraphManagerService; + private GraphManagerService GraphManagerService => _GraphManagerService ??= new GraphManagerService(this); + private int PopupX = 0; private int PopupY = 0; private Vector2 PopupNodePosition; @@ -67,7 +71,6 @@ protected override void OnInitialized() Diagram.Links.Added += x => OnConnectionAdded(x, false); Diagram.Links.Removed += OnConnectionRemoved; Diagram.SelectionChanged += SelectionChanged; - } #endregion @@ -138,7 +141,7 @@ private void OnGraphChangedFromCore((Graph, bool) _) #region UpdateConnectionType - private void UpdateConnectionType(Connection connection) + public void UpdatePortColor(Connection connection) { var node = Diagram.Nodes.OfType().FirstOrDefault(x => x.Node == connection.Parent); if (node == null) @@ -155,16 +158,6 @@ private void UpdateConnectionType(Connection connection) #endregion - #region UpdateNodeBaseInfo - - private void UpdateNodeBaseInfo(Node node) - { - var nodeModel = Diagram.Nodes.OfType().First(x => x.Node == node); - nodeModel.UpdateNodeBaseInfo(node); - } - - #endregion - #region UpdateNodes private void UpdateNodes(IEnumerable nodes) @@ -239,7 +232,7 @@ private void OnConnectionUpdated(BaseLinkModel baseLinkModel, Anchor old, Anchor { DisableConnectionUpdate = true; var old = baseLinkModel.Source; - baseLinkModel.SetSource(baseLinkModel.Target); + baseLinkModel.SetSource(baseLinkModel.Target); // this is necessary as everything assumes that the source is an output and vice versa baseLinkModel.SetTarget(old); DisableConnectionUpdate = false; @@ -248,47 +241,7 @@ private void OnConnectionUpdated(BaseLinkModel baseLinkModel, Anchor old, Anchor destination = tmp; } - Graph.Connect(source.Connection, destination.Connection, false); - - // we're plugging something something with a generic into something without a generic - if (source.Connection.Type.HasUndefinedGenerics && !destination.Connection.Type.HasUndefinedGenerics) - { - if (source.Connection.Type.IsAssignableTo(destination.Connection.Type, out var newTypes) && newTypes.Count != 0) - { - PropagateNewGeneric(source.Connection.Parent, newTypes, false); - } - } - else if (destination.Connection.Type.HasUndefinedGenerics && !source.Connection.Type.HasUndefinedGenerics) - { - if (source.Connection.Type.IsAssignableTo(destination.Connection.Type, out var newTypes) && newTypes.Count != 0) - { - PropagateNewGeneric(destination.Connection.Parent, newTypes, false); - } - } - - // we have to remove the textbox ? - if (destination.Connection.Connections.Count == 1 && destination.Connection.Type.AllowTextboxEdit) - UpdateConnectionType(destination.Connection); - - if (baseLinkModel is LinkModel link && link.Source.Model is GraphPortModel sourcePort && link.Target.Model is GraphPortModel targetPort) - { - link.Color = GetTypeShapeColor(sourcePort.Connection.Type, sourcePort.Connection.Parent.TypeFactory); - // we have to disconnect the previously connected exec, since exec outputs can only have one connection - if (source.Connection.Type.IsExec && source.Connection.Connections.Count > 1) - { - Diagram.Links.Remove(Diagram.Links.First(x => (x.Source.Model as GraphPortModel)?.Connection == source.Connection && (x.Target.Model as GraphPortModel)?.Connection != targetPort.Connection)); - Graph.Disconnect(source.Connection, source.Connection.Connections.First(x => x != targetPort.Connection), false); - } - else if (!destination.Connection.Type.IsExec && destination.Connection.Connections.Count > 1) - { - Diagram.Links.Remove(Diagram.Links.First(x => (x.Source.Model as GraphPortModel)?.Connection != source.Connection && (x.Target.Model as GraphPortModel)?.Connection == targetPort.Connection)); - Graph.Disconnect(destination.Connection, destination.Connection.Connections.First(x => x != sourcePort.Connection), false); - } - } - - UpdateVerticesInConnection(source.Connection, destination.Connection, baseLinkModel); - - Graph.RaiseGraphChanged(true); + GraphManagerService.AddNewConnectionBetween(source.Connection, destination.Connection); }); } @@ -318,6 +271,11 @@ public void OnConnectionAdded(BaseLinkModel baseLinkModel, bool force) } } + /// + /// Return the output connection except for execs, in that case we return the input connection. + /// This is because vertices are stored for the port, and execs conveniently only have one output connection while other types only have one input connection. + /// + /// private Connection GetConnectionContainingVertices(Connection source, Connection destination) { if (source.Type.IsExec) // execs can only have one connection, therefor they always contains the vertex information @@ -380,7 +338,7 @@ public void OnConnectionRemoved(BaseLinkModel baseLinkModel) // We have to add back the textbox editor if (destination.Connections.Count == 0 && destination.Type.AllowTextboxEdit) - UpdateConnectionType(destination); + UpdatePortColor(destination); UpdateVerticesInConnection(source, destination, baseLinkModel); } @@ -433,11 +391,13 @@ private void OnNewNodeTypeSelected(NodeProvider.NodeSearchResult searchResult) Diagram.Batch(() => { + CreateGraphNodeModel(node); + if (PopupNodeConnection != null && PopupNode != null) { // check if the source was an input or output and choose the proper destination based on that List sources, destinations; - bool isPopupNodeInput = PopupNode.Inputs.Contains(PopupNodeConnection); + bool isPopupNodeInput = PopupNodeConnection.IsInput; if (isPopupNodeInput) { sources = PopupNode.Inputs; @@ -453,43 +413,19 @@ private void OnNewNodeTypeSelected(NodeProvider.NodeSearchResult searchResult) if (PopupNodeConnection.Type is UndefinedGenericType) // can connect to anything except exec destination = destinations.FirstOrDefault(x => !x.Type.IsExec); else // can connect to anything that is assignable to the type - destination = destinations.FirstOrDefault(x => PopupNodeConnection.Type.IsAssignableTo(x.Type, out _) || (x.Type is UndefinedGenericType && !PopupNodeConnection.Type.IsExec)); + destination = destinations.FirstOrDefault(x => PopupNodeConnection.Type.IsAssignableTo(x.Type, out _, out _) || (x.Type is UndefinedGenericType && !PopupNodeConnection.Type.IsExec)); // if we found a connection, connect them together if (destination != null) { - Graph.Connect(PopupNodeConnection, destination, false); - - if (destination.Connections.Count == 1 && destination.Type.AllowTextboxEdit) - UpdateConnectionType(destination); - if (PopupNodeConnection.Connections.Count == 1 && PopupNodeConnection.Type.AllowTextboxEdit) - UpdateConnectionType(PopupNodeConnection); - var source = isPopupNodeInput ? destination : PopupNodeConnection; var target = isPopupNodeInput ? PopupNodeConnection : destination; - // check if we need to propagate some generic - if (!destination.Type.IsExec && source.Type.IsAssignableTo(target.Type, out var changedGenerics)) - { - PropagateNewGeneric(node, changedGenerics, false); - PropagateNewGeneric(destination.Parent, changedGenerics, false); - } - else if (source.Type.IsExec && source.Connections.Count > 1) // check if we have to disconnect the previously connected exec - { - Diagram.Links.Remove(Diagram.Links.First(x => (x.Source.Model as GraphPortModel)?.Connection == source && (x.Target.Model as GraphPortModel)?.Connection != target)); - var toRemove = source.Connections.FirstOrDefault(x => x != target); - if (toRemove != null) - Graph.Disconnect(source, toRemove, false); - } + GraphManagerService.AddNewConnectionBetween(source, target); } } CancelPopup(); - - CreateGraphNodeModel(node); - AddNodeLinks(node, false); - - UpdateNodes(Graph.Nodes.Values.ToList()); }); } @@ -513,9 +449,7 @@ private void OnNewOverloadSelected(Node.AlternateOverload overload) if (PopupNode == null) return; - PopupNode.SelectOverload(overload, out var newConnections, out var removedConnections); - - Graph.MergedRemovedConnectionsWithNewConnections(newConnections, removedConnections); + GraphManagerService.SelectNodeOverload(PopupNode, overload); CancelPopup(); } @@ -525,9 +459,9 @@ private void OnNewOverloadSelected(Node.AlternateOverload overload) #region OnGenericTypeSelectionMenuAsked private bool IsShowingGenericTypeSelection = false; - private UndefinedGenericType? GenericTypeSelectionMenuGeneric; + private string? GenericTypeSelectionMenuGeneric; - public void OnGenericTypeSelectionMenuAsked(GraphNodeModel nodeModel, UndefinedGenericType undefinedGenericType) + public void OnGenericTypeSelectionMenuAsked(GraphNodeModel nodeModel, string undefinedGenericType) { PopupNode = nodeModel.Node; var p = Diagram.GetScreenPoint(nodeModel.Position.X, nodeModel.Position.Y) - Diagram.Container!.NorthWest; @@ -544,7 +478,7 @@ private void OnGenericTypeSelected(TypeBase type) if (PopupNode == null || GenericTypeSelectionMenuGeneric == null) return; - PropagateNewGeneric(PopupNode, new Dictionary() { [GenericTypeSelectionMenuGeneric] = type }, false); + GraphManagerService.PropagateNewGeneric(PopupNode, new Dictionary() { [GenericTypeSelectionMenuGeneric] = type }, false, null, overrideInitialTypes: true); // Prefer updating the nodes directly instead of calling Graph.RaiseGraphChanged(true) to be sure it is called as soon as possible UpdateNodes(Graph.Nodes.Values.ToList()); @@ -552,34 +486,6 @@ private void OnGenericTypeSelected(TypeBase type) CancelPopup(); } - private void PropagateNewGeneric(Node node, IReadOnlyDictionary changedGenerics, bool requireUIRefresh) - { - foreach (var port in node.InputsAndOutputs) // check if any of the ports have the generic we just solved - { - if (port.Type.GetUndefinedGenericTypes().Any(changedGenerics.ContainsKey)) - { - var isPortInput = node.Inputs.Contains(port); - - port.UpdateType(port.Type.ReplaceUndefinedGeneric(changedGenerics)); - - UpdateConnectionType(port); - - // check if other connections had their own generics and if we just solved them - foreach (var other in port.Connections.ToList()) - { - var source = isPortInput ? other : port; - var target = isPortInput ? port : other; - if (source.Type.IsAssignableTo(target.Type, out var changedGenerics2) && changedGenerics2.Count != 0) - PropagateNewGeneric(other.Parent, changedGenerics2, requireUIRefresh); - else if ((changedGenerics2?.Count ?? 0) != 0)// damn, looks like changing the generic made it so we can't link to this connection anymore - Graph.Disconnect(port, other, false); // no need to refresh UI here as it'll already be refresh at the end of this method - } - } - } - - Graph.RaiseGraphChanged(requireUIRefresh); - } - #endregion #region OnTextboxValueChanged @@ -664,6 +570,26 @@ private void CancelPopup() #endregion + #region RemoveLink + + public void RemoveLinkFromGraphCanvas(Connection source, Connection destination) + { + Graph.Invoke(() => + { + DisableConnectionUpdate = true; + try + { + Diagram.Links.Remove(Diagram.Links.First(x => (x.Source.Model as GraphPortModel)?.Connection == source && (x.Target.Model as GraphPortModel)?.Connection == destination)); + } + finally + { + DisableConnectionUpdate = false; + } + }); + } + + #endregion + #region CreateGraphNodeModel private void CreateGraphNodeModel(Node node) diff --git a/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs b/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs index 7147b9c..3c5fbf0 100644 --- a/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs +++ b/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs @@ -24,11 +24,6 @@ public class GraphNodeModel : NodeModel public GraphPortModel GetPort(Connection connection) => Ports.OfType().First( x=> x.Connection == connection); - internal void UpdateNodeBaseInfo(Node node) - { - - } - internal void OnNodeExecuted(Connection exec) { diff --git a/src/NodeDev.Blazor/DiagramsModels/GraphNodeWidget.razor b/src/NodeDev.Blazor/DiagramsModels/GraphNodeWidget.razor index 596e953..9e8e670 100644 --- a/src/NodeDev.Blazor/DiagramsModels/GraphNodeWidget.razor +++ b/src/NodeDev.Blazor/DiagramsModels/GraphNodeWidget.razor @@ -16,7 +16,7 @@ < @foreach (var undefinedGeneric in undefinedGenerics) { - @(undefinedGeneric.Name + (undefinedGeneric == undefinedGenerics[^1] ? "" : ", ")) + @(undefinedGeneric + (undefinedGeneric == undefinedGenerics[^1] ? "" : ", ")) } > } diff --git a/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs b/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs index 9489c93..57681a4 100644 --- a/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs +++ b/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs @@ -29,6 +29,6 @@ public override bool CanAttachTo(ILinkable other) if(Alignment == otherPort.Alignment) // can't plug input to input or output to output return false; - return Connection.Type.IsAssignableTo(otherPort.Connection.Type, out _); + return Connection.Type.IsAssignableTo(otherPort.Connection.Type, out _, out _); } } diff --git a/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs b/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs new file mode 100644 index 0000000..161f6b1 --- /dev/null +++ b/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs @@ -0,0 +1,106 @@ +using NodeDev.Core; +using NodeDev.Core.Connections; +using NodeDev.Core.Nodes; +using NodeDev.Core.Types; + +namespace NodeDev.Blazor.Services.GraphManager; + +/// +/// Contains the logic on how to manipulate Graphs and Nodes. +/// +public class GraphManagerService +{ + private readonly IGraphCanvas GraphCanvas; + + private Graph Graph => GraphCanvas.Graph; + + public GraphManagerService(IGraphCanvas graphCanvas) + { + GraphCanvas = graphCanvas; + } + + public void AddNewConnectionBetween(Connection source, Connection destination) + { + Graph.Connect(source, destination, false); + + // we're plugging something something with a generic into something without a generic + if (source.IsAssignableTo(destination, true, true, out var newTypesLeft, out var newTypesRight, out var usedInitialTypes)) + { + if (newTypesLeft.Count != 0) + PropagateNewGeneric(source.Parent, newTypesLeft, usedInitialTypes, destination, false); + if (newTypesRight.Count != 0) + PropagateNewGeneric(destination.Parent, newTypesRight, usedInitialTypes, source, false); + } + + GraphCanvas.UpdatePortColor(source); + GraphCanvas.UpdatePortColor(destination); + + // we have to disconnect the previously connected exec, since exec outputs can only have one connection + if (source.Type.IsExec && source.Connections.Count > 1) + DisconnectConnectionBetween(source, source.Connections.First(x => x != destination)); + else if (!destination.Type.IsExec && destination.Connections.Count > 1) // non-exec inputs can only have one connection + DisconnectConnectionBetween(destination.Connections.First(x => x != source), destination); + + Graph.RaiseGraphChanged(true); + } + + public void DisconnectConnectionBetween(Connection source, Connection destination) + { + Graph.Disconnect(source, destination, false); + GraphCanvas.RemoveLinkFromGraphCanvas(source, destination); + + GraphCanvas.UpdatePortColor(source); + GraphCanvas.UpdatePortColor(destination); + } + + /// + /// Propagate the new generic type to all the connections of the node and recursively to the connected nodes. + /// + /// The connection that initiated the propagation. This is used to avoid reupdating back and forth, sometimes erasing information in the process. + public void PropagateNewGeneric(Node node, IReadOnlyDictionary changedGenerics, bool useInitialTypes, Connection? initiatingConnection, bool overrideInitialTypes) + { + bool hadAnyChanges = false; + foreach (var port in node.InputsAndOutputs) // check if any of the ports have the generic we just solved + { + var previousType = useInitialTypes ? port.InitialType : port.Type; + + if (!previousType.GetUndefinedGenericTypes().Any(changedGenerics.ContainsKey)) + continue; + + // update port.Type property as well as the textbox visibility if necessary + port.UpdateTypeAndTextboxVisibility(previousType.ReplaceUndefinedGeneric(changedGenerics), overrideInitialType: overrideInitialTypes); + hadAnyChanges |= node.GenericConnectionTypeDefined(port).Count != 0; + GraphCanvas.UpdatePortColor(port); + + var isPortInput = port.IsInput; // cache for performance, IsInput is slow + // check if other connections had their own generics and if we just solved them + foreach (var other in port.Connections.ToList()) + { + if (other == initiatingConnection) + continue; + + var source = isPortInput ? other : port; + var target = isPortInput ? port : other; + if (source.IsAssignableTo(target, isPortInput, !isPortInput, out var changedGenericsLeft2, out var changedGenericsRight2, out var usedInitialTypes) && (changedGenericsLeft2.Count != 0 || changedGenericsRight2.Count != 0)) + { + if (changedGenericsLeft2.Count != 0) + PropagateNewGeneric(port.Parent, changedGenericsLeft2, usedInitialTypes, other, false); + if (changedGenericsRight2.Count != 0) + PropagateNewGeneric(other.Parent, changedGenericsRight2, usedInitialTypes, port, false); + } + else if ((changedGenericsLeft2?.Count ?? 0) != 0)// looks like changing the generic made it so we can't link to this connection anymore + DisconnectConnectionBetween(port, other); + } + } + + if (hadAnyChanges) + Graph.RaiseGraphChanged(false); + } + + public void SelectNodeOverload(Node popupNode, Node.AlternateOverload overload) + { + popupNode.SelectOverload(overload, out var newConnections, out var removedConnections); + + Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections); + } +} diff --git a/src/NodeDev.Blazor/Services/GraphManager/IGraphCanvas.cs b/src/NodeDev.Blazor/Services/GraphManager/IGraphCanvas.cs new file mode 100644 index 0000000..e556365 --- /dev/null +++ b/src/NodeDev.Blazor/Services/GraphManager/IGraphCanvas.cs @@ -0,0 +1,13 @@ +using NodeDev.Core; +using NodeDev.Core.Connections; + +namespace NodeDev.Blazor.Services.GraphManager; + +public interface IGraphCanvas +{ + Graph Graph { get; } + + void UpdatePortColor(Connection connection); + + void RemoveLinkFromGraphCanvas(Connection source, Connection destination); +} diff --git a/src/NodeDev.Core/Class/NodeClassMethod.cs b/src/NodeDev.Core/Class/NodeClassMethod.cs index 184004d..0e2d8a1 100644 --- a/src/NodeDev.Core/Class/NodeClassMethod.cs +++ b/src/NodeDev.Core/Class/NodeClassMethod.cs @@ -34,13 +34,19 @@ public NodeClassMethod(NodeClass ownerClass, string name, TypeBase returnType, G public Graph Graph { get; } + public TypeFactory TypeFactory => Class.TypeFactory; + public bool IsStatic { get; set; } public TypeBase DeclaringType => Class.ClassTypeBase; public bool HasReturnValue => ReturnType != Class.TypeFactory.Void; - public void Rename(string newName) + public EntryNode? EntryNode => Graph.Nodes.Values.OfType().FirstOrDefault(); + + public IEnumerable ReturnNodes => Graph.Nodes.Values.OfType(); + + public void Rename(string newName) { if (string.IsNullOrWhiteSpace(newName)) return; diff --git a/src/NodeDev.Core/Class/NodeClassMethodParameter.cs b/src/NodeDev.Core/Class/NodeClassMethodParameter.cs index 976618a..d0208a4 100644 --- a/src/NodeDev.Core/Class/NodeClassMethodParameter.cs +++ b/src/NodeDev.Core/Class/NodeClassMethodParameter.cs @@ -73,7 +73,7 @@ private void RefreshAllMethodCalls() { methodCall.SelectOverload(((IMethodInfo)Method).AlternateOverload(), out var newConnections, out var removedConnections); - methodCall.Graph.MergedRemovedConnectionsWithNewConnections(newConnections, removedConnections); + methodCall.Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections); } } } diff --git a/src/NodeDev.Core/Connections/Connection.cs b/src/NodeDev.Core/Connections/Connection.cs index c689c1d..692b918 100644 --- a/src/NodeDev.Core/Connections/Connection.cs +++ b/src/NodeDev.Core/Connections/Connection.cs @@ -2,6 +2,7 @@ using NodeDev.Core.Types; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Text; @@ -10,135 +11,178 @@ namespace NodeDev.Core.Connections { - [System.Diagnostics.DebuggerDisplay("{Parent.Name}:{Name} - {Type}, {Connections.Count}")] - public class Connection - { - public string Id { get; } + [System.Diagnostics.DebuggerDisplay("{Parent.Name}:{Name} - {Type}, {Connections.Count}")] + public class Connection + { + public string Id { get; } - public string Name { get; set; } + public string Name { get; set; } - public Node Parent { get; } - - public TypeBase Type { get; private set; } - - internal readonly List _Connections = []; - - public IReadOnlyList Connections => _Connections; - - /// - /// Vertices of the connection. Used for drawing connections with multiple segments. - /// This is defined either if the connection is an input AND not exec, or if the connection is an output AND exec. - /// This is because for every possible types of inputs there is always only one output connected to it. Except for execs since multiple output paths can be connected to it. - /// - public readonly List Vertices = []; - - public string? TextboxValue { get; private set; } - - public object? ParsedTextboxValue { get; set; } - - /// - /// Global index of this connection in the graph. Each connection of each node in a graph has a unique index. - /// - public int GraphIndex { get; set; } = -1; - - /// - /// LinkedExec can be used to make sure a connection can only ever be used while on a path inside the linked exec connection. - /// Ex. during a 'foreach' loop, the 'Item' connection can only be used inside the 'Loop Exec' connection. - /// - public Connection? LinkedExec { get; } - - public Connection(string name, Node parent, TypeBase type, string? id = null, Connection? linkedExec = null) - { - Id = id ?? Guid.NewGuid().ToString(); - Name = name; - Parent = parent; - Type = type; - LinkedExec = linkedExec; - } - - #region Serialization - - public record class SerializedConnectionVertex(float X, float Y); - public record SerializedConnection(string Id, string Name, TypeBase.SerializedType SerializedType, List Connections, string? TextboxValue, List? Vertices, string? LinkedExec); - internal SerializedConnection Serialize() - { - var connections = Connections.ToList(); - - var serializedConnection = new SerializedConnection(Id, Name, Type.SerializeWithFullTypeName(), Connections.Select(x => x.Id).ToList(), TextboxValue, Vertices.Select(x => new SerializedConnectionVertex(x.X, x.Y)).ToList(), LinkedExec?.Id); - - return serializedConnection; - } - - internal static Connection Deserialize(Node parent, SerializedConnection serializedConnectionObj, bool isInput) - { - // Find the LinkedExec connection, if any - Connection? linkedExec = null; - if(linkedExec != null) - linkedExec = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == serializedConnectionObj.LinkedExec); - - var type = TypeBase.Deserialize(parent.TypeFactory, serializedConnectionObj.SerializedType); - var connection = new Connection(serializedConnectionObj.Name, parent, type, serializedConnectionObj.Id, linkedExec); - - connection.TextboxValue = serializedConnectionObj.TextboxValue; - if (connection.TextboxValue != null && isInput) - connection.ParsedTextboxValue = connection.Type.ParseTextboxEdit(connection.TextboxValue); - - if (serializedConnectionObj.Vertices != null) - connection.Vertices.AddRange(serializedConnectionObj.Vertices.Select(x => new Vector2(x.X, x.Y))); - - foreach (var connectionId in serializedConnectionObj.Connections) - { - var otherConnection = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == connectionId); - if (otherConnection == null) - continue; - - if (!connection.Connections.Contains(otherConnection)) - connection._Connections.Add(otherConnection); - if (!otherConnection.Connections.Contains(connection)) - otherConnection._Connections.Add(connection); - } - - return connection; - } - - #endregion - - public void UpdateVertices(IEnumerable vertices) - { - Vertices.Clear(); - Vertices.AddRange(vertices); - } - - - public void UpdateType(TypeBase newType) - { - Type = newType; - - if (Type.AllowTextboxEdit) - TextboxValue = Type.DefaultTextboxValue; - else - TextboxValue = null; - } - - public void UpdateTextboxText(string? text) - { - if (Type.AllowTextboxEdit) - { - TextboxValue = text; - if (text == null) - ParsedTextboxValue = null; - else - { - try - { - ParsedTextboxValue = Type.ParseTextboxEdit(text); - } - catch (Exception) - { } - } - } - } - - - } + public Node Parent { get; } + + public TypeBase Type { get; private set; } + + /// + /// Initial type of the connection. Used to remember the type before generics were resolved in case we need to re-resolve them differently. + /// + public TypeBase InitialType { get; private set; } + + internal readonly List _Connections = []; + + public IReadOnlyList Connections => _Connections; + + /// + /// Vertices of the connection. Used for drawing connections with multiple segments. + /// This is defined either if the connection is an input AND not exec, or if the connection is an output AND exec. + /// This is because for every possible types of inputs there is always only one output connected to it. Except for execs since multiple output paths can be connected to it. + /// + public readonly List Vertices = []; + + public string? TextboxValue { get; private set; } + + public object? ParsedTextboxValue { get; set; } + + /// + /// Global index of this connection in the graph. Each connection of each node in a graph has a unique index. + /// + public int GraphIndex { get; set; } = -1; + + /// + /// LinkedExec can be used to make sure a connection can only ever be used while on a path inside the linked exec connection. + /// Ex. during a 'foreach' loop, the 'Item' connection can only be used inside the 'Loop Exec' connection. + /// + public Connection? LinkedExec { get; } + + public bool IsInput => Parent.Inputs.Contains(this); + public bool IsOutput => Parent.Outputs.Contains(this); + + public Connection(string name, Node parent, TypeBase type, string? id = null, Connection? linkedExec = null) + { + Id = id ?? Guid.NewGuid().ToString(); + Name = name; + Parent = parent; + Type = type; + InitialType = type; + LinkedExec = linkedExec; + } + + #region Serialization + + public record class SerializedConnectionVertex(float X, float Y); + public record SerializedConnection(string Id, string Name, TypeBase.SerializedType SerializedType, List Connections, string? TextboxValue, List? Vertices, string? LinkedExec); + internal SerializedConnection Serialize() + { + var connections = Connections.ToList(); + + var serializedConnection = new SerializedConnection(Id, Name, Type.SerializeWithFullTypeName(), Connections.Select(x => x.Id).ToList(), TextboxValue, Vertices.Select(x => new SerializedConnectionVertex(x.X, x.Y)).ToList(), LinkedExec?.Id); + + return serializedConnection; + } + + internal static Connection Deserialize(Node parent, SerializedConnection serializedConnectionObj, bool isInput) + { + // Find the LinkedExec connection, if any + Connection? linkedExec = null; + if (linkedExec != null) + linkedExec = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == serializedConnectionObj.LinkedExec); + + var type = TypeBase.Deserialize(parent.TypeFactory, serializedConnectionObj.SerializedType); + var connection = new Connection(serializedConnectionObj.Name, parent, type, serializedConnectionObj.Id, linkedExec); + + connection.TextboxValue = serializedConnectionObj.TextboxValue; + if (connection.TextboxValue != null && isInput) + connection.ParsedTextboxValue = connection.Type.ParseTextboxEdit(connection.TextboxValue); + + if (serializedConnectionObj.Vertices != null) + connection.Vertices.AddRange(serializedConnectionObj.Vertices.Select(x => new Vector2(x.X, x.Y))); + + foreach (var connectionId in serializedConnectionObj.Connections) + { + var otherConnection = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == connectionId); + if (otherConnection == null) + continue; + + if (!connection.Connections.Contains(otherConnection)) + connection._Connections.Add(otherConnection); + if (!otherConnection.Connections.Contains(connection)) + otherConnection._Connections.Add(connection); + } + + return connection; + } + + #endregion + + public bool IsAssignableTo(Connection other, bool alsoValidateInitialTypeSource, bool alsoValidateInitialTypeDestination, [MaybeNullWhen(false)] out Dictionary changedGenericsLeft, [MaybeNullWhen(false)] out Dictionary changedGenericsRight, out bool usedInitialTypes) + { + if (Type.IsAssignableTo(other.Type, out var changedGenericsLeft1, out var changedGenericsRight1, out var depth1)) + { + if (alsoValidateInitialTypeSource || alsoValidateInitialTypeDestination) + { + var initialType = alsoValidateInitialTypeSource ? InitialType : Type; + var otherInitialType = alsoValidateInitialTypeDestination ? other.InitialType : other.Type; + if ((initialType != Type || otherInitialType != other.Type) && initialType.IsAssignableTo(otherInitialType, out var changedGenericsLeft2, out var changedGenericsRight2, out var depth2)) + { + if ((changedGenericsLeft2.Count != 0 || changedGenericsRight2.Count != 0) && depth2 < depth1) + { + changedGenericsLeft = changedGenericsLeft2; + changedGenericsRight = changedGenericsRight2; + usedInitialTypes = true; + return true; + } + } + } + + changedGenericsLeft = changedGenericsLeft1; + changedGenericsRight = changedGenericsRight1; + usedInitialTypes = false; + return true; + } + + changedGenericsLeft = changedGenericsRight = null; + usedInitialTypes = false; + return false; + } + + public void UpdateVertices(IEnumerable vertices) + { + Vertices.Clear(); + Vertices.AddRange(vertices); + } + + + public void UpdateTypeAndTextboxVisibility(TypeBase newType, bool overrideInitialType) + { + Type = newType; + + if (overrideInitialType) + InitialType = newType; + + if (Type.AllowTextboxEdit) + TextboxValue = Type.DefaultTextboxValue; + else + TextboxValue = null; + } + + public void UpdateTextboxText(string? text) + { + if (Type.AllowTextboxEdit) + { + TextboxValue = text; + if (text == null) + ParsedTextboxValue = null; + else + { + try + { + ParsedTextboxValue = Type.ParseTextboxEdit(text); + } + catch (Exception) + { } + } + } + } + + + } } diff --git a/src/NodeDev.Core/Graph.cs b/src/NodeDev.Core/Graph.cs index 968eacc..3ad5e64 100644 --- a/src/NodeDev.Core/Graph.cs +++ b/src/NodeDev.Core/Graph.cs @@ -385,7 +385,7 @@ public async Task Invoke(Func action) #region Connect / Disconnect / MergedRemovedConnectionsWithNewConnections - public void MergedRemovedConnectionsWithNewConnections(List newConnections, List removedConnections) + public void MergeRemovedConnectionsWithNewConnections(List newConnections, List removedConnections) { foreach (var removedConnection in removedConnections) { diff --git a/src/NodeDev.Core/NodeProvider.cs b/src/NodeDev.Core/NodeProvider.cs index c50969d..dac98ad 100644 --- a/src/NodeDev.Core/NodeProvider.cs +++ b/src/NodeDev.Core/NodeProvider.cs @@ -68,7 +68,7 @@ IEnumerable GetPropertiesAndFields(TypeBase type, string text) // only keep the methods that are using the startConnection type, if provided if (startConnection?.Type?.IsExec == false) - methods = methods.Where(x => x.GetParameters().Any(y => startConnection.Type.IsAssignableTo(y.ParameterType, out _))); + methods = methods.Where(x => x.GetParameters().Any(y => startConnection.Type.IsAssignableTo(y.ParameterType, out _, out _))); results = results.Concat(methods.Select(x => new MethodCallNode(typeof(MethodCall), x))); @@ -123,7 +123,7 @@ private static IEnumerable GetExtensionMethods(TypeBase t, TypeFact .GetTypes() .Where(type => !type.IsGenericType) .SelectMany(x => x.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) - .Where(method => method.IsDefined(typeof(ExtensionAttribute), false) && t.IsAssignableTo(typeFactory.Get(method.GetParameters()[0].ParameterType, null), out _)) + .Where(method => method.IsDefined(typeof(ExtensionAttribute), false) && t.IsAssignableTo(typeFactory.Get(method.GetParameters()[0].ParameterType, null), out _, out _)) .Select(x => new RealMethodInfo(typeFactory, x, typeFactory.Get(x.DeclaringType!, null))) .ToList(); }); diff --git a/src/NodeDev.Core/Nodes/ArrayGet.cs b/src/NodeDev.Core/Nodes/ArrayGet.cs new file mode 100644 index 0000000..c8bdfe1 --- /dev/null +++ b/src/NodeDev.Core/Nodes/ArrayGet.cs @@ -0,0 +1,34 @@ +using NodeDev.Core.Class; +using NodeDev.Core.Connections; +using NodeDev.Core.Types; +using System.Linq.Expressions; + +namespace NodeDev.Core.Nodes; + +public class ArrayGet : NoFlowNode +{ + public override string Name + { + get => $"{Outputs[0].Type.Name} Get"; + set { } + } + + public ArrayGet(Graph graph, string? id = null) : base(graph, id) + { + var undefinedT = new UndefinedGenericType("T"); + + Inputs.Add(new("Array", this, undefinedT.ArrayType)); + Inputs.Add(new("Index", this, TypeFactory.Get())); + + Outputs.Add(new("Obj", this, undefinedT)); + } + + internal override Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) + { + if (!Inputs[0].Type.IsArray) + throw new Exception("ArrayGet.Inputs[0] should be an array type"); + + var arrayIndex = Expression.ArrayIndex(info.LocalVariables[Inputs[0]], info.LocalVariables[Inputs[1]]); + return Expression.Assign(info.LocalVariables[Outputs[1]], arrayIndex); + } +} diff --git a/src/NodeDev.Core/Nodes/ArraySet.cs b/src/NodeDev.Core/Nodes/ArraySet.cs new file mode 100644 index 0000000..849fa3c --- /dev/null +++ b/src/NodeDev.Core/Nodes/ArraySet.cs @@ -0,0 +1,33 @@ +using NodeDev.Core.Class; +using NodeDev.Core.Connections; +using NodeDev.Core.Types; +using System.Linq.Expressions; + +namespace NodeDev.Core.Nodes; + +public class ArraySet : NormalFlowNode +{ + public override string Name + { + get => $"{Inputs[0].Type.Name} Set"; + set { } + } + + public ArraySet(Graph graph, string? id = null) : base(graph, id) + { + var undefinedT = new UndefinedGenericType("T"); + + Inputs.Add(new("Array", this, undefinedT.ArrayType)); + Inputs.Add(new("Index", this, TypeFactory.Get())); + Inputs.Add(new("Obj", this, undefinedT)); + } + + internal override Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) + { + if (!Inputs[1].Type.IsArray) + throw new Exception("ArrayGet.Inputs[1] should be an array type"); + + var arrayIndex = Expression.ArrayIndex(info.LocalVariables[Inputs[1]], info.LocalVariables[Inputs[2]]); + return Expression.Assign(arrayIndex, info.LocalVariables[Inputs[3]]); + } +} diff --git a/src/NodeDev.Core/Nodes/Flow/EntryNode.cs b/src/NodeDev.Core/Nodes/Flow/EntryNode.cs index a64e882..e1af2c6 100644 --- a/src/NodeDev.Core/Nodes/Flow/EntryNode.cs +++ b/src/NodeDev.Core/Nodes/Flow/EntryNode.cs @@ -43,7 +43,7 @@ internal Connection UpdateParameterType(NodeClassMethodParameter parameter, int { var connection = Outputs[index + 1]; - connection.UpdateType(parameter.ParameterType); + connection.UpdateTypeAndTextboxVisibility(parameter.ParameterType, overrideInitialType: true); return connection; } @@ -56,7 +56,7 @@ internal void Refresh() Outputs.RemoveRange(1, Outputs.Count - 1); Outputs.AddRange(newConnections); - Graph.MergedRemovedConnectionsWithNewConnections(newConnections, removedConnections); + Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections); } internal override Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) diff --git a/src/NodeDev.Core/Nodes/Flow/ForeachNode.cs b/src/NodeDev.Core/Nodes/Flow/ForeachNode.cs index 806d1ac..1e00370 100644 --- a/src/NodeDev.Core/Nodes/Flow/ForeachNode.cs +++ b/src/NodeDev.Core/Nodes/Flow/ForeachNode.cs @@ -44,19 +44,6 @@ public override string GetExecOutputPathId(string pathId, Connection execOutput) public override bool DoesOutputPathAllowMerge(Connection execOutput) => execOutput == Outputs[2]; // The ExecOut path allows merging but not the loop. The loop is always a dead end. - public override List GenericConnectionTypeDefined(UndefinedGenericType previousType, Connection connection, TypeBase newType) - { - if (Inputs[1].Type.HasUndefinedGenerics) - { - var type = newType.Generics[0]; // get the 'T' our of IEnumerable - Inputs[1].UpdateType(type); - - return [Inputs[1]]; - } - - return []; - } - private readonly string LabelName = $"break_{Random.Shared.Next(0, 100000)}"; internal override Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) { diff --git a/src/NodeDev.Core/Nodes/Flow/ReturnNode.cs b/src/NodeDev.Core/Nodes/Flow/ReturnNode.cs index 688f329..3a5aa02 100644 --- a/src/NodeDev.Core/Nodes/Flow/ReturnNode.cs +++ b/src/NodeDev.Core/Nodes/Flow/ReturnNode.cs @@ -60,6 +60,6 @@ internal void Refresh() Inputs.RemoveRange(1, Inputs.Count - 1); Inputs.AddRange(newConnections); - Graph.MergedRemovedConnectionsWithNewConnections(newConnections, removedConnections); + Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections); } } diff --git a/src/NodeDev.Core/Nodes/Math/Equals.cs b/src/NodeDev.Core/Nodes/Math/Equals.cs index a17eda3..377e508 100644 --- a/src/NodeDev.Core/Nodes/Math/Equals.cs +++ b/src/NodeDev.Core/Nodes/Math/Equals.cs @@ -12,7 +12,7 @@ public Equals(Graph graph, string? id = null) : base(graph, id) { Name = "Equals"; - Outputs[0].UpdateType(TypeFactory.Get(typeof(bool), null)); + Outputs[0].UpdateTypeAndTextboxVisibility(TypeFactory.Get(typeof(bool), null), overrideInitialType: true); } internal override void BuildInlineExpression(BuildExpressionInfo info) diff --git a/src/NodeDev.Core/Nodes/Math/NotEquals.cs b/src/NodeDev.Core/Nodes/Math/NotEquals.cs index 7222139..908de81 100644 --- a/src/NodeDev.Core/Nodes/Math/NotEquals.cs +++ b/src/NodeDev.Core/Nodes/Math/NotEquals.cs @@ -11,7 +11,7 @@ public NotEquals(Graph graph, string? id = null) : base(graph, id) { Name = "NotEquals"; - Outputs[0].UpdateType(TypeFactory.Get(typeof(bool), null)); + Outputs[0].UpdateTypeAndTextboxVisibility(TypeFactory.Get(typeof(bool), null), overrideInitialType: true); } internal override void BuildInlineExpression(BuildExpressionInfo info) diff --git a/src/NodeDev.Core/Nodes/Math/TwoOperationMath.cs b/src/NodeDev.Core/Nodes/Math/TwoOperationMath.cs index a80880d..8e09243 100644 --- a/src/NodeDev.Core/Nodes/Math/TwoOperationMath.cs +++ b/src/NodeDev.Core/Nodes/Math/TwoOperationMath.cs @@ -21,8 +21,8 @@ public TwoOperationMath(Graph graph, string? id = null) : base(graph, id) Outputs.Add(new("c", this, new UndefinedGenericType("T3"))); } - public override List GenericConnectionTypeDefined(UndefinedGenericType previousType, Connection connection, TypeBase newType) - { + public override List GenericConnectionTypeDefined(Connection connection) + { if (Inputs.Count(x => x.Type is RealType t && (t.BackendType.IsPrimitive || t.BackendType == typeof(string))) == 2) { if (!Outputs[0].Type.HasUndefinedGenerics) @@ -51,7 +51,7 @@ public override List GenericConnectionTypeDefined(UndefinedGenericTy else resultingType = typeof(int); - Outputs[0].UpdateType(TypeFactory.Get(resultingType, null)); + Outputs[0].UpdateTypeAndTextboxVisibility(TypeFactory.Get(resultingType, null), overrideInitialType: true); return new() { Outputs[0] }; } @@ -64,7 +64,7 @@ public override List GenericConnectionTypeDefined(UndefinedGenericTy if(correctOne != null) { - Outputs[0].UpdateType(TypeFactory.Get(correctOne.ReturnType, null)); + Outputs[0].UpdateTypeAndTextboxVisibility(TypeFactory.Get(correctOne.ReturnType, null), overrideInitialType: true); return new() { Outputs[0] }; } } diff --git a/src/NodeDev.Core/Nodes/New.cs b/src/NodeDev.Core/Nodes/New.cs index e5f4728..16edf45 100644 --- a/src/NodeDev.Core/Nodes/New.cs +++ b/src/NodeDev.Core/Nodes/New.cs @@ -7,65 +7,81 @@ namespace NodeDev.Core.Nodes; public class New : NormalFlowNode { - public override string Name - { - get => Outputs[1].Type.HasUndefinedGenerics ? "New ?" : $"New {Outputs[1].Type.FriendlyName}"; - set { } - } + public override string Name + { + get => Outputs[1].Type.HasUndefinedGenerics ? "New ?" : $"New {Outputs[1].Type.FriendlyName}"; + set { } + } - public New(Graph graph, string? id = null) : base(graph, id) - { - Outputs.Add(new("Obj", this, new UndefinedGenericType("T"))); - } + public New(Graph graph, string? id = null) : base(graph, id) + { + Outputs.Add(new("Obj", this, new UndefinedGenericType("T"))); + } - public override IEnumerable AlternatesOverloads - { - get - { - if (Outputs[1].Type is UndefinedGenericType) - return []; + public override IEnumerable AlternatesOverloads + { + get + { + // If we don't know the type yet we are constructing an array, there are no overloads to show + if (Outputs[1].Type is UndefinedGenericType || Outputs[1].Type.IsArray) + return []; - if (Outputs[1].Type is RealType realType) - { - var constructors = realType.BackendType.GetConstructors(); - return constructors.Select(x => new AlternateOverload(Outputs[1].Type, x.GetParameters().Select(y => new RealMethodParameterInfo(y, TypeFactory, realType)).OfType().ToList())).ToList(); - } - else if (Outputs[1].Type is NodeClassType nodeClassType) - return [new(Outputs[1].Type, [])]; // for now, we don't handle custom constructors + if (Outputs[1].Type is RealType realType) + { + var constructors = realType.BackendType.GetConstructors(); + return constructors.Select(x => new AlternateOverload(Outputs[1].Type, x.GetParameters().Select(y => new RealMethodParameterInfo(y, TypeFactory, realType)).OfType().ToList())).ToList(); + } + else if (Outputs[1].Type is NodeClassType nodeClassType) + return [new(Outputs[1].Type, [])]; // for now, we don't handle custom constructors - else - throw new Exception("Unknowned type in New node: " + Outputs[1].Type.Name); - } - } - public override List GenericConnectionTypeDefined(UndefinedGenericType previousType, Connection connection, TypeBase newType) - { - var constructor = AlternatesOverloads.First(); + else + throw new Exception("Unknown type in New node: " + Outputs[1].Type.Name); + } + } - Inputs.AddRange(constructor.Parameters.Select(x => new Connection(x.Name ?? "??", this, x.ParameterType))); + public override List GenericConnectionTypeDefined(Connection connection) + { + if (Outputs[1].Type.IsArray) + { + Inputs.Add(new("Length", this, TypeFactory.Get())); + } + else + { + var constructor = AlternatesOverloads.First(); - return []; - } + Inputs.AddRange(constructor.Parameters.Select(x => new Connection(x.Name ?? "??", this, x.ParameterType))); + } - public override void SelectOverload(AlternateOverload overload, out List newConnections, out List removedConnections) - { - removedConnections = Inputs.Skip(1).ToList(); - Inputs.RemoveRange(1, Inputs.Count - 1); + return []; + } - newConnections = overload.Parameters.Select(x => new Connection(x.Name, this, x.ParameterType)).ToList(); - Inputs.AddRange(newConnections); - } + public override void SelectOverload(AlternateOverload overload, out List newConnections, out List removedConnections) + { + removedConnections = Inputs.Skip(1).ToList(); + Inputs.RemoveRange(1, Inputs.Count - 1); - internal override Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) - { - var type = Outputs[1].Type.MakeRealType(); + newConnections = overload.Parameters.Select(x => new Connection(x.Name, this, x.ParameterType)).ToList(); + Inputs.AddRange(newConnections); + } - var argumentTypes = Inputs.Skip(1).Select(x => x.Type.MakeRealType()).ToArray(); - var constructor = type.GetConstructor(argumentTypes); + internal override Expression BuildExpression(Dictionary? subChunks, BuildExpressionInfo info) + { + var type = Outputs[1].Type.MakeRealType(); + if (type.IsArray) + { + var length = info.LocalVariables[Inputs[1]]; + return Expression.Assign(info.LocalVariables[Outputs[1]], Expression.NewArrayBounds(type.GetElementType()!, length)); + } + else + { + var argumentTypes = Inputs.Skip(1).Select(x => x.Type.MakeRealType()).ToArray(); + var constructor = type.GetConstructor(argumentTypes); - if (constructor == null) - throw new Exception($"Constructor not found: {Outputs[1].Type.FriendlyName}"); + if (constructor == null) + throw new Exception($"Constructor not found: {Outputs[1].Type.FriendlyName}"); - var arguments = Inputs.Skip(1).Select(x => info.LocalVariables[x]).ToArray(); - return Expression.Assign(info.LocalVariables[Outputs[1]], Expression.New(constructor, arguments)); - } + var arguments = Inputs.Skip(1).Select(x => info.LocalVariables[x]).ToArray(); + return Expression.Assign(info.LocalVariables[Outputs[1]], Expression.New(constructor, arguments)); + } + } } diff --git a/src/NodeDev.Core/Nodes/Node.cs b/src/NodeDev.Core/Nodes/Node.cs index b778932..5ed36f8 100644 --- a/src/NodeDev.Core/Nodes/Node.cs +++ b/src/NodeDev.Core/Nodes/Node.cs @@ -61,18 +61,18 @@ public Node(Graph graph, string? id = null) /// public int GraphIndex { get; set; } = -1; - public IEnumerable GetUndefinedGenericTypes() => InputsAndOutputs.SelectMany(x => x.Type.GetUndefinedGenericTypes()).Distinct(); + public IEnumerable GetUndefinedGenericTypes() => InputsAndOutputs.SelectMany(x => x.Type.GetUndefinedGenericTypes()).Distinct(); public record class AlternateOverload(TypeBase ReturnType, List Parameters); - public virtual IEnumerable AlternatesOverloads => Enumerable.Empty(); + public virtual IEnumerable AlternatesOverloads => []; /// /// returns a list of changed connections, if any /// /// The connection that was generic, it is not generic anymore - public virtual List GenericConnectionTypeDefined(UndefinedGenericType previousType, Connection connection, TypeBase baseType) - { - return new(); + public virtual List GenericConnectionTypeDefined(Connection connection) + { + return []; } public virtual void SelectOverload(AlternateOverload overload, out List newConnections, out List removedConnections) diff --git a/src/NodeDev.Core/Project.cs b/src/NodeDev.Core/Project.cs index 2bdfcb7..16c552f 100644 --- a/src/NodeDev.Core/Project.cs +++ b/src/NodeDev.Core/Project.cs @@ -66,19 +66,28 @@ public IEnumerable GetNodes() where T : Node #region CreateNewDefaultProject public static Project CreateNewDefaultProject() - { + { + return CreateNewDefaultProject(out _); + } + + public static Project CreateNewDefaultProject(out NodeClassMethod main) + { var project = new Project(Guid.NewGuid()); var programClass = new NodeClass("Program", "NewProject", project); - var main = new NodeClassMethod(programClass, "Main", project.TypeFactory.Get(typeof(void), null), new Graph()); + main = new NodeClassMethod(programClass, "Main", project.TypeFactory.Get(typeof(void), null), new Graph()); main.IsStatic = true; - main.Graph.AddNode(new EntryNode(main.Graph), false); - main.Graph.AddNode(new ReturnNode(main.Graph), false); + var entry = new EntryNode(main.Graph); + var returnNode = new ReturnNode(main.Graph); + main.Graph.AddNode(entry, false); + main.Graph.AddNode(returnNode, false); programClass.Methods.Add(main); + project.Classes.Add(programClass); - project.Classes.Add(programClass); + // Connect entry's exec to return node's exec + main.Graph.Connect(entry.Outputs[0], returnNode.Inputs[0], false); return project; } diff --git a/src/NodeDev.Core/Types/ExecType.cs b/src/NodeDev.Core/Types/ExecType.cs index 5cf4932..0793eba 100644 --- a/src/NodeDev.Core/Types/ExecType.cs +++ b/src/NodeDev.Core/Types/ExecType.cs @@ -24,7 +24,13 @@ public class ExecType : TypeBase public override TypeBase[] Interfaces => throw new NotImplementedException(); - public override IEnumerable GetMembers() => throw new NotImplementedException(); + public override bool IsArray => false; + + public override TypeBase ArrayInnerType => throw new Exception("Can't call ArrayInnerType on ExecType"); + + public override TypeBase ArrayType => throw new Exception("Can't call ArrayType on ExecType"); + + public override IEnumerable GetMembers() => throw new NotImplementedException(); public override IEnumerable GetMethods() => []; diff --git a/src/NodeDev.Core/Types/NodeClassArrayType.cs b/src/NodeDev.Core/Types/NodeClassArrayType.cs new file mode 100644 index 0000000..f9ab0b6 --- /dev/null +++ b/src/NodeDev.Core/Types/NodeClassArrayType.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NodeDev.Core.Types +{ + public class NodeClassArrayType : TypeBase + { + public readonly NodeClassType InnerNodeClassType; + + public readonly int NbArrayLevels; + + public NodeClassArrayType(NodeClassType innerClassType, int nbArrayLevels) + { + if (nbArrayLevels == 0) + throw new ArgumentException("NodeClassArrayType cannot have 0 array level. That imples 'not array', in which case NodeClassType should be used instead", nameof(nbArrayLevels)); + + InnerNodeClassType = innerClassType; + NbArrayLevels = nbArrayLevels; + } + + public override string Name => InnerNodeClassType.Name + GetArrayString(NbArrayLevels); + + public override string FullName => InnerNodeClassType.FullName + GetArrayString(NbArrayLevels); + + public override TypeBase[] Generics => InnerNodeClassType.Generics; + + public override TypeBase? BaseType => InnerNodeClassType.NodeClass.TypeFactory.Get(); + + public override TypeBase[] Interfaces => [InnerNodeClassType.NodeClass.TypeFactory.Get(typeof(IReadOnlyList<>), [ArrayInnerType])]; + + public override bool IsArray => true; + + public override string FriendlyName => InnerNodeClassType.FriendlyName + GetArrayString(NbArrayLevels); + + public override TypeBase ArrayInnerType => NbArrayLevels == 1 ? InnerNodeClassType : new NodeClassArrayType(InnerNodeClassType, NbArrayLevels - 1); + + public override TypeBase ArrayType => new NodeClassArrayType(InnerNodeClassType, NbArrayLevels + 1); + + public override TypeBase CloneWithGenerics(TypeBase[] newGenerics) + { + return new NodeClassArrayType((NodeClassType)InnerNodeClassType.CloneWithGenerics(newGenerics), NbArrayLevels); + } + + public override IEnumerable GetMembers() + { + return []; + } + + public override IEnumerable GetMethods() + { + return []; + } + + public override IEnumerable GetMethods(string name) + { + return []; + } + + public override bool IsSameBackend(TypeBase typeBase) + { + if(typeBase is not NodeClassArrayType nodeClassArrayType) + return false; + + return NbArrayLevels == nodeClassArrayType.NbArrayLevels && InnerNodeClassType.IsSameBackend(nodeClassArrayType.InnerNodeClassType); + } + + public override Type MakeRealType() + { + var realBaseType = InnerNodeClassType.MakeRealType(); + + for(int i = 0; i < NbArrayLevels; i++) + realBaseType = realBaseType.MakeArrayType(); + + return realBaseType; + } + + private record class SerializedNodeClassArrayType(string InnerNodeClassType, int NbArrayLevels); + protected internal override string Serialize() + { + return System.Text.Json.JsonSerializer.Serialize(new SerializedNodeClassArrayType(InnerNodeClassType.Serialize(), NbArrayLevels)); + } + + public new static NodeClassArrayType Deserialize(TypeFactory typeFactory, string serializedString) + { + var deserialized = System.Text.Json.JsonSerializer.Deserialize(serializedString); + if(deserialized == null) + throw new ArgumentException("Failed to deserialize NodeClassArrayType"); + + return new NodeClassArrayType(NodeClassType.Deserialize(typeFactory, deserialized.InnerNodeClassType), deserialized.NbArrayLevels); + } + + + public static string GetArrayString(int nbArrayLevels) + { + if (nbArrayLevels == 0) + return string.Empty; + + var str = string.Create(nbArrayLevels * 2, nbArrayLevels, static (span, nbLevels) => + { + for (int i = 0; i < span.Length; i += 2) + { + span[i] = '['; + span[i + 1] = ']'; + } + }); + + return str; + } + } +} diff --git a/src/NodeDev.Core/Types/NodeClassType.cs b/src/NodeDev.Core/Types/NodeClassType.cs index ff5f21f..2c1ad83 100644 --- a/src/NodeDev.Core/Types/NodeClassType.cs +++ b/src/NodeDev.Core/Types/NodeClassType.cs @@ -7,69 +7,83 @@ namespace NodeDev.Core.Types { - public class NodeClassType : TypeBase - { - public readonly NodeClass NodeClass; + public class NodeClassType : TypeBase + { + public readonly NodeClass NodeClass; - public NodeClassType(NodeClass nodeClass, TypeBase[] generics) - { - if (generics.Length != 0) - throw new NotImplementedException("Generics are not supported yet for NodeClass"); + public NodeClassType(NodeClass nodeClass, TypeBase[] generics) + { + if (generics.Length != 0) + throw new NotImplementedException("Generics are not supported yet for NodeClass"); - NodeClass = nodeClass; - Generics = generics; - } + NodeClass = nodeClass; + Generics = generics; + } - public override string Name => NodeClass.Name; + public override string Name => NodeClass.Name; - public override string FullName => NodeClass.Namespace + "." + NodeClass.Name; + public override string FullName => NodeClass.Namespace + "." + NodeClass.Name; - public override TypeBase[] Generics { get; } + public override TypeBase[] Generics { get; } - override public TypeBase? BaseType => null; + override public TypeBase? BaseType => null; - public override string FriendlyName => Name; + public override string FriendlyName => Name; - public override TypeBase[] Interfaces => Array.Empty(); + public override bool IsArray => false; - public override IEnumerable GetMembers() =>NodeClass.Properties; + public override TypeBase ArrayInnerType => throw new NotImplementedException(); - internal protected override string Serialize() - { - return FullName; - } + public override TypeBase ArrayType => new NodeClassArrayType(this, 1); - public override TypeBase CloneWithGenerics(TypeBase[] newGenerics) - { - if (Generics.Length != newGenerics.Length) - throw new ArgumentException("Generics count mismatch"); + public override TypeBase[] Interfaces => []; - return new NodeClassType(NodeClass, newGenerics); - } + public override IEnumerable GetMembers() => NodeClass.Properties; - public new static NodeClassType Deserialize(TypeFactory typeFactory, string typeName) - { - return typeFactory.Project.GetNodeClassType(typeFactory.Project.Classes.First(x => x.Namespace + "." + x.Name == typeName)); - } + public NodeClassType GetNonArray() + { + if(!IsArray) + return this; - public override IEnumerable GetMethods() - { - return NodeClass.Methods; - } + return NodeClass.Project.GetNodeClassType(NodeClass, Generics); + } - public override IEnumerable GetMethods(string name) - { - return NodeClass.Methods.Where(x => x.Name == name); - } + internal protected override string Serialize() + { + return FullName; + } - public override Type MakeRealType() - { - return NodeClass.Project.GetCreatedClassType(NodeClass); - } + public override TypeBase CloneWithGenerics(TypeBase[] newGenerics) + { + if (Generics.Length != newGenerics.Length) + throw new ArgumentException("Generics count mismatch"); - public override bool IsSameBackend(TypeBase typeBase) - { - return typeBase is NodeClassType nodeClassType && nodeClassType.NodeClass == NodeClass; - } - } + return new NodeClassType(NodeClass, newGenerics); + } + + public new static NodeClassType Deserialize(TypeFactory typeFactory, string typeName) + { + return typeFactory.Project.GetNodeClassType(typeFactory.Project.Classes.First(x => x.Namespace + "." + x.Name == typeName)); + } + + public override IEnumerable GetMethods() + { + return NodeClass.Methods; + } + + public override IEnumerable GetMethods(string name) + { + return NodeClass.Methods.Where(x => x.Name == name); + } + + public override Type MakeRealType() + { + return NodeClass.Project.GetCreatedClassType(NodeClass); + } + + public override bool IsSameBackend(TypeBase typeBase) + { + return typeBase is NodeClassType nodeClassType && nodeClassType.NodeClass == NodeClass; + } + } } diff --git a/src/NodeDev.Core/Types/RealType.cs b/src/NodeDev.Core/Types/RealType.cs index 5939e0c..acc6bf8 100644 --- a/src/NodeDev.Core/Types/RealType.cs +++ b/src/NodeDev.Core/Types/RealType.cs @@ -28,7 +28,13 @@ public class RealType : TypeBase private TypeBase[]? _Interfaces; public override TypeBase[] Interfaces => _Interfaces ?? InitializeInterfaces(); - public override bool IsIn(int genericIndex) => BackendType.GetGenericArguments()[genericIndex].IsGenericParameter && (BackendType.GetGenericArguments()[genericIndex].GenericParameterAttributes & System.Reflection.GenericParameterAttributes.Contravariant) != System.Reflection.GenericParameterAttributes.None; + public override bool IsArray => BackendType.IsArray; + + public override TypeBase ArrayType => TypeFactory.Get(BackendType.MakeArrayType(), Generics); + + public override TypeBase ArrayInnerType => IsArray ? TypeFactory.Get(BackendType.GetElementType()!, Generics) : throw new Exception("Can't call ArrayInnerType on non-array type"); + + public override bool IsIn(int genericIndex) => BackendType.GetGenericArguments()[genericIndex].IsGenericParameter && (BackendType.GetGenericArguments()[genericIndex].GenericParameterAttributes & System.Reflection.GenericParameterAttributes.Contravariant) != System.Reflection.GenericParameterAttributes.None; public override bool IsOut(int genericIndex) => BackendType.GetGenericArguments()[genericIndex].IsGenericParameter && (BackendType.GetGenericArguments()[genericIndex].GenericParameterAttributes & System.Reflection.GenericParameterAttributes.Covariant) != System.Reflection.GenericParameterAttributes.None; diff --git a/src/NodeDev.Core/Types/TypeBase.cs b/src/NodeDev.Core/Types/TypeBase.cs index 55ed8c0..2bfbc90 100644 --- a/src/NodeDev.Core/Types/TypeBase.cs +++ b/src/NodeDev.Core/Types/TypeBase.cs @@ -11,344 +11,460 @@ namespace NodeDev.Core.Types; public abstract class TypeBase { - public abstract string Name { get; } + public abstract string Name { get; } - public abstract string FullName { get; } + public abstract string FullName { get; } - public virtual bool IsClass => true; + public virtual bool IsClass => true; - public abstract TypeBase[] Generics { get; } + public abstract TypeBase[] Generics { get; } - public abstract TypeBase? BaseType { get; } + public abstract TypeBase? BaseType { get; } - public abstract TypeBase[] Interfaces { get; } - - public bool HasUndefinedGenerics => Generics.Any(x => x is UndefinedGenericType || x.HasUndefinedGenerics); - - public virtual bool IsExec => false; - - public virtual bool AllowTextboxEdit => false; - - public virtual string? DefaultTextboxValue => null; - - public abstract string FriendlyName { get; } - - internal protected abstract string Serialize(); - - public abstract Type MakeRealType(); - - public abstract TypeBase CloneWithGenerics(TypeBase[] newGenerics); - - public virtual object? ParseTextboxEdit(string text) => throw new NotImplementedException(); - - public abstract IEnumerable GetMembers(); - - public record class SerializedType(string TypeFullName, string SerializedTypeCustom); - public SerializedType SerializeWithFullTypeName() - { - var serializedType = new SerializedType(GetType().FullName!, Serialize()); - - return serializedType; - } - public string SerializeWithFullTypeNameString() => JsonSerializer.Serialize(SerializeWithFullTypeName()); - - public IEnumerable GetUndefinedGenericTypes() - { - IEnumerable undefinedGenericTypes = this is UndefinedGenericType undefinedGenericType ? new[] { undefinedGenericType } : Enumerable.Empty(); - - return undefinedGenericTypes.Concat(Generics.SelectMany(x => x.GetUndefinedGenericTypes())).Distinct(); - } - - public TypeBase ReplaceUndefinedGeneric(IReadOnlyDictionary genericTypes) - { - if (this is UndefinedGenericType undefinedGeneric) - { - if (genericTypes.TryGetValue(undefinedGeneric, out var newType)) - return newType; - else - return undefinedGeneric; // put back the undefined generic if we didn't find a replacement - } - - var generics = new TypeBase[Generics.Length]; - - for (int i = 0; i < Generics.Length; ++i) - { - var generic = Generics[i]; - if (generic is UndefinedGenericType undefinedGenericType) - { - if (genericTypes.TryGetValue(undefinedGenericType, out var newType)) - generics[i] = newType; - else - generics[i] = undefinedGenericType; // put back the undefined generic if we didn't find a replacement - } - else - generics[i] = generic.ReplaceUndefinedGeneric(genericTypes); // ask a more complex type to replace its own undefined generics - } - - return CloneWithGenerics(generics); - } - - #region Assignation checks - - /// - /// Returns in the backend type is the same, ignoring generics - /// For RealType, that means the actual 'Type' Backend - /// - /// - /// - public abstract bool IsSameBackend(TypeBase typeBase); - - internal bool IsDirectlyAssignableTo(TypeBase other, bool allowInOutGenerics, [MaybeNullWhen(false)] out Dictionary changedGenerics) - { - if (this is UndefinedGenericType thisUndefinedGenericType) - { - if (other is UndefinedGenericType) - { - changedGenerics = new(); // nothing to change, we're plugging a generic into another generic - return true; - } - else // we can change 'this' to the same type as 'other' and plug into it - { - changedGenerics = new() - { - [thisUndefinedGenericType] = other - }; - return true; - } - - } - else if (other is UndefinedGenericType otherUndefinedGenericType) // we can change the other generic into the current type - { - changedGenerics = new() - { - [otherUndefinedGenericType] = this - }; - return true; - } - - if (IsSameBackend(other)) // same backend, List would be the same backend as List or List - { - // check all the generics, they have to either be undefined, the same or covariant - changedGenerics = new(); - for (int i = 0; i < Generics.Length; ++i) - { - if (allowInOutGenerics && other.IsOut(i)) // we can plug a less derived type, like IEnumerable to IEnumerable - { - if (Generics[i].IsAssignableTo(other.Generics[i], out var changedGenericsLocal)) - { - foreach (var changed in changedGenericsLocal) - changedGenerics[changed.Key] = changed.Value; - continue; // it worked, check the next generic - } - } - else if (allowInOutGenerics && other.IsIn(i)) // We can plug a more derived type, like IComparable to IComparable - { - if (other.Generics[i].IsAssignableTo(Generics[i], out var changedGenericsLocal)) - { - foreach (var changed in changedGenericsLocal) - changedGenerics[changed.Key] = changed.Value; - continue; // it worked, check the next generic - } - } - else - { - // the generic is not covariant, so it has to be the same - if (Generics[i].IsDirectlyAssignableTo(other.Generics[i], allowInOutGenerics, out var changedGenericsLocal)) - { - foreach (var changed in changedGenericsLocal) - changedGenerics[changed.Key] = changed.Value; - continue; // it worked, check the next generic - } - } - - - // one of the generics is not assignable, so we're not assignable - changedGenerics = null; - return false; - } - - return true; - } - - changedGenerics = null; - return false; - } - - public bool IsAssignableTo(TypeBase other, [MaybeNullWhen(false)] out Dictionary changedGenerics) - { - if ((this is ExecType && other is not ExecType) || (other is ExecType && this is not ExecType)) - { - changedGenerics = null; - return false; - } - - if (IsDirectlyAssignableTo(other, true, out changedGenerics)) - return true; // Either plugging something easy like List to List, some generic or covariant like IEnumerable to IEnumerable - - var myAssignableTypes = GetAssignableTypes().OrderBy(x => x.Depth).Select(x => x.Type).ToList(); - - var changedGenericsLocal = new Dictionary(); - foreach (var myAssignableType in myAssignableTypes) - { - if (!myAssignableType.IsSameBackend(other)) - continue; - - if (myAssignableType.Generics.Length != other.Generics.Length) - throw new Exception("Unable to compare types with different number of generics, this should never happen since the BackendType is identical"); - - bool isAssignable = true; - changedGenericsLocal.Clear(); // reuse the same dictionary for optimisation purposes - for (int i = 0; i < myAssignableType.Generics.Length; i++) - { - // check if either, both or none of the current and other type are undefined generics - if (myAssignableType.Generics[i] is UndefinedGenericType currentUndefinedGeneric) - { - if (myAssignableType.Generics[i] is UndefinedGenericType) - continue; // this works out fine, we're plugging a generic into another generic - else // we're plugging a generic into a non generic type, just we record that and keep checking the others - { - changedGenericsLocal[currentUndefinedGeneric] = myAssignableType.Generics[i]; - continue; - } - } - else if (other.Generics[i] is UndefinedGenericType otherUndefinedGeneric) - { - // we know we're not a generic, so we're plugging a type into a generic - changedGenericsLocal[otherUndefinedGeneric] = myAssignableType.Generics[i]; - continue; - } - - bool checkParents = true; - TypeBase left, right; - // let's check if they have the same backend type, like List and List share the List<> backend type - // if they are 2 types that could be assigned only by looking at their BaseType or interfaces (Like List to IEnumerable), that will be checked later - if (!myAssignableType.Generics[i].IsSameBackend(other.Generics[i])) - { - // Check if the types are assignable even if the generic isn't the same - // By example, List can be assigned to IEnumerable even though ParentClass and ChildClass aren't the same - if (other.IsOut(i)) - { - // we have to check if otherAssignableType.Generics[i] is less derived than myAssignableType.Generics[i] - left = myAssignableType.Generics[i]; - right = other.Generics[i]; - } - else if (other.IsIn(i)) - { - // we have to check if otherAssignableType.Generics[i] is more derived than myAssignableType.Generics[i] - left = other.Generics[i]; - right = myAssignableType.Generics[i]; - } - else - { - // we have to check if otherAssignableType.Generics[i] is the same as myAssignableType.Generics[i] - left = myAssignableType.Generics[i]; - right = other.Generics[i]; - checkParents = false; - } - } - else - { - left = myAssignableType.Generics[i]; - right = other.Generics[i]; - checkParents = false; - } - - if (checkParents) - { - if (left.IsAssignableTo(right, out var changedGenericsLocally)) - { - foreach (var changed in changedGenericsLocally) - changedGenericsLocal[changed.Key] = changed.Value; - continue; - } - } - else - { - if (left.IsDirectlyAssignableTo(right, false, out var changedGenericsLocally)) - { - foreach (var changed in changedGenericsLocally) - changedGenericsLocal[changed.Key] = changed.Value; - continue; - } - } - - - isAssignable = false; - break; - } - - if (isAssignable) - { - changedGenerics = changedGenericsLocal; - return true; - } - } - - - changedGenerics = null; - return false; - } - - #endregion - - /// - /// If true, We can plug any less derived type. Ex IComparer can be assigned to IComparer even though a person is not necessarily an employee. This one is unusual - /// - public virtual bool IsIn(int genericIndex) => false; - - /// - /// If true, we can plug any derived type. Ex List ca be assigned to IEnumerable - /// - public virtual bool IsOut(int genericIndex) => false; - - /// - /// Returns a list of types that this type can be assigned to - /// - public IEnumerable<(TypeBase Type, int Depth)> GetAssignableTypes() - { - return GetAssignableTypes(0).DistinctBy(x => x.Type); - } - - private IEnumerable<(TypeBase Type, int Depth)> GetAssignableTypes(int depth) - { - yield return (this, depth); - - if (BaseType != null) - { - foreach (var baseType in BaseType.GetAssignableTypes(depth + 1)) - yield return baseType; - } - - foreach (var @interface in Interfaces) - { - foreach (var interfaceType in @interface.GetAssignableTypes(depth + 1)) - yield return interfaceType; - } - } - - public abstract IEnumerable GetMethods(); - - public abstract IEnumerable GetMethods(string name); - - public static TypeBase DeserializeFullTypeNameString(TypeFactory typeFactory, string serializedTypeStr) - { - var serializedType = JsonSerializer.Deserialize(serializedTypeStr) ?? throw new Exception("Unable to deserialize type"); + public abstract TypeBase[] Interfaces { get; } + + public abstract bool IsArray { get; } + + public abstract TypeBase ArrayType { get; } + + public abstract TypeBase ArrayInnerType { get; } + + public bool HasUndefinedGenerics => Generics.Any(x => x is UndefinedGenericType || x.HasUndefinedGenerics); + + public virtual bool IsExec => false; + + public virtual bool AllowTextboxEdit => false; + + public virtual string? DefaultTextboxValue => null; + + public abstract string FriendlyName { get; } + + internal protected abstract string Serialize(); + + public abstract Type MakeRealType(); + + public abstract TypeBase CloneWithGenerics(TypeBase[] newGenerics); + + public virtual object? ParseTextboxEdit(string text) => throw new NotImplementedException(); + + public abstract IEnumerable GetMembers(); + + public record class SerializedType(string TypeFullName, string SerializedTypeCustom); + public SerializedType SerializeWithFullTypeName() + { + var serializedType = new SerializedType(GetType().FullName!, Serialize()); + + return serializedType; + } + public string SerializeWithFullTypeNameString() => JsonSerializer.Serialize(SerializeWithFullTypeName()); + + public IEnumerable GetUndefinedGenericTypes() + { + IEnumerable undefinedGenericTypes = this is UndefinedGenericType undefinedGenericType ? [undefinedGenericType.UndefinedGenericTypeName] : []; + + return undefinedGenericTypes.Concat(Generics.SelectMany(x => x.GetUndefinedGenericTypes())).Distinct(); + } + + public TypeBase ReplaceUndefinedGeneric(IReadOnlyDictionary genericTypes) + { + if (this is UndefinedGenericType undefinedGeneric) + { + if (!genericTypes.TryGetValue(undefinedGeneric.UndefinedGenericTypeName, out var newType)) + return undefinedGeneric; // put back the undefined generic if we didn't find a replacement + + // We know what 'T' is matched with. Now if we have T[], we have to increase the array level of the matched type too. + for (int i = 0; i < undefinedGeneric.NbArrayLevels; ++i) + newType = newType.ArrayType; + + return newType; + } + + var generics = new TypeBase[Generics.Length]; + + for (int i = 0; i < Generics.Length; ++i) + generics[i] = Generics[i].ReplaceUndefinedGeneric(genericTypes); // ask a more complex type to replace its own undefined generics + + return CloneWithGenerics(generics); + } + + #region Assignation checks + + /// + /// Returns in the backend type is the same, ignoring generics + /// For RealType, that means the actual 'Type' Backend + /// + /// + /// + public abstract bool IsSameBackend(TypeBase typeBase); + + /// + /// Validate if 'this' is directly assignable to 'other' without checkout the entire hierarchy of inheritance and interface implementations + /// + /// Other type we are trying to plug into + /// Check for covariant and contravariant generics + /// Generics that needs to be updated in 'this' in order for the assignation to work + /// Generics that needs to be updated in 'other' in order for the assignation to work + /// + internal bool IsDirectlyAssignableTo(TypeBase other, bool allowInOutGenerics, [MaybeNullWhen(false)] out Dictionary changedGenericsLeft, [MaybeNullWhen(false)] out Dictionary changedGenericsRight, out int totalDepths) + { + if (this is UndefinedGenericType thisUndefinedGenericType) + { + if (other is UndefinedGenericType) + { + changedGenericsLeft = []; // nothing to change, we're plugging a generic into another generic + changedGenericsRight = []; + totalDepths = 0; + return true; + } + else if (!IsArray || other.IsArray) // we can change 'this' to the same type as 'other' and plug into it + { + // We are either plugging: + // T into string[], T into string or T[] into string[] + changedGenericsLeft = new() + { + [thisUndefinedGenericType.UndefinedGenericTypeName] = thisUndefinedGenericType.SimplifyToMatchWith(other) + }; + changedGenericsRight = []; + totalDepths = 0; + return true; + } + + changedGenericsLeft = changedGenericsRight = null; + totalDepths = -1; + return false; + } + else if (other is UndefinedGenericType otherUndefinedGenericType) // we can change the other generic into the current type + { + if (IsArray || !other.IsArray) + { + // We are either plugging : string[] into T[] or string[] into T + // OR (the 'if' OR...) + // We are plugging string string into T + // In both cases we want to update the generic to the array type and let the system handle the rest later on + changedGenericsLeft = []; + changedGenericsRight = new() + { + [otherUndefinedGenericType.UndefinedGenericTypeName] = otherUndefinedGenericType.SimplifyToMatchWith(this) + }; + totalDepths = 0; + return true; + } + + // string into T[] <----- this is not allowed + changedGenericsLeft = changedGenericsRight = null; + totalDepths = -1; + return false; + } + + if (IsSameBackend(other)) // same backend, List would be the same backend as List or List + { + // check all the generics, they have to either be undefined, the same or covariant + changedGenericsLeft = []; + changedGenericsRight = []; + totalDepths = 0; + for (int i = 0; i < Generics.Length; ++i) + { + if (allowInOutGenerics && other.IsOut(i)) // we can plug a less derived type, like IEnumerable to IEnumerable + { + if (Generics[i].IsAssignableTo(other.Generics[i], out var changedGenericsLeftLocal, out var changedGenericsRightLocal, out var totalSubDepths)) + { + totalDepths += totalSubDepths; + + foreach (var changed in changedGenericsLeftLocal) + changedGenericsLeft[changed.Key] = changed.Value; + foreach (var changed in changedGenericsRightLocal) + changedGenericsRight[changed.Key] = changed.Value; + continue; // it worked, check the next generic + } + } + else if (allowInOutGenerics && other.IsIn(i)) // We can plug a more derived type, like IComparable to IComparable + { + // Invert the 'changedGenerics' left and right here, since we're plugging 'other' into 'this' this time + if (other.Generics[i].IsAssignableTo(Generics[i], out var changedGenericsRightLocal, out var changedGenericsLeftLocal, out var totalSubDepths)) + { + totalDepths += totalSubDepths; + + foreach (var changed in changedGenericsLeftLocal) + changedGenericsLeft[changed.Key] = changed.Value; + foreach (var changed in changedGenericsRightLocal) + changedGenericsRight[changed.Key] = changed.Value; + continue; // it worked, check the next generic + } + } + else + { + // the generic is not covariant, so it has to be the same + if (Generics[i].IsDirectlyAssignableTo(other.Generics[i], allowInOutGenerics, out var changedGenericsLeftLocal, out var changedGenericsRightLocal, out var totalSubDepths)) + { + totalDepths += totalSubDepths; + + foreach (var changed in changedGenericsLeftLocal) + changedGenericsLeft[changed.Key] = changed.Value; + foreach (var changed in changedGenericsRightLocal) + changedGenericsRight[changed.Key] = changed.Value; + continue; // it worked, check the next generic + } + } + + // one of the generics is not assignable, so we're not assignable + changedGenericsLeft = changedGenericsRight = null; + totalDepths = -1; + return false; + } + + return true; + } + + changedGenericsLeft = changedGenericsRight = null; + totalDepths = -1; + return false; + } + + public bool IsAssignableTo(TypeBase other, [MaybeNullWhen(false)] out Dictionary changedGenericsLeft, [MaybeNullWhen(false)] out Dictionary changedGenericsRight) + { + return IsAssignableTo(other, out changedGenericsLeft, out changedGenericsRight, out _); + } + + /// + /// Check if 'this' is assignable to 'other' and return the depth of the assignation. + /// Also return the generics that needs to be updated in order for the assignation to work. + /// + /// + /// Total depths of the assignation. That is, how far up the inheritance tree did we need to go to make this assignation work. + /// It also sums up the depths of the generics assignations. + /// This is use to prioritize assignations of lower depth, such as converting List to IList instead of IEnumerable, if possible. + /// + /// True if the assignation is possible. Then and are both set. + public bool IsAssignableTo(TypeBase other, [MaybeNullWhen(false)] out Dictionary changedGenericsLeft, [MaybeNullWhen(false)] out Dictionary changedGenericsRight, out int totalDepths) + { + if ((this is ExecType && other is not ExecType) || (other is ExecType && this is not ExecType)) + { + changedGenericsLeft = changedGenericsRight = null; + totalDepths = -1; + return false; + } + + if (IsDirectlyAssignableTo(other, true, out changedGenericsLeft, out changedGenericsRight, out var totalSubDepths)) + { + totalDepths = totalSubDepths; + return true; // Either plugging something easy like List to List, some generic or covariant like IEnumerable to IEnumerable + } + + var myAssignableTypes = GetAssignableTypes().OrderBy(x => x.Depth); + + var changedGenericsLeftLocal = new Dictionary(); + var changedGenericsRightLocal = new Dictionary(); + foreach ((var myAssignableType, var assignableDepth) in myAssignableTypes) + { + if (!myAssignableType.IsSameBackend(other)) + continue; + + if (myAssignableType.Generics.Length != other.Generics.Length) + throw new Exception("Unable to compare types with different number of generics, this should never happen since the BackendType is identical"); + + var isAssignable = true; + changedGenericsLeftLocal.Clear(); // reuse the same dictionary for optimization purposes + changedGenericsRightLocal.Clear(); // reuse the same dictionary for optimization purposes + var totalDepthsLocal = 0; + for (int i = 0; i < myAssignableType.Generics.Length; i++) + { + // check if either, both or none of the current and other type are undefined generics + if (myAssignableType.Generics[i] is UndefinedGenericType currentUndefinedGeneric) + { + if (other.Generics[i] is UndefinedGenericType) + continue; // this works out fine, we're plugging a generic into another generic + else + { + // we're plugging a generic into a non generic type, we just record that and keep checking the others + // however this only works if we're either plugging : + // T into string or T[] into string[] + if (currentUndefinedGeneric.IsArray && !other.Generics[i].IsArray) + { + isAssignable = false; + break; + } + else + { + changedGenericsLeftLocal[currentUndefinedGeneric.UndefinedGenericTypeName] = currentUndefinedGeneric.SimplifyToMatchWith(other.Generics[i]); + continue; + } + } + } + else if (other.Generics[i] is UndefinedGenericType otherUndefinedGeneric) + { + // we know we're not a generic, so we're plugging a type into a generic + // However, this only works if we're either plugging : + // string into T or string[] into T[] + if (myAssignableType.IsArray && !other.Generics[i].IsArray) + { + isAssignable = false; + break; + } + + changedGenericsRightLocal[otherUndefinedGeneric.UndefinedGenericTypeName] = otherUndefinedGeneric.SimplifyToMatchWith(myAssignableType.Generics[i]); + continue; + } + + bool checkParents = true; + TypeBase left, right; + bool swapped = false; // true if left and right changed generics need to be swapped + // let's check if they have the same backend type, like List and List share the List<> backend type + // if they are 2 types that could be assigned only by looking at their BaseType or interfaces (Like List to IEnumerable), that will be checked later + if (!myAssignableType.Generics[i].IsSameBackend(other.Generics[i])) + { + // Check if the types are assignable even if the generic isn't the same + // By example, List can be assigned to IEnumerable even though ParentClass and ChildClass aren't the same + if (other.IsOut(i)) + { + // we have to check if otherAssignableType.Generics[i] is less derived than myAssignableType.Generics[i] + left = myAssignableType.Generics[i]; + right = other.Generics[i]; + } + else if (other.IsIn(i)) + { + // we have to check if otherAssignableType.Generics[i] is more derived than myAssignableType.Generics[i] + left = other.Generics[i]; + right = myAssignableType.Generics[i]; + swapped = true; + } + else + { + // we have to check if otherAssignableType.Generics[i] is the same as myAssignableType.Generics[i] + left = myAssignableType.Generics[i]; + right = other.Generics[i]; + checkParents = false; + } + } + else + { + // we have to check if otherAssignableType.Generics[i] is the same as myAssignableType.Generics[i] + left = myAssignableType.Generics[i]; + right = other.Generics[i]; + checkParents = false; + } + + // Check parents will recursively check if the left is assignable to the right, going up the inheritance tree + if (checkParents) + { + if (left.IsAssignableTo(right, out var changedGenericsLeftLocally, out var changedGenericsRightLocally, out var totalDepths1)) + { + totalDepthsLocal += totalDepths1; + + if (swapped) + { + var temp = changedGenericsLeftLocally; + changedGenericsLeftLocally = changedGenericsRightLocally; + changedGenericsRightLocally = temp; + } + + foreach (var changed in changedGenericsLeftLocally) + changedGenericsLeftLocal[changed.Key] = changed.Value; + foreach (var changed in changedGenericsRightLocally) + changedGenericsRightLocal[changed.Key] = changed.Value; + continue; + } + } + else + { + if (left.IsDirectlyAssignableTo(right, false, out var changedGenericsLeftLocally, out var changedGenericsRightLocally, out totalSubDepths)) + { + totalDepthsLocal += totalSubDepths; + + if (swapped) + { + var temp = changedGenericsLeftLocally; + changedGenericsLeftLocally = changedGenericsRightLocally; + changedGenericsRightLocally = temp; + } + + foreach (var changed in changedGenericsLeftLocally) + changedGenericsLeftLocal[changed.Key] = changed.Value; + foreach (var changed in changedGenericsRightLocally) + changedGenericsRightLocal[changed.Key] = changed.Value; + continue; + } + } + + + isAssignable = false; + break; + } + + if (isAssignable) + { + changedGenericsLeft = changedGenericsLeftLocal; + changedGenericsRight = changedGenericsRightLocal; + totalDepths = assignableDepth + 1 + totalDepthsLocal; + return true; + } + } + + + changedGenericsLeft = changedGenericsRight = null; + totalDepths = -1; + return false; + } + + #endregion + + /// + /// If true, We can plug any less derived type. Ex IComparer can be assigned to IComparer even though a person is not necessarily an employee. This one is unusual + /// + public virtual bool IsIn(int genericIndex) => false; + + /// + /// If true, we can plug any derived type. Ex List ca be assigned to IEnumerable + /// + public virtual bool IsOut(int genericIndex) => false; + + /// + /// Returns a list of types that this type can be assigned to + /// + public IEnumerable<(TypeBase Type, int Depth)> GetAssignableTypes() + { + return GetAssignableTypes(0).DistinctBy(x => x.Type); + } + + private IEnumerable<(TypeBase Type, int Depth)> GetAssignableTypes(int depth) + { + yield return (this, depth); + + if (BaseType != null) + { + foreach (var baseType in BaseType.GetAssignableTypes(depth + 1)) + yield return baseType; + } + + foreach (var @interface in Interfaces) + { + foreach (var interfaceType in @interface.GetAssignableTypes(depth + 1)) + yield return interfaceType; + } + } + + public abstract IEnumerable GetMethods(); + + public abstract IEnumerable GetMethods(string name); + + public static TypeBase DeserializeFullTypeNameString(TypeFactory typeFactory, string serializedTypeStr) + { + var serializedType = JsonSerializer.Deserialize(serializedTypeStr) ?? throw new Exception("Unable to deserialize type"); return Deserialize(typeFactory, serializedType); } - public static TypeBase Deserialize(TypeFactory typeFactory, SerializedType serializedType) + public static TypeBase Deserialize(TypeFactory typeFactory, SerializedType serializedType) { - var type = typeFactory.GetTypeByFullName(serializedType.TypeFullName) ?? throw new Exception($"Type not found: {serializedType.TypeFullName}"); + var type = typeFactory.GetTypeByFullName(serializedType.TypeFullName) ?? throw new Exception($"Type not found: {serializedType.TypeFullName}"); - var deserializeMethod = type.GetMethod("Deserialize", BindingFlags.Public | BindingFlags.Static) ?? throw new Exception($"Deserialize method not found in type: {serializedType.TypeFullName}"); + var deserializeMethod = type.GetMethod("Deserialize", BindingFlags.Public | BindingFlags.Static) ?? throw new Exception($"Deserialize method not found in type: {serializedType.TypeFullName}"); - var deserializedType = deserializeMethod.Invoke(null, new object[] { typeFactory, serializedType.SerializedTypeCustom }); + var deserializedType = deserializeMethod.Invoke(null, new object[] { typeFactory, serializedType.SerializedTypeCustom }); - if (deserializedType is TypeBase typeBase) - return typeBase; + if (deserializedType is TypeBase typeBase) + return typeBase; - throw new Exception($"Deserialize method in type {serializedType.TypeFullName} returned invalid type"); - } + throw new Exception($"Deserialize method in type {serializedType.TypeFullName} returned invalid type"); + } } diff --git a/src/NodeDev.Core/Types/TypeFactory.cs b/src/NodeDev.Core/Types/TypeFactory.cs index b448a9c..f4c4228 100644 --- a/src/NodeDev.Core/Types/TypeFactory.cs +++ b/src/NodeDev.Core/Types/TypeFactory.cs @@ -96,6 +96,13 @@ public RealType Get(Type type, TypeBase[]? generics) public string? CreateBaseFromUserInput(string typeName, out TypeBase? type) { + int nbArray = 0; + while(typeName.EndsWith("[]")) + { + ++nbArray; + typeName = typeName[..^2]; + } + typeName = typeName.Replace(" ", ""); if (typeName.Count(c => c == '<') != typeName.Count(c => c == '>')) { @@ -119,6 +126,9 @@ public RealType Get(Type type, TypeBase[]? generics) else if (currentRealType != null) { type = Get(currentRealType, null); + for (int i = 0; i < nbArray; ++i) + type = type.ArrayType; + return null; } else if (currentRealType == null) @@ -127,7 +137,10 @@ public RealType Get(Type type, TypeBase[]? generics) if (nodeClass != null) { type = nodeClass.ClassTypeBase; - return null; + for (int i = 0; i < nbArray; ++i) + type = type.ArrayType; + + return null; } } @@ -166,7 +179,10 @@ public RealType Get(Type type, TypeBase[]? generics) // create the generic type type = Get(baseType, genericArgsTypes); - return null; + for (int i = 0; i < nbArray; ++i) + type = type.ArrayType; + + return null; } #endregion diff --git a/src/NodeDev.Core/Types/UndefinedGenericType.cs b/src/NodeDev.Core/Types/UndefinedGenericType.cs index b45a46a..ef1f829 100644 --- a/src/NodeDev.Core/Types/UndefinedGenericType.cs +++ b/src/NodeDev.Core/Types/UndefinedGenericType.cs @@ -6,50 +6,87 @@ namespace NodeDev.Core.Types; public class UndefinedGenericType : TypeBase { - private record class SerializedUndefinedGenericType(string Name); + private record class SerializedUndefinedGenericType(string Name, int NbArrayLevels = 0); + public int NbArrayLevels { get; } - public override string Name { get; } + public override string Name { get; } - public override string FullName { get; } + public override string FullName { get; } - public override TypeBase[] Generics => Array.Empty(); + public override TypeBase[] Generics => []; - public override string FriendlyName => Name; + public override string FriendlyName => Name; - public override TypeBase? BaseType => throw new NotImplementedException(); + public override TypeBase? BaseType => throw new NotImplementedException(); - public override TypeBase[] Interfaces => throw new NotImplementedException(); + public override TypeBase[] Interfaces => throw new NotImplementedException(); - public override TypeBase CloneWithGenerics(TypeBase[] newGenerics) => throw new NotImplementedException(); + public override TypeBase CloneWithGenerics(TypeBase[] newGenerics) => throw new NotImplementedException(); - public override IEnumerable GetMembers() => throw new NotImplementedException(); + public override IEnumerable GetMembers() => throw new NotImplementedException(); - public UndefinedGenericType(string name) - { - FullName = Name = name; - } + public override bool IsArray => NbArrayLevels != 0; - public override IEnumerable GetMethods() => []; + public override UndefinedGenericType ArrayType => new(UndefinedGenericTypeName, NbArrayLevels + 1); - public override IEnumerable GetMethods(string name) => []; + public override UndefinedGenericType ArrayInnerType => IsArray ? new UndefinedGenericType(UndefinedGenericTypeName, NbArrayLevels - 1) : throw new Exception("Can't call ArrayInnerType on non-array type"); - internal protected override string Serialize() => JsonSerializer.Serialize(new SerializedUndefinedGenericType(Name)); + public readonly string UndefinedGenericTypeName; - public new static UndefinedGenericType Deserialize(TypeFactory typeFactory, string serialized) - { - var deserialized = JsonSerializer.Deserialize(serialized) ?? throw new Exception("Unable to deserialize UndefinedGenericType"); + public UndefinedGenericType(string name, int nbArrayLevels = 0) + { + UndefinedGenericTypeName = name; + FullName = Name = name + NodeClassArrayType.GetArrayString(nbArrayLevels); + NbArrayLevels = nbArrayLevels; + } - return new(deserialized.Name); - } + /// + /// Simplifies the current undefined generic to match as easily as possible with the other type. + /// T[] to string[] will return string. T to string[] will return string[]. + /// + public TypeBase SimplifyToMatchWith(TypeBase otherType) + { + var thisUndefined = this; + while(thisUndefined.IsArray) + { + if(!otherType.IsArray) + throw new Exception("Can't simplify array to non-array type"); - public override Type MakeRealType() - { - throw new Exception("Unable to make real type with undefined generics"); - } + thisUndefined = thisUndefined.ArrayInnerType; + otherType = otherType.ArrayInnerType; + } - public override bool IsSameBackend(TypeBase typeBase) - { - return Name == typeBase.Name; - } + return otherType; + } + + + public override IEnumerable GetMethods() => []; + + public override IEnumerable GetMethods(string name) => []; + + #region Serialize / Deserialize + + internal protected override string Serialize() => JsonSerializer.Serialize(new SerializedUndefinedGenericType(UndefinedGenericTypeName, NbArrayLevels)); + public static UndefinedGenericType Deserialize(TypeFactory typeFactory, string serialized) + { + var deserialized = JsonSerializer.Deserialize(serialized) ?? throw new Exception("Unable to deserialize UndefinedGenericType"); + + return new UndefinedGenericType(deserialized.Name, deserialized.NbArrayLevels); + } + + #endregion + + public override Type MakeRealType() + { + throw new Exception("Unable to make real type with undefined generics"); + } + + public override bool IsSameBackend(TypeBase typeBase) + { + if (typeBase is not UndefinedGenericType undefinedGenericType) + return false; + + return Name == undefinedGenericType.Name; + } } diff --git a/src/NodeDev.EndToEndTests/Hooks/Hooks.cs b/src/NodeDev.EndToEndTests/Hooks/Hooks.cs index f7564c2..6afad71 100644 --- a/src/NodeDev.EndToEndTests/Hooks/Hooks.cs +++ b/src/NodeDev.EndToEndTests/Hooks/Hooks.cs @@ -87,7 +87,14 @@ public async Task RegisterSingleInstancePractitioner() Headless = Environment.GetEnvironmentVariable("HEADLESS") == "true" // -> Use this option to be able to see your test running }); //Setup a browser context - var context1 = await browser.NewContextAsync(); + var context1 = await browser.NewContextAsync(new() + { + ViewportSize = new() + { + Width = 1900, + Height = 1000 + } + }); //Initialise a page on the browser context. User = await context1.NewPageAsync(); diff --git a/src/NodeDev.EndToEndTests/NodeDev.EndToEndTests.csproj b/src/NodeDev.EndToEndTests/NodeDev.EndToEndTests.csproj index 03e19a0..674e17d 100644 --- a/src/NodeDev.EndToEndTests/NodeDev.EndToEndTests.csproj +++ b/src/NodeDev.EndToEndTests/NodeDev.EndToEndTests.csproj @@ -6,6 +6,19 @@ enable + + + + + + + + + + + + + @@ -15,11 +28,6 @@ - - - - - diff --git a/src/NodeDev.Tests/EventsTests.cs b/src/NodeDev.Tests/EventsTests.cs index 7e69c80..7a6fac2 100644 --- a/src/NodeDev.Tests/EventsTests.cs +++ b/src/NodeDev.Tests/EventsTests.cs @@ -31,7 +31,7 @@ public void TestPropertyRenameAndTypeChange() returnNode.Inputs.Add(new("Result", entryNode, myClass.TypeFactory.Get())); var newNode = new New(graph); - newNode.Outputs[1].UpdateType(myClass.ClassTypeBase); + newNode.Outputs[1].UpdateTypeAndTextboxVisibility(myClass.ClassTypeBase, overrideInitialType: true); var setProp = new SetPropertyOrField(graph); setProp.SetMemberTarget(prop); diff --git a/src/NodeDev.Tests/GraphExecutorTests.cs b/src/NodeDev.Tests/GraphExecutorTests.cs index 9caa6e8..fb7c80b 100644 --- a/src/NodeDev.Tests/GraphExecutorTests.cs +++ b/src/NodeDev.Tests/GraphExecutorTests.cs @@ -29,9 +29,9 @@ public static Graph CreateSimpleAddGraph(out Core.Nodes.Flow.EntryNod addNode = new Core.Nodes.Math.Add(graph); - addNode.Inputs[0].UpdateType(nodeClass.TypeFactory.Get()); - addNode.Inputs[1].UpdateType(nodeClass.TypeFactory.Get()); - addNode.Outputs[0].UpdateType(nodeClass.TypeFactory.Get()); + addNode.Inputs[0].UpdateTypeAndTextboxVisibility(nodeClass.TypeFactory.Get(), overrideInitialType: true); + addNode.Inputs[1].UpdateTypeAndTextboxVisibility(nodeClass.TypeFactory.Get(), overrideInitialType: true); + addNode.Outputs[0].UpdateTypeAndTextboxVisibility(nodeClass.TypeFactory.Get(), overrideInitialType: true); graph.AddNode(entryNode, false); graph.AddNode(addNode, false); @@ -80,8 +80,8 @@ public void TestBranch(SerializableBuildOptions options) returnNode1.Inputs[1].UpdateTextboxText("1"); var smallerThan = new Core.Nodes.Math.SmallerThan(graph); - smallerThan.Inputs[0].UpdateType(graph.SelfClass.TypeFactory.Get()); - smallerThan.Inputs[1].UpdateType(graph.SelfClass.TypeFactory.Get()); + smallerThan.Inputs[0].UpdateTypeAndTextboxVisibility(graph.SelfClass.TypeFactory.Get(), overrideInitialType: true); + smallerThan.Inputs[1].UpdateTypeAndTextboxVisibility(graph.SelfClass.TypeFactory.Get(), overrideInitialType: true); smallerThan.Inputs[1].UpdateTextboxText("0"); graph.AddNode(smallerThan, false); graph.Connect(addNode.Outputs[0], smallerThan.Inputs[0], false); @@ -117,8 +117,8 @@ public void TestProjectRun(SerializableBuildOptions options) var smallerThan = new Core.Nodes.Math.SmallerThan(graph); graph.AddNode(smallerThan, false); - smallerThan.Inputs[0].UpdateType(graph.SelfClass.TypeFactory.Get()); - smallerThan.Inputs[1].UpdateType(graph.SelfClass.TypeFactory.Get()); + smallerThan.Inputs[0].UpdateTypeAndTextboxVisibility(graph.SelfClass.TypeFactory.Get(), overrideInitialType: true); + smallerThan.Inputs[1].UpdateTypeAndTextboxVisibility(graph.SelfClass.TypeFactory.Get(), overrideInitialType: true); smallerThan.Inputs[1].UpdateTextboxText("0"); graph.Connect(addNode.Outputs[0], smallerThan.Inputs[0], false); diff --git a/src/NodeDev.Tests/GraphManagerServiceTests.cs b/src/NodeDev.Tests/GraphManagerServiceTests.cs new file mode 100644 index 0000000..64680a4 --- /dev/null +++ b/src/NodeDev.Tests/GraphManagerServiceTests.cs @@ -0,0 +1,237 @@ +using NodeDev.Blazor.Services.GraphManager; +using NodeDev.Core; +using NodeDev.Core.Connections; +using NodeDev.Core.Nodes; +using NodeDev.Core.Nodes.Flow; +using NSubstitute; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NodeDev.Tests; + +public class GraphManagerServiceTests : NodeDevTestsBase +{ + [Fact] + public void ConnectTwoExecInOneOutput_ShouldDisconnectFirstExec() + { + var project = Project.CreateNewDefaultProject(out var main); + Assert.NotNull(main.EntryNode); + Assert.Single(main.ReturnNodes); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + + // create fake IGraphCanvas + var graphCanvas = Substitute.For(); + graphCanvas.Graph.Returns(main.Graph); + + var graphManager = new GraphManagerService(graphCanvas); + + // create a random method call used to test the connection + var methodCall = new MethodCall(main.Graph); + main.Graph.AddNode(methodCall, false); + + + // This should also disconnect the entry node's existing exec connection + graphManager.AddNewConnectionBetween(main.EntryNode.Outputs[0], methodCall.Inputs[0]); + + // main entry node was disconnected from the other node and is now connected to the method call + Assert.Single(main.EntryNode.Outputs[0].Connections); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], methodCall.Inputs[0]); + + // return node is not connected to anything + Assert.Empty(main.ReturnNodes.Single().Inputs[0].Connections); + + // check that each connection was updated + graphCanvas.Received().UpdatePortColor(Arg.Is(main.EntryNode.Outputs[0].Connections[0])); + graphCanvas.Received().UpdatePortColor(Arg.Is(methodCall.Inputs[0])); + graphCanvas.Received().UpdatePortColor(Arg.Is(main.ReturnNodes.Single().Inputs[0])); + + // check that the old connection was removed from the graph canvas + graphCanvas.Received().RemoveLinkFromGraphCanvas(Arg.Is(main.EntryNode.Outputs[0]), Arg.Is(main.ReturnNodes.Single().Inputs[0])); + } + + [Fact] + public void ConnectTwoOutputsInOneInput_ShouldDisconnectFirstOutput() + { + var project = Project.CreateNewDefaultProject(out var main); + Assert.NotNull(main.EntryNode); + Assert.Single(main.ReturnNodes); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + + // create fake IGraphCanvas + var graphCanvas = Substitute.For(); + graphCanvas.Graph.Returns(main.Graph); + + var graphManager = new GraphManagerService(graphCanvas); + + var addNode1 = AddNewAddNodeToGraph(main.Graph); + var addNode2 = AddNewAddNodeToGraph(main.Graph); + var addNode3 = AddNewAddNodeToGraph(main.Graph); + + // connect output of addNode1 to input of addNode3 + graphManager.AddNewConnectionBetween(addNode1.Outputs[0], addNode3.Inputs[0]); + graphCanvas.Received().UpdatePortColor(Arg.Is(addNode1.Outputs[0])); + graphCanvas.Received().UpdatePortColor(Arg.Is(addNode3.Inputs[0])); + Assert.Single(addNode1.Outputs[0].Connections); + Assert.Single(addNode3.Inputs[0].Connections); + Assert.Equal(addNode1.Outputs[0].Connections[0], addNode3.Inputs[0]); + + // connect output of addNode2 to input of addNode3. It should disconnect the existing connection + graphManager.AddNewConnectionBetween(addNode2.Outputs[0], addNode3.Inputs[0]); + graphCanvas.Received(2).UpdatePortColor(Arg.Is(addNode1.Outputs[0])); // when first adding, then when disconnecting + graphCanvas.Received(1).UpdatePortColor(Arg.Is(addNode2.Outputs[0])); + graphCanvas.Received(3).UpdatePortColor(Arg.Is(addNode3.Inputs[0])); // when first adding, adding a second time, then disconnecting + Assert.Empty(addNode1.Outputs[0].Connections); + Assert.Single(addNode2.Outputs[0].Connections); + Assert.Single(addNode3.Inputs[0].Connections); + Assert.Equal(addNode2.Outputs[0].Connections[0], addNode3.Inputs[0]); + + graphCanvas.Received().RemoveLinkFromGraphCanvas(Arg.Is(addNode1.Outputs[0]), Arg.Is(addNode3.Inputs[0])); + } + + [Fact] + public void ConnectTwoExecOutputsInOneInput_ShouldAllow() + { + var project = Project.CreateNewDefaultProject(out var main); + Assert.NotNull(main.EntryNode); + Assert.Single(main.ReturnNodes); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + + // create fake IGraphCanvas + var graphCanvas = Substitute.For(); + graphCanvas.Graph.Returns(main.Graph); + + var graphManager = new GraphManagerService(graphCanvas); + + // create a random method call used to test the connection + var methodCall = new MethodCall(main.Graph); + main.Graph.AddNode(methodCall, false); + + // connect output of addNode1 to input of addNode3 + graphManager.AddNewConnectionBetween(methodCall.Outputs[0], main.ReturnNodes.Single().Inputs[0]); + graphCanvas.Received().UpdatePortColor(Arg.Is(methodCall.Outputs[0])); + graphCanvas.Received().UpdatePortColor(Arg.Is(main.ReturnNodes.Single().Inputs[0])); + Assert.Single(main.EntryNode.Outputs[0].Connections); + Assert.Equal(2, main.ReturnNodes.Single().Inputs[0].Connections.Count); + Assert.Single(methodCall.Outputs[0].Connections); + Assert.Equal(methodCall.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + graphCanvas.DidNotReceiveWithAnyArgs().RemoveLinkFromGraphCanvas(Arg.Any(), Arg.Any()); + } + + [Fact] + public void ConnectArrayToIEnumerableT_ShouldAllow() + { + var project = Project.CreateNewDefaultProject(out var main); + Assert.NotNull(main.EntryNode); + Assert.Single(main.ReturnNodes); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + + var typeFactory = main.TypeFactory; + + // create fake IGraphCanvas + var graphCanvas = Substitute.For(); + graphCanvas.Graph.Returns(main.Graph); + + var graphManager = new GraphManagerService(graphCanvas); + + // create a random method call used to test the connection + var methodCall = AddMethodCall(main.Graph, typeFactory.Get(), nameof(Array.Empty)); + methodCall.Outputs[1].UpdateTypeAndTextboxVisibility(typeFactory.Get(), overrideInitialType: true); + + var foreachNode = new ForeachNode(main.Graph); + main.Graph.AddNode(foreachNode, false); + + // connect output of Array.Empty() to input of foreachNode + graphManager.AddNewConnectionBetween(methodCall.Outputs[1], foreachNode.Inputs[1]); + graphCanvas.Received().UpdatePortColor(Arg.Is(methodCall.Outputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(foreachNode.Inputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(foreachNode.Outputs[1])); + Assert.Equal(typeFactory.Get>(), foreachNode.Inputs[1].Type); + Assert.Equal(typeFactory.Get(), foreachNode.Outputs[1].Type); + } + + [Fact] + public void ConnectListArrayToForeach_ShouldPropagateChange() + { + var project = Project.CreateNewDefaultProject(out var main); + Assert.NotNull(main.EntryNode); + Assert.Single(main.ReturnNodes); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + + var typeFactory = main.TypeFactory; + + // create fake IGraphCanvas + var graphCanvas = Substitute.For(); + graphCanvas.Graph.Returns(main.Graph); + + var graphManager = new GraphManagerService(graphCanvas); + + // create a random method call used to test the connection + var newListArray = new New(main.Graph); + newListArray.Outputs[1].UpdateTypeAndTextboxVisibility(typeFactory.Get>(), overrideInitialType: true); + newListArray.GenericConnectionTypeDefined(newListArray.Outputs[1]); + + var foreachNode = new ForeachNode(main.Graph); + main.Graph.AddNode(foreachNode, false); + + var foreachNode2 = new ForeachNode(main.Graph); + main.Graph.AddNode(foreachNode2, false); + + // connect output of foreachNode into input of foreachNode2 + graphManager.AddNewConnectionBetween(foreachNode.Outputs[1], foreachNode2.Inputs[1]); + + // connect output of new List to input of foreachNode + graphManager.AddNewConnectionBetween(newListArray.Outputs[1], foreachNode.Inputs[1]); + graphCanvas.Received().UpdatePortColor(Arg.Is(newListArray.Outputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(foreachNode.Inputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(foreachNode.Outputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(foreachNode2.Inputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(foreachNode2.Outputs[1])); + + // Input of foreach node should be IEnumerable, output should be string[] + Assert.Equal(typeFactory.Get>(), foreachNode.Inputs[1].Type); + Assert.Equal(typeFactory.Get(), foreachNode.Outputs[1].Type); + + // Input of foreach node 2 should be string[], output should be string + Assert.Equal(typeFactory.Get>(), foreachNode2.Inputs[1].Type); + Assert.Equal(typeFactory.Get(), foreachNode2.Outputs[1].Type); + } + + [Fact] + public void ConnectArrayToArrayT_ShouldPropagateChange() + { + var project = Project.CreateNewDefaultProject(out var main); + Assert.NotNull(main.EntryNode); + Assert.Single(main.ReturnNodes); + Assert.Equal(main.EntryNode.Outputs[0].Connections[0], main.ReturnNodes.Single().Inputs[0]); + + var typeFactory = main.TypeFactory; + + // create fake IGraphCanvas + var graphCanvas = Substitute.For(); + graphCanvas.Graph.Returns(main.Graph); + + var graphManager = new GraphManagerService(graphCanvas); + + // output string[] + var newArray = new New(main.Graph); + newArray.Outputs[1].UpdateTypeAndTextboxVisibility(typeFactory.Get(), overrideInitialType: true); + newArray.GenericConnectionTypeDefined(newArray.Outputs[1]); + + var arrayGet = new ArrayGet(main.Graph); + main.Graph.AddNode(arrayGet, false); + + // connect output of foreachNode into input of foreachNode2 + graphManager.AddNewConnectionBetween(newArray.Outputs[1], arrayGet.Inputs[0]); + + graphCanvas.Received().UpdatePortColor(Arg.Is(newArray.Outputs[1])); + graphCanvas.Received().UpdatePortColor(Arg.Is(arrayGet.Inputs[0])); + graphCanvas.Received().UpdatePortColor(Arg.Is(arrayGet.Outputs[0])); + + // Input of arrayGet should be string[], output should be string + Assert.Equal(typeFactory.Get(), arrayGet.Inputs[0].Type); + Assert.Equal(typeFactory.Get(), arrayGet.Outputs[0].Type); + } +} diff --git a/src/NodeDev.Tests/NodeClassTypeCreatorTests.cs b/src/NodeDev.Tests/NodeClassTypeCreatorTests.cs index 86be9a3..cb299ef 100644 --- a/src/NodeDev.Tests/NodeClassTypeCreatorTests.cs +++ b/src/NodeDev.Tests/NodeClassTypeCreatorTests.cs @@ -3,6 +3,7 @@ using NodeDev.Core.Nodes; using NodeDev.Core.Nodes.Flow; using NodeDev.Core.Types; +using System.Reflection; namespace NodeDev.Tests; @@ -24,12 +25,22 @@ public void SimpleProjectTest(SerializableBuildOptions options) creator.CreateProjectClassesAndAssembly(); Assert.NotNull(creator.Assembly); - Assert.Single(creator.Assembly.DefinedTypes.Where(x => x.IsVisible)); - Assert.Contains(creator.Assembly.DefinedTypes, x => x.Name == "TestClass"); - var instance = creator.Assembly.CreateInstance(myClass.Name); + //var inMemoryAssemblyStream = new MemoryStream(); + //creator.Assembly.Save(inMemoryAssemblyStream); + //inMemoryAssemblyStream.Position = 0; - Assert.IsType(creator.GeneratedTypes[project.GetNodeClassType(myClass)].Type, instance); + //var assembly = Assembly.Load(inMemoryAssemblyStream.ToArray()); + + var assembly = creator.Assembly; + + Assert.Single(assembly.DefinedTypes, x => x.IsVisible); + Assert.Contains(assembly.DefinedTypes, x => x.Name == "TestClass"); + + var instance = assembly.CreateInstance(myClass.Name); + + Assert.NotNull(instance); + Assert.Equal(creator.GeneratedTypes[project.GetNodeClassType(myClass)].Type.FullName!, instance.GetType().FullName); } [Fact] @@ -75,7 +86,7 @@ public void TestNewGetSet(SerializableBuildOptions options) returnNode.Inputs.Add(new("Result", entryNode, myClass.TypeFactory.Get())); var newNode = new New(graph); - newNode.Outputs[1].UpdateType(myClass.ClassTypeBase); + newNode.Outputs[1].UpdateTypeAndTextboxVisibility(myClass.ClassTypeBase, overrideInitialType: true); var setProp = new SetPropertyOrField(graph); setProp.SetMemberTarget(prop); diff --git a/src/NodeDev.Tests/NodeDev.Tests.csproj b/src/NodeDev.Tests/NodeDev.Tests.csproj index 1443909..2959486 100644 --- a/src/NodeDev.Tests/NodeDev.Tests.csproj +++ b/src/NodeDev.Tests/NodeDev.Tests.csproj @@ -10,9 +10,10 @@ - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/NodeDev.Tests/NodeDevTestsBase.cs b/src/NodeDev.Tests/NodeDevTestsBase.cs new file mode 100644 index 0000000..75712c4 --- /dev/null +++ b/src/NodeDev.Tests/NodeDevTestsBase.cs @@ -0,0 +1,34 @@ +using NodeDev.Core; +using NodeDev.Core.Nodes; +using NodeDev.Core.Nodes.Math; +using NodeDev.Core.Types; + +namespace NodeDev.Tests; + +public class NodeDevTestsBase +{ + protected Add AddNewAddNodeToGraph(Graph graph) + { + var addNode = new Add(graph); + graph.AddNode(addNode, false); + + addNode.Inputs[0].UpdateTypeAndTextboxVisibility(graph.Project.TypeFactory.Get(), overrideInitialType: true); + addNode.Inputs[1].UpdateTypeAndTextboxVisibility(graph.Project.TypeFactory.Get(), overrideInitialType: true); + addNode.Outputs[0].UpdateTypeAndTextboxVisibility(graph.Project.TypeFactory.Get(), overrideInitialType: true); + + return addNode; + } + + protected MethodCall AddMethodCall(Graph graph, TypeBase type, string methodName, params TypeBase[] args) + { + var methods = type.GetMethods(methodName); + + var methodCall = new MethodCall(graph); + graph.AddNode(methodCall, false); + + var method = methods.First(x => x.GetParameters().Select(y => y.ParameterType).SequenceEqual(args)); + methodCall.SetMethodTarget(method); + + return methodCall; + } +} diff --git a/src/NodeDev.Tests/TypeBaseTests.cs b/src/NodeDev.Tests/TypeBaseTests.cs index b6d60ed..f0695ea 100644 --- a/src/NodeDev.Tests/TypeBaseTests.cs +++ b/src/NodeDev.Tests/TypeBaseTests.cs @@ -11,11 +11,11 @@ public void Generics_ReplaceUndefinedGeneric() var typeFactory = new TypeFactory(new(Guid.NewGuid())); var t = new UndefinedGenericType("T"); - var type = typeFactory.Get(typeof(List<>), new[] { t }); + var type = typeFactory.Get(typeof(List<>), [t]); - var newType = type.ReplaceUndefinedGeneric(new Dictionary() + var newType = type.ReplaceUndefinedGeneric(new Dictionary() { - [t] = typeFactory.Get() + [t.Name] = typeFactory.Get() }); Assert.False(newType.HasUndefinedGenerics); @@ -31,9 +31,9 @@ public void Assignations_GetAssignableTypes_Basic() var type = typeFactory.Get(typeof(List), null); - var assignables = type.GetAssignableTypes(); + var assignableTypes = type.GetAssignableTypes(); - var types = assignables.Select(x => x.Type.MakeRealType()).ToList(); + var types = assignableTypes.Select(x => x.Type.MakeRealType()).ToList(); Assert.Contains(typeof(IList), types); Assert.Contains(typeof(ICollection), types); @@ -56,41 +56,50 @@ public void Assignations_IsAssignableTo_Basic() var parentReadOnlyEnumerable = typeFactory.Get>>(); var parentListEnumerable = typeFactory.Get>>(); - Assert.True(child.IsAssignableTo(parent, out var changedGenerics)); - Assert.Empty(changedGenerics); + Assert.True(child.IsAssignableTo(parent, out var changedGenericsLeft, out var changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); - Assert.False(parent.IsAssignableTo(child, out changedGenerics)); - Assert.Null(changedGenerics); + Assert.False(parent.IsAssignableTo(child, out changedGenericsLeft, out changedGenericsRight)); + Assert.Null(changedGenericsLeft); + Assert.Null(changedGenericsRight); - Assert.True(childList.IsAssignableTo(parentEnumerable, out changedGenerics)); - Assert.Empty(changedGenerics); + Assert.True(childList.IsAssignableTo(parentEnumerable, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); - Assert.False(parentEnumerable.IsAssignableTo(childList, out changedGenerics)); - Assert.Null(changedGenerics); + Assert.False(parentEnumerable.IsAssignableTo(childList, out changedGenericsLeft, out changedGenericsRight)); + Assert.Null(changedGenericsLeft); + Assert.Null(changedGenericsRight); - Assert.True(childListList.IsAssignableTo(parentReadOnlyEnumerable, out changedGenerics)); - Assert.Empty(changedGenerics); + Assert.True(childListList.IsAssignableTo(parentReadOnlyEnumerable, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); - Assert.False(parentReadOnlyEnumerable.IsAssignableTo(childListList, out changedGenerics)); - Assert.Null(changedGenerics); + Assert.False(parentReadOnlyEnumerable.IsAssignableTo(childListList, out changedGenericsLeft, out changedGenericsRight)); + Assert.Null(changedGenericsLeft); + Assert.Null(changedGenericsRight); - Assert.False(childListList.IsAssignableTo(parentListEnumerable, out changedGenerics)); - Assert.Null(changedGenerics); - } + Assert.False(childListList.IsAssignableTo(parentListEnumerable, out changedGenericsLeft, out changedGenericsRight)); + Assert.Null(changedGenericsLeft); + Assert.Null(changedGenericsRight); + } [Fact] public void Assignations_IsDirectlyAssignable_InOut() { var typeFactory = new TypeFactory(new(Guid.NewGuid())); - var childEnumerable = typeFactory.Get(typeof(IEnumerable<>), new[] { typeFactory.Get() }); - var parentEnumerable = typeFactory.Get(typeof(IEnumerable<>), new[] { typeFactory.Get() }); - Assert.True(childEnumerable.IsDirectlyAssignableTo(parentEnumerable, true, out var changedGenerics)); - Assert.Empty(changedGenerics); + var childEnumerable = typeFactory.Get(typeof(IEnumerable<>), [typeFactory.Get()]); + var parentEnumerable = typeFactory.Get(typeof(IEnumerable<>), [typeFactory.Get()]); + Assert.True(childEnumerable.IsDirectlyAssignableTo(parentEnumerable, true, out var changedGenericsLeft, out var changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); - Assert.False(parentEnumerable.IsDirectlyAssignableTo(childEnumerable, true, out changedGenerics)); - Assert.Null(changedGenerics); - } + Assert.False(parentEnumerable.IsDirectlyAssignableTo(childEnumerable, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Null(changedGenericsLeft); + Assert.Null(changedGenericsRight); + } [Fact] public void Assignations_IsDirectlyAssignable_Basic() @@ -99,28 +108,221 @@ public void Assignations_IsDirectlyAssignable_Basic() var type = typeFactory.Get(typeof(List), null); - Assert.True(type.IsDirectlyAssignableTo(type, true, out var changedGenerics)); - Assert.Empty(changedGenerics); + Assert.True(type.IsDirectlyAssignableTo(type, true, out var changedGenericsLeft, out var changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + + Assert.True(type.IsDirectlyAssignableTo(typeFactory.Get(typeof(List<>), [new UndefinedGenericType("T")]), true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Equal(typeof(int), changedGenericsRight.First().Value.MakeRealType()); + + Assert.False(type.IsDirectlyAssignableTo(typeFactory.Get(typeof(IEnumerable<>), [new UndefinedGenericType("T")]), true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Null(changedGenericsLeft); + Assert.Null(changedGenericsRight); + + Assert.True(type.IsDirectlyAssignableTo(new UndefinedGenericType("T"), true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(type, changedGenericsRight.First().Value); + + Assert.True(new UndefinedGenericType("T").IsDirectlyAssignableTo(type, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(type, changedGenericsLeft.First().Value); + + Assert.True(new UndefinedGenericType("T").IsDirectlyAssignableTo(new UndefinedGenericType("T2"), true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + } + + [Fact] + public void Assignations_IsDirectlyAssignable_BasicArray() + { + var typeFactory = new TypeFactory(new(Guid.NewGuid())); + + var type = typeFactory.Get(typeof(int), null); // int + var typeArr = typeFactory.Get(typeof(int[]), null); // int[] + var undefined = new UndefinedGenericType("T"); // T + var undefinedArr = undefined.ArrayType; // T[] + + // int[] -> int[] + Assert.True(typeArr.IsDirectlyAssignableTo(typeArr, true, out var changedGenericsLeft, out var changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + + // int[] -> int + Assert.False(typeArr.IsDirectlyAssignableTo(type, true, out _, out _, out _)); + + // int -> int[] + Assert.False(type.IsDirectlyAssignableTo(typeArr, true, out _, out _, out _)); + + // int[] -> T + Assert.True(typeArr.IsDirectlyAssignableTo(undefined, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(typeArr, changedGenericsRight.First().Value); + + // int[] -> T[] + Assert.True(typeArr.IsDirectlyAssignableTo(undefinedArr, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(typeArr.ArrayInnerType, changedGenericsRight.First().Value); + + // T -> int[] + Assert.True(undefined.IsDirectlyAssignableTo(typeArr, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(typeArr, changedGenericsLeft.First().Value); + + // T[] -> int[] + Assert.True(undefinedArr.IsDirectlyAssignableTo(typeArr, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(typeArr.ArrayInnerType, changedGenericsLeft.First().Value); + + // T[] -> T[] + Assert.True(undefinedArr.IsDirectlyAssignableTo(undefinedArr, true, out changedGenericsLeft, out changedGenericsRight, out _)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + } + + [Fact] + public void Assignations_IsAssignableTo_BasicArray() + { + var typeFactory = new TypeFactory(new(Guid.NewGuid())); + + var undefinedType = new UndefinedGenericType("T"); // T + var undefinedTypeArr = undefinedType.ArrayType; // T[] + var undefinedArr = typeFactory.Get(typeof(List<>), [undefinedTypeArr]); // List + var undefined = typeFactory.Get(typeof(List<>), [undefinedType]); // List + var typeArr = typeFactory.Get(typeof(List<>), [typeFactory.Get()]); // List + var type = typeFactory.Get(typeof(List<>), [typeFactory.Get()]); // List + + + // List -> List + Assert.True(typeArr.IsAssignableTo(typeArr, out var changedGenericsLeft, out var changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + + // List -> List + Assert.False(typeArr.IsAssignableTo(type, out _, out _)); + + // List -> List + Assert.False(type.IsAssignableTo(typeArr, out _, out _)); + + // List -> List + Assert.True(typeArr.IsAssignableTo(undefined, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(typeArr.Generics[0], changedGenericsRight.First().Value); + + // List -> List + Assert.True(typeArr.IsAssignableTo(undefinedArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(typeArr.Generics[0].ArrayInnerType, changedGenericsRight.First().Value); + + // List -> List + Assert.True(undefined.IsAssignableTo(typeArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(typeArr.Generics[0], changedGenericsLeft.First().Value); + + // List -> List + Assert.True(undefinedArr.IsAssignableTo(typeArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(typeArr.Generics[0].ArrayInnerType, changedGenericsLeft.First().Value); + + // List -> List + Assert.True(undefinedArr.IsAssignableTo(undefinedArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + } + + [Fact] + public void Assignations_IsAssignableTo_ArrayToImplementation() + { + var typeFactory = new TypeFactory(new(Guid.NewGuid())); + + var undefinedType = new UndefinedGenericType("T"); // T + var undefinedTypeArr = undefinedType.ArrayType; // T[] + var undefinedArr = typeFactory.Get(typeof(List<>), [undefinedTypeArr]); // List + var undefined = typeFactory.Get(typeof(List<>), [undefinedType]); // List + var typeArr = typeFactory.Get(typeof(List<>), [typeFactory.Get()]); // List + var type = typeFactory.Get(typeof(List<>), [typeFactory.Get()]); // List + var typeImpl = typeFactory.Get(typeof(IEnumerable<>), [typeFactory.Get()]); // IEnumerable + var typeImplArr = typeFactory.Get(typeof(IEnumerable<>), [typeFactory.Get()]); // IEnumerable + var undefinedImpl = typeFactory.Get(typeof(IEnumerable<>), [undefinedType]); // IEnumerable + var undefinedImplArr = typeFactory.Get(typeof(IEnumerable<>), [undefinedTypeArr]); // IEnumerable + + + // List -> IEnumerable + Assert.True(typeArr.IsAssignableTo(typeImplArr, out var changedGenericsLeft, out var changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); + + // List -> IEnumerable + Assert.False(typeArr.IsAssignableTo(typeImpl, out _, out _)); + + // List -> IEnumerable + Assert.False(type.IsAssignableTo(typeImplArr, out _, out _)); + + // IEnumerable -> List + Assert.False(typeImplArr.IsAssignableTo(type, out _, out _)); + + // IEnumerable -> List + Assert.False(typeImpl.IsAssignableTo(typeArr, out _, out _)); + + // IEnumerable -> List + Assert.False(typeImplArr.IsAssignableTo(typeArr, out _, out _)); + + // IEnumerable -> List + Assert.False(typeImpl.IsAssignableTo(type, out _, out _)); - Assert.True(type.IsDirectlyAssignableTo(typeFactory.Get(typeof(List<>), new[] { new UndefinedGenericType("T") }), true, out changedGenerics)); - Assert.Single(changedGenerics); - Assert.Equal("T", changedGenerics.First().Key.Name); - Assert.Equal(typeof(int), changedGenerics.First().Value.MakeRealType()); + // List -> IEnumerable + Assert.True(typeArr.IsAssignableTo(undefinedImpl, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(typeArr.Generics[0], changedGenericsRight.First().Value); - Assert.False(type.IsDirectlyAssignableTo(typeFactory.Get(typeof(IEnumerable<>), new[] { new UndefinedGenericType("T") }), true, out changedGenerics)); - Assert.Null(changedGenerics); + // List -> IEnumerable + Assert.True(typeArr.IsAssignableTo(undefinedImplArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Single(changedGenericsRight); + Assert.Equal("T", changedGenericsRight.First().Key); + Assert.Same(typeArr.Generics[0].ArrayInnerType, changedGenericsRight.First().Value); - Assert.True(type.IsDirectlyAssignableTo(new UndefinedGenericType("T"), true, out changedGenerics)); - Assert.Single(changedGenerics); - Assert.Equal("T", changedGenerics.First().Key.Name); - Assert.Same(type, changedGenerics.First().Value); + // List -> IEnumerable + Assert.True(undefined.IsAssignableTo(typeImplArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(typeArr.Generics[0], changedGenericsLeft.First().Value); - Assert.True(new UndefinedGenericType("T").IsDirectlyAssignableTo(type, true, out changedGenerics)); - Assert.Single(changedGenerics); - Assert.Equal("T", changedGenerics.First().Key.Name); - Assert.Same(type, changedGenerics.First().Value); + // List -> IEnumerable + Assert.True(undefinedArr.IsAssignableTo(typeImplArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsRight); + Assert.Single(changedGenericsLeft); + Assert.Equal("T", changedGenericsLeft.First().Key); + Assert.Same(typeArr.Generics[0].ArrayInnerType, changedGenericsLeft.First().Value); - Assert.True(new UndefinedGenericType("T").IsDirectlyAssignableTo(new UndefinedGenericType("T2"), true, out changedGenerics)); - Assert.Empty(changedGenerics); + // List -> IEnumerable + Assert.True(undefinedArr.IsAssignableTo(undefinedImplArr, out changedGenericsLeft, out changedGenericsRight)); + Assert.Empty(changedGenericsLeft); + Assert.Empty(changedGenericsRight); } } \ No newline at end of file diff --git a/src/NodeDev.Tests/UndefinedGenericTypeTests.cs b/src/NodeDev.Tests/UndefinedGenericTypeTests.cs index cd874db..e2ec96a 100644 --- a/src/NodeDev.Tests/UndefinedGenericTypeTests.cs +++ b/src/NodeDev.Tests/UndefinedGenericTypeTests.cs @@ -9,10 +9,10 @@ public class UndefinedGenericTypeTests public void SerializeUndefinedGenericType_ReturnsExpectedJson() { var typeFactory = new TypeFactory(new(Guid.NewGuid())); - var undefinedGenericType = new UndefinedGenericType("T"); + var undefinedGenericType = new UndefinedGenericType("T").ArrayType.ArrayType; var serialized = undefinedGenericType.Serialize(); - var expectedJson = JsonSerializer.Serialize(new { Name = "T" }); + var expectedJson = JsonSerializer.Serialize(new { Name = "T", NbArrayLevels = 2 }); Assert.Equal(expectedJson, serialized); } @@ -21,7 +21,7 @@ public void SerializeUndefinedGenericType_ReturnsExpectedJson() public void SerializeUndefinedGenericType_Deserialize() { var typeFactory = new TypeFactory(new(Guid.NewGuid())); - var undefinedGenericType = new UndefinedGenericType("T"); + var undefinedGenericType = new UndefinedGenericType("T").ArrayType.ArrayType; var serialized = undefinedGenericType.SerializeWithFullTypeName(); @@ -30,7 +30,8 @@ public void SerializeUndefinedGenericType_Deserialize() Assert.IsType(deserialized); Assert.Equal(undefinedGenericType.Name, ((UndefinedGenericType)deserialized).Name); - } + Assert.Equal(undefinedGenericType.NbArrayLevels, ((UndefinedGenericType)deserialized).NbArrayLevels); + } } \ No newline at end of file