private void Generate(LifecycleMethods methods)
        {
            var names = methods.Owner.GetSymbolNames();

            var(dispose, appendToConstructor, appendToFinalizer) = methods.Disposes.Any()
                                ? GetDispose(names, methods)
                                : (null, null, null);

            var constructor = methods.Constructors.Any() || appendToConstructor.HasValue()
                                ? GetConstructor(names, methods, appendToConstructor)
                                : null;

            var finalize = methods.Finalizers.Any() || appendToFinalizer.HasValue()
                                ? GetFinalizer(names, methods, appendToFinalizer)
                                : null;

            var code = new StringWriter();

            using (var writer = new IndentedTextWriter(code, "\t"))
            {
                writer.WriteLine("// <auto-generated>");
                writer.WriteLine("// ***************************************************************************************************************************");
                writer.WriteLine("// This file has been generated by Uno.CodeGen (ClassLifecycleGenerator), available at https://github.com/nventive/Uno.CodeGen");
                writer.WriteLine("// ***************************************************************************************************************************");
                writer.WriteLine("// </auto-generated>");
                writer.WriteLine("#pragma warning disable");
                writer.WriteLine();
                writer.WriteLine("using global::System;");

                using (writer.NameSpaceOf(methods.Owner))
                {
                    using (writer.Block($"partial class {names.NameWithGenerics} {(methods.Disposes.Any() ? ": global::System.IDisposable" : "")}"))
                    {
                        writer.WriteLine(constructor);
                        writer.WriteLine(dispose);
                        writer.WriteLine(finalize);
                    }
                }
            }

            _context.AddCompilationUnit(names.FilePath, code.ToString());
        }
Example #2
0
        private string GetFinalizer(SymbolNames names, LifecycleMethods methods, string appendToFinalizer = null)
        {
            var result = string.Empty;

            // Validate that dispose methods does not have any parameter
            result += methods
                      .Disposes
                      .Where(dispose => dispose.Parameters.Any())
                      .Select(dispose => $"\r\n#error You cannot define any parameter on your finalizer method ({dispose.SimpleLocationText()}).")
                      .JoinByEmpty();

            // Validate that the return type of each dispose is void
            result += methods
                      .Finalizers
                      .Where(finalizer => !finalizer.ReturnsVoid)
                      .Select(finalizer => $"\r\n#error The return type of {finalizer.SimpleLocationText()} must be 'void'.")
                      .JoinByEmpty();

            if (methods
                .AllMethods
                .Any(method => method.Name == $"~{names.Name}"))
            {
                result +=
                    "\r\n#error Your class has some methods marked with the '[FinalizerMethod]' attribute. " +
                    "The finalizer will be generated, and you must not implement it in your code. " +
                    "If you want to add some code to the dispose, rename your method and mark it with the '[FinalizerMethod]' attribute.";
            }
            else
            {
                result += $@"
					~{names.Name}()
					{{
						{appendToFinalizer ?? ""}
						{methods.Finalizers.Select(m => m.GetInvocationText()).JoinBy("\r\n")}
					}}"                    ;
            }

            return(result);
        }
Example #3
0
        private (string code, string appendToConstructor, string appendToFinalizer) GetDispose(SymbolNames names, LifecycleMethods methods)
        {
            string result = string.Empty, appendToConstructor = string.Empty, appendToFinalizer = string.Empty;

            // Validate that dispose methods does not have any parameter
            result += methods
                      .Disposes
                      .Where(dispose => dispose.Parameters.Any())
                      .Select(dispose => $"\r\n#error You cannot define any parameter on your dispose method ({dispose.SimpleLocationText()}).")
                      .JoinByEmpty();

            // Validate that the return type each dispose is void
            result += methods
                      .Disposes
                      .Where(dispose => !dispose.ReturnsVoid)
                      .Select(dispose => $"\r\n#error The return type of {dispose.SimpleLocationText()} must be 'void'.")
                      .JoinByEmpty();

            var iDisposableImplementation = methods.Owner.FindImplementationForInterfaceMember(_iDisposable_Dispose);
            var iExtensibleDisposableImplementation = _extentibleDisposable_RegisterExtension == null
                                ? default(IMethodSymbol)
                                : methods.Owner.FindImplementationForInterfaceMember(_extentibleDisposable_RegisterExtension) as IMethodSymbol;

            var(patternKind, patternMethod) = methods.Owner.GetDisposablePatternImplementation();
            var(disposeKind, disposeMethod) = methods.Owner.GetDisposableImplementation();

            // If the class inherits from another class that has [DisposeMtehod], we know that it will implement the dispose pattern
            var baseHasDisposableMethods = methods.Bases.Any(b => b.Disposes.Any());

            if (baseHasDisposableMethods)
            {
                patternKind = DisposePatternImplementationKind.DisposePatternOnBase;
            }

            // Finaliy generate the code
            if (iDisposableImplementation == null && !baseHasDisposableMethods)
            {
                ImplementIDisposable();
            }
            else if (patternKind != DisposePatternImplementationKind.None || baseHasDisposableMethods)
            {
                ExtendDisposePattern();
            }
            else if (iExtensibleDisposableImplementation != null)
            {
                AppendToExtensibleDisposable();
            }
            else if (disposeKind != DisposeImplementationKind.None)
            {
                ExtendDispose();
            }
            else
            {
                // OUPS! something went wrong: we found that there is an implementation of IDisposable (FindImplementationForInterfaceMember(_iDisposable_Dispose)),
                // but we fail to determine the kind ... fail kind of gracefully.
                result += "\r\n#error Something went wrong in the ClassLifecycle generator (failed to determine the kind of implementation of IDispose)";
            }

            return(result, appendToConstructor, appendToFinalizer);

            void ImplementIDisposable()
            {
                if (methods.Owner.IsSealed)
                {
                    result += $@"
						[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
						private int __lifecycleIsDisposed;

						/// <inheritdoc cref=""global::System.IDisposable.Dispose""/>
						public void Dispose()
						{{
							if (global::System.Threading.Interlocked.Exchange(ref __lifecycleIsDisposed, 1) == 0)
							{{
								{methods.Disposes.Select(m => m.GetInvocationText()).JoinBy("\r\n")}
							}}
						}}"                        ;
                }
                else
                {
                    appendToFinalizer = "Dispose(false);";
                    result           += $@"
						[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
						private int __lifecycleIsDisposed;

						/// <summary>
						/// Overridable method to perform application-defined tasks associated with freeing, releasing, or resetting resources.
						/// </summary>
						/// <param name=""isDisposing"" >
						/// A boolean which indicates if this is invoked by the <see cref=""gloabl::System.IDisposable.Dispose""/> (so you should release all ressources), 
						/// or by the class finalizer (so you should release only unmanaged ressources).
						/// </param>
						protected virtual void Dispose(bool isDisposing)
						{{
							if (global::System.Threading.Interlocked.Exchange(ref __lifecycleIsDisposed, 1) == 0
								&& isDisposing)
							{{
								{methods.Disposes.Select(m => m.GetInvocationText()).JoinBy("\r\n")}
							}}
						}}

						/// <inheritdoc cref=""global::System.IDisposable.Dispose""/>
						public void Dispose()
						{{
							Dispose(true);
							{(methods.Finalizers.None() ? "global::System.GC.SuppressFinalize(this);" : "")}
						}}"                        ;
                }
            }

            void ExtendDisposePattern()
            {
                switch (patternKind)
                {
                case DisposePatternImplementationKind.DisposePattern:
                    result +=
                        "\r\n#error Your class has some methods marked with the '[DisposeMethod]' attribute. " +
                        "The 'Dispose' pattern will be generated, and you must not implement it in your code. " +
                        "If you want to add some code to the dispose, rename your method and mark it with the '[DisposeMethod]' attribute. " +
                        $"You get this error because you defined {patternMethod.SimpleLocationText()}.";
                    break;

                case DisposePatternImplementationKind.DisposePatternOnBase:
                    result += $@"
							[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
							private int __lifecycleIsDisposed;

							/// <inheritdoc />
							{patternMethod?.DeclaredAccessibility.ToString().ToLowerInvariant() ?? "protected"} override void Dispose(bool isDisposing)
							{{
								base.Dispose(isDisposing);

								if (global::System.Threading.Interlocked.Exchange(ref __lifecycleIsDisposed, 1) == 0)
								{{
									{methods.Disposes.Select(m => m.GetInvocationText()).JoinBy("\r\n")}
								}}
							}}"                            ;
                    break;

                case DisposePatternImplementationKind.SealedDisposePatternOnBase:
                    result +=
                        "\r\n#error Your class has some methods marked with the '[DisposeMethod]' attribute, " +
                        $"but the base class {patternMethod.ContainingType.Name} sealed its implementation. " +
                        $"You have either to remove the inheritance, or remove the method marked with the '[DisposeMethod]' attribute.";
                    break;

                case DisposePatternImplementationKind.NonOverridableDisposePatternOnBase:
                    result +=
                        "\r\n#error Your class has some methods marked with the '[DisposeMethod]' attribute, " +
                        $"but the base class {patternMethod.ContainingType.Name} did not implemented the dispose pattern properly. " +
                        $"You have either to make the Dispose(bool) method 'virtual' on {patternMethod.ContainingType.Name}, " +
                        $"remove the inheritance, or remove the method marked with the '[DisposeMethod]' attribute.";
                    break;

                default:
                    throw new NotSupportedException($"Invalid dispose pattern kind : '{patternKind}'");
                }
            }

            void AppendToExtensibleDisposable()
            {
                appendToConstructor = $"{iExtensibleDisposableImplementation.GetInvocationText(parameters: "new __LifecycleDisposables(this)")}";
                result += $@"
					[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
					private class __LifecycleDisposables : global::System.IDisposable
					{{
						private readonly {names.NameWithGenerics} _parent;
						private int _isDisposed;

						public __LifecycleDisposables({names.NameWithGenerics} parent)
						{{
							_parent = parent;
						}}

						public void Dispose()
						{{
							if (global::System.Threading.Interlocked.Exchange(ref _isDisposed, 1) == 0)
							{{
								{methods.Disposes.Select(m => m.GetInvocationText(target: "_parent")).JoinBy("\r\n")}
							}}
						}}
					}}"                    ;
            }

            void ExtendDispose()
            {
                switch (disposeKind)
                {
                case DisposeImplementationKind.Dispose:
                    result +=
                        "\r\n#error Your class has some methods marked with the '[DisposeMethod]' attribute. " +
                        "The 'Dispose' pattern will be generated, and you must not implement it in your code. " +
                        "If you want to add some code to the dispose, rename your method and mark it with the '[DisposeMethod]' attribute. " +
                        $"You get this error because you defined {disposeMethod.SimpleLocationText()}.";
                    break;

                case DisposeImplementationKind.DisposeOnBase:
                    result +=
                        "\r\n#error Your class has some methods marked with the '[DisposeMethod]' attribute, " +
                        $"but the base class {disposeMethod.ContainingType.Name} sealed its implementation. " +
                        $"You have either to remove the inheritance, or remove the method marked with the '[DisposeMethod]' attribute.";
                    break;

                case DisposeImplementationKind.OverridableDisposeOnBase:
                    result += $@"
							/// <inheritdoc />
							{disposeMethod.DeclaredAccessibility.ToString().ToLowerInvariant()} override void Dispose()
							{{
								base.Dispose();
								{methods.Disposes.Select(m => m.GetInvocationText()).JoinBy("\r\n")}
							}}"                            ;
                    break;

                default:
                    throw new NotSupportedException($"Invalid dispose pattern kind : '{disposeKind}'");
                }
            }
        }
Example #4
0
        private string GetConstructor(SymbolNames names, LifecycleMethods methods, string appendToConstructor = null)
        {
            var declaredConstructors = methods
                                       .Owner
                                       .Constructors
                                       .Where(ctor => !ctor.IsImplicitlyDeclared)
                                       .ToList();

            var declaredParameterlessConstructor = declaredConstructors
                                                   .FirstOrDefault(ctor => !ctor.Parameters.Any());

            var parameters = methods
                             .Constructors
                             .SelectMany(ctor => ctor.Parameters)
                             .GroupBy(parameter => parameter.Name)
                             .Select(parameter =>
            {
                var type           = parameter.First().Type;
                var isTypeMismatch = parameter.Any(p => !Equals(p.Type, type));

                var isOptional             = parameter.All(p => p.IsOptional);
                var defaultValue           = default(object);
                var isDefaultValueMismatch = false;
                if (isOptional)
                {
                    defaultValue           = parameter.First().ExplicitDefaultValue;
                    isDefaultValueMismatch = parameter.Any(p => p.ExplicitDefaultValue?.ToString() != defaultValue?.ToString());
                }

                return(new
                {
                    name = parameter.Key,
                    references = parameter as IEnumerable <IParameterSymbol>,

                    isOptional = parameter.All(p => p.IsOptional),
                    isDefaultValueMismatch = isDefaultValueMismatch,
                    defaultValue = isDefaultValueMismatch ? null : defaultValue,

                    isTypeMismatch = isTypeMismatch,
                    type = isTypeMismatch ? null : type
                });
            })
                             .ToList();

            var initializeParameters = parameters
                                       .Where(parameter => !parameter.isTypeMismatch && !parameter.isDefaultValueMismatch)
                                       .OrderBy(parameter => parameter.isOptional ? 1 : 0)
                                       .ThenBy(parameter => parameter.name)
                                       .Select(parameter => $"{parameter.type.GlobalTypeDefinitionText()} {parameter.name} {(parameter.isOptional ? $"= {parameter.defaultValue?.ToString() ?? "null"}" : "")}")
                                       .JoinBy(", ");

            var initializeParametersAreOptionalOrEmpty = parameters.None(p => !p.isOptional) &&
                                                         (methods.Owner.BaseType == null ||
                                                          methods.Owner.BaseType.SpecialType == SpecialType.System_Object ||
                                                          methods.Owner.BaseType.Constructors.Any(ctor => ctor.Parameters.None(p => !p.IsOptional)));

            var result = string.Empty;

            // Validate that the return type each conctructors is void
            result += methods
                      .Constructors
                      .Where(ctor => !ctor.ReturnsVoid)
                      .Select(ctor => $"\r\n#error The return type of {ctor.SimpleLocationText()} must be 'void'.")
                      .JoinByEmpty();

            // Validate that each constructor declared invokes the 'Initialize' method
            result += declaredConstructors
                      .Where(ctor => !InvokesInitialize(ctor))
                      .Select(ctor =>
                              $"\r\n#error Constructor {ctor.SimpleLocationText()} does not invoke the 'Initialize' method. " +
                              "As you marked (or used some builders that marked) some method with the '[ConstructorMethod]' and since you have defined some constructors in your code, " +
                              "you must invoke this method in all your constructor (either directly, or by invoking another contructor). "
                              + (initializeParametersAreOptionalOrEmpty ? "Tips: The easiet way is to invoke the parameterless constructor (i.e. add ': this()' to your constructor)" : ""))
                      .JoinByEmpty();

            // Validate that all constructors parameters are compatibles
            result += parameters
                      .Where(parameter => parameter.isTypeMismatch || parameter.isDefaultValueMismatch)
                      .SelectMany(parameter =>
            {
                return(Errors());

                IEnumerable <string> Errors()
                {
                    if (parameter.isTypeMismatch)
                    {
                        yield return
                        ($"\r\n#error There is a type mismatch for the parameter named '{parameter.name}' beetween your constructor methods " +
                         "(i.e. methods marked with the '[ConstructorMethod]' attribute). " +
                         parameter.references.Select(p => $"It is of type '{p.Type}' in {p.ContainingSymbol.SimpleLocationText()}").JoinBy("; "));
                    }
                    if (parameter.isDefaultValueMismatch)
                    {
                        yield return
                        ($"\r\n#error There is a default value mismatch for the optional parameter named '{parameter.name}' beetween your constructor methods " +
                         "(i.e. methods marked with the '[ConstructorMethod]' attribute). " +
                         parameter.references.Select(p => $"It has default value '{p.ExplicitDefaultValue}' in {p.ContainingSymbol.SimpleLocationText()}").JoinBy("; "));
                    }
                }
            })
                      .JoinByEmpty();

            // Try to generate the parameterless constructor if possible
            if (declaredParameterlessConstructor == null && initializeParametersAreOptionalOrEmpty)
            {
                result += $@"
					{(declaredConstructors.Any() ? "private" : "public")} {names.Name}()
					{{
						Initialize();
					}}"                    ;
            }

            // Generate the initialize method
            result += $@"
				[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
				private int __lifecycleIsInitialized;

				/// <summary>
				/// Completes construction of this class.
				/// </summary>
				/// <remarks>This method MUST be invoked in the constructor of this class</remarks>
				private void Initialize({initializeParameters})
				{{
					if (global::System.Threading.Interlocked.Exchange(ref __lifecycleIsInitialized, 1) == 0)
					{{
						{appendToConstructor ?? ""}
						{methods.Constructors.Select(ctor => ctor.GetInvocationText(ctor.Parameters)).JoinBy("\r\n")}
					}}
				}}"                ;

            return(result);

            bool InvokesInitialize(ISymbol constructor)
            {
                return(constructor
                       .DeclaringSyntaxReferences
                       .Select(reference => reference.GetSyntax())
                       .OfType <ConstructorDeclarationSyntax>()
                       .Any(constructorSyntax =>
                {
                    // Check if any body statements is 'Initialize();'
                    if (constructorSyntax.Body?.Statements.Any(IsInitializeStatement) ?? false)
                    {
                        return true;
                    }

                    // Check if any expression body is 'Initialize();'
                    // The property appears to be public ... but is innacessible by code ...
                    var expressionBody = constructorSyntax.GetType().GetProperty("ExpressionBody", BindingFlags.Instance | BindingFlags.Public)?.GetValue(constructorSyntax) as ArrowExpressionClauseSyntax;
                    if (expressionBody?.Expression is InvocationExpressionSyntax expressionBodyInvocation && IsInitializeInvocation(expressionBodyInvocation))
                    {
                        return true;
                    }


                    // Check if constructor is invoking another constructor
                    var initializer = constructorSyntax.Initializer;
                    if (initializer == null)
                    {
                        return false;
                    }

                    // Check if this other constructor is invoking 'Initialize'
                    var parentConstructor = _context.Compilation.GetSemanticModel(initializer.SyntaxTree).GetSymbolInfo(initializer).Symbol;
                    if (parentConstructor == null)
                    {
                        // It is invoking the parameter less contructor we are generating !
                        return declaredParameterlessConstructor == null && initializer.ArgumentList.Arguments.None();
                    }
                    else if (!Equals(parentConstructor.ContainingSymbol, constructor.ContainingSymbol))
                    {
                        // Currently we don't support Intialize inheritance (the issue is that as each inheriance layer may add some parameters,
                        // the base class cannot invoke a single method that is overriden by children)
                        // We could allow this scenario for parameter-less Initialize(), but it would propably be more confusing.
                        return false;
                    }
                    else
                    {
                        return InvokesInitialize(parentConstructor);
                    }
                }));
            }

            bool IsInitializeStatement(StatementSyntax syntax)
            {
                var statement  = syntax as ExpressionStatementSyntax;
                var invocation = statement?.Expression as InvocationExpressionSyntax;

                return(IsInitializeInvocation(invocation));
            }

            bool IsInitializeInvocation(InvocationExpressionSyntax invocation)
            {
                var identifier = invocation?.Expression as IdentifierNameSyntax;

                return(identifier?.Identifier.Text == "Initialize");
            }
        }