Added a check for referenced types to be immutable

This commit is contained in:
Carl de Billy 2018-02-09 16:10:53 -05:00
Родитель 982ab18c0b
Коммит dbc76af409
10 изменённых файлов: 515 добавлений и 121 удалений

Просмотреть файл

@ -322,4 +322,17 @@ All attributes are replicated, except those defined in `Uno.Immutables` and
to use the `[ImmutableAttributeCopyIgnore(<regex>)]` attribute.
For a finer control, you can put it at assembly level, on a type or
even on a property itself.
even on a property itself.
## What about arrays? They are not immutables
Exactly, arrays are not immutable. But they have one big advantages over other
declarations: they are _great looking_. Declaring a property as `A[]` is
more concise than declaring `IReadonlyCollection<T>`.
You should not use arrays for immutable types, but if you really prefer the
concise declaration of arrays, you can allow arrays to be treated as immutables
by setting this attribute on your assembly:
``` csharp
[assembly: Uno.ImmutableGenerationOptions(TreatArrayAsImmutable = true)]
```

Просмотреть файл

@ -18,10 +18,11 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Uno.ImmutableGenerationOptions(TreatArrayAsImmutable = true)]
namespace Uno.CodeGen.Tests
{
[TestClass]
@ -105,18 +106,17 @@ namespace Uno.CodeGen.Tests
public void Immutable_When_CreatingHierarchyOfBuilders_Then_NullRefException_Is_Prevented()
{
var list = ImmutableList<string>.Empty;
A a = null;
MyGenericImmutable<A> nullObject = null;
var newObj = a.WithEntity(b => b.WithList(list));
var newObj = nullObject.WithEntity(a => a.WithEntity(b => b.WithList(list)));
newObj.Entity.Entity.List.Should().BeSameAs(list);
newObj.Entity.List.Should().BeSameAs(list);
}
[TestMethod]
public void Immutable_When_Using_Attributes_Then_They_Are_Copied_On_Builder()
{
var tBuilder = typeof(MySuperGenericImmutable<string, string, string, string, string, string>.Builder);
var tBuilder = typeof(MySuperGenericImmutable<MyImmutableEntity, MyImmutableEntity, MyImmutableEntity[], string, string, string>.Builder);
var idProperty = tBuilder.GetProperty("Id");
var attributes = idProperty.GetCustomAttributes(false);
attributes.Should().HaveCount(3);
@ -152,6 +152,8 @@ namespace Uno.CodeGen.Tests
[GeneratedImmutable]
public partial class A
{
public Type T { get; }
public MyImmutableEntity Entity { get; } = MyImmutableEntity.Default;
public bool IsSomething { get; } = true;
@ -174,6 +176,12 @@ namespace Uno.CodeGen.Tests
public int MyField2 { get; } = 75;
public int? MyField3 { get; }
public string[] MyField4 { get; }
public IReadOnlyList<string[]> MyField5 { get; }
public int Sum => MyField1 + MyField2; // won't generate any builder code
public DateTimeOffset Date { get; } = DateTimeOffset.Now;
@ -182,7 +190,7 @@ namespace Uno.CodeGen.Tests
}
[GeneratedImmutable]
public partial class MyGenericImmutable<T>
public abstract partial class MyGenericImmutable<T>
{
public T Entity { get; }
}
@ -199,7 +207,10 @@ namespace Uno.CodeGen.Tests
[ImmutableAttributeCopyIgnore("RequiredAttribute")]
[GeneratedImmutable(GenerateEquality = true)]
public partial class MySuperGenericImmutable<T1, T2, T3, T4, T5, T6>
public abstract partial class MySuperGenericImmutable<T1, T2, T3, T4, T5, T6>
where T1: MyImmutableEntity
where T2: T1
where T3: IReadOnlyCollection<T1>
{
[Required, System.ComponentModel.DataAnnotations.DataType(DataType.Text)]
[System.ComponentModel.DataAnnotations.Key]
@ -222,4 +233,9 @@ namespace Uno.CodeGen.Tests
private static int GetHash_Entity6(T6 value) => 50;
}
public partial class MySuperGenericConcrete<T> : MySuperGenericImmutable<MyImmutableEntity, MyImmutableEntity, MyImmutableEntity[], T, T, T>
where T: IReadOnlyCollection<string>
{
}
}

Просмотреть файл

@ -16,6 +16,7 @@
// ******************************************************************
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
@ -23,18 +24,19 @@ namespace Uno.Helpers
{
public static class NamedTypeSymbolExtensions
{
public static (string symbolName, string genericArguments, string symbolNameWithGenerics, string symbolForXml, string symbolNameDefinition, string symbolFilename)
GetSymbolNames(this INamedTypeSymbol typeSymbol)
public static SymbolNames GetSymbolNames(this INamedTypeSymbol typeSymbol)
{
var symbolName = typeSymbol.Name;
if (typeSymbol.TypeArguments.Length == 0) // not a generic type
{
return (symbolName, "", symbolName, symbolName, symbolName, symbolName);
return new SymbolNames(typeSymbol, symbolName, "", symbolName, symbolName, symbolName, symbolName);
}
var argumentNames = typeSymbol.GetTypeArgumentNames();
var genericArguments = string.Join(", ", argumentNames);
//Debugger.Launch();
// symbolNameWithGenerics: MyType<T1, T2>
var symbolNameWithGenerics = $"{symbolName}<{genericArguments}>";
@ -47,12 +49,50 @@ namespace Uno.Helpers
// symbolNameWithGenerics: MyType_T1_T2
var symbolFilename = $"{symbolName}_{string.Join("_", argumentNames)}";
return (symbolName, $"<{genericArguments}>", symbolNameWithGenerics, symbolForXml, symbolNameDefinition, symbolFilename);
return new SymbolNames(typeSymbol, symbolName, $"<{genericArguments}>", symbolNameWithGenerics, symbolForXml, symbolNameDefinition, symbolFilename);
}
public static string[] GetTypeArgumentNames(this ITypeSymbol typeSymbol)
{
return (typeSymbol as INamedTypeSymbol)?.TypeArguments.Select(ta => ta.MetadataName).ToArray() ?? new string[0];
return (typeSymbol as INamedTypeSymbol)?.TypeArguments.Select(ta => ta.ToString()).ToArray() ?? new string[0];
}
}
public class SymbolNames
{
public SymbolNames(INamedTypeSymbol symbol, string symbolName, string genericArguments, string symbolNameWithGenerics, string symbolFoxXml, string symbolNameDefinition, string symbolFilename)
{
Symbol = symbol;
SymbolName = symbolName;
GenericArguments = genericArguments;
SymbolNameWithGenerics = symbolNameWithGenerics;
SymbolFoxXml = symbolFoxXml;
SymbolNameDefinition = symbolNameDefinition;
SymbolFilename = symbolFilename;
}
public INamedTypeSymbol Symbol { get; }
public string SymbolName { get; }
public string GenericArguments { get; }
public string SymbolNameWithGenerics { get; }
public string SymbolFoxXml { get; }
public string SymbolNameDefinition { get; }
public string SymbolFilename { get; }
public void Deconstruct(
out string symbolName,
out string genericArguments,
out string symbolNameWithGenerics,
out string symbolFoxXml,
out string symbolNameDefinition,
out string symbolFilename)
{
symbolName = SymbolName;
genericArguments = GenericArguments;
symbolNameWithGenerics = SymbolNameWithGenerics;
symbolFoxXml = SymbolFoxXml;
symbolNameDefinition = SymbolNameDefinition;
symbolFilename = SymbolFilename;
}
}
}

Просмотреть файл

@ -0,0 +1,127 @@
// ******************************************************************
// Copyright <20> 2015-2018 nventive inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ******************************************************************
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Uno.Helpers
{
public static class TypeSymbolExtensions
{
public static bool IsGenericArgument(this ITypeSymbol type)
{
return type.GetType().Name.Equals("SourceTypeParameterSymbol");
}
public static bool IsImmutable(this ITypeSymbol type, bool treatArrayAsImmutable)
{
foreach (var attribute in type.GetAttributes())
{
switch (attribute.AttributeClass.ToString())
{
case "Uno.ImmutableAttribute":
case "Uno.GeneratedImmutableAttribute":
return true;
}
}
switch (type.SpecialType)
{
case SpecialType.System_Boolean:
case SpecialType.System_Byte:
case SpecialType.System_Char:
case SpecialType.System_DateTime:
case SpecialType.System_Decimal:
case SpecialType.System_Double:
case SpecialType.System_Enum:
case SpecialType.System_Int16:
case SpecialType.System_Int32:
case SpecialType.System_Int64:
case SpecialType.System_IntPtr:
case SpecialType.System_SByte:
case SpecialType.System_String:
case SpecialType.System_Single:
case SpecialType.System_UInt16:
case SpecialType.System_UInt32:
case SpecialType.System_UInt64:
case SpecialType.System_UIntPtr:
return true;
case SpecialType.None:
case SpecialType.System_Collections_Generic_IReadOnlyCollection_T:
case SpecialType.System_Collections_Generic_IReadOnlyList_T:
break; // need further validation
default:
return false;
}
if (type is IArrayTypeSymbol arrayType)
{
return arrayType.ElementType.IsImmutable(treatArrayAsImmutable);
}
var definitionType = type;
while ((definitionType as INamedTypeSymbol)?.ConstructedFrom.Equals(definitionType) == false)
{
definitionType = ((INamedTypeSymbol)definitionType).ConstructedFrom;
}
switch (definitionType.ToString())
{
case "System.Attribute": // strange, but valid
case "System.TimeSpan":
case "System.DateTime":
case "System.DateTimeOffset":
case "System.Type":
return true;
case "System.Nullable<T>":
case "System.Collections.Generic.IReadOnlyList<T>":
case "System.Collections.Generic.IReadOnlyCollection<T>":
case "System.Collections.Immutable.IImmutableArray<T>":
case "System.Collections.Immutable.ImmutableArray<T>":
case "System.Collections.Immutable.IImmutableList<T>":
case "System.Collections.Immutable.ImmutableList<T>":
{
var argumentParameter = (type as INamedTypeSymbol)?.TypeArguments.FirstOrDefault();
return argumentParameter == null || argumentParameter.IsImmutable(treatArrayAsImmutable);
}
}
switch (definitionType.GetType().Name)
{
case "TupleTypeSymbol":
return true; // tuples are immutables
}
switch (definitionType.BaseType.ToString())
{
case "System.Enum":
return true;
case "System.Array":
return treatArrayAsImmutable;
}
if (definitionType.IsReferenceType)
{
return false;
}
return false;
}
}
}

Просмотреть файл

@ -25,9 +25,11 @@ using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
using Uno.Helpers;
using Uno.RoslynHelpers;
using Uno.SourceGeneration;
using TypeSymbolExtensions = Uno.Helpers.TypeSymbolExtensions;
namespace Uno
{
@ -39,7 +41,10 @@ namespace Uno
private INamedTypeSymbol _generatedImmutableAttributeSymbol;
private INamedTypeSymbol _immutableBuilderAttributeSymbol;
private INamedTypeSymbol _immutableAttributeCopyIgnoreAttributeSymbol;
private INamedTypeSymbol _immutableGenerationOptionsAttributeSymbol;
private bool _generateOptionCode = true;
private (bool generateOptionCode, bool treatArrayAsImmutable) _generationOptions;
private Regex[] _copyIgnoreAttributeRegexes;
@ -51,8 +56,7 @@ namespace Uno
_generatedImmutableAttributeSymbol = context.Compilation.GetTypeByMetadataName("Uno.GeneratedImmutableAttribute");
_immutableBuilderAttributeSymbol = context.Compilation.GetTypeByMetadataName("Uno.ImmutableBuilderAttribute");
_immutableAttributeCopyIgnoreAttributeSymbol = context.Compilation.GetTypeByMetadataName("Uno.ImmutableAttributeCopyIgnoreAttribute");
_generateOptionCode = context.Compilation.GetTypeByMetadataName("Uno.Option") != null;
_immutableGenerationOptionsAttributeSymbol = context.Compilation.GetTypeByMetadataName("Uno.ImmutableGenerationOptionsAttribute");
var generationData = EnumerateImmutableGeneratedEntities()
.OrderBy(x => x.symbol.Name)
@ -65,6 +69,10 @@ namespace Uno
.Concat(new[] {new Regex(@"^Uno\.Immutable"), new Regex(@"^Uno\.Equality")})
.ToArray();
_generationOptions = ExtractGenerationOptions(context.Compilation.Assembly);
_generateOptionCode = _generationOptions.generateOptionCode && context.Compilation.GetTypeByMetadataName("Uno.Option") != null;
foreach ((var type, var moduleAttribute) in generationData)
{
var baseTypeInfo = GetTypeInfo(context, type, immutableEntitiesToGenerate);
@ -82,6 +90,34 @@ namespace Uno
.Select(a => new Regex(a.ConstructorArguments[0].Value.ToString()));
}
private (bool generateOptionCode, bool treatArrayAsImmutable) ExtractGenerationOptions(IAssemblySymbol assembly)
{
var generateOptionCode = true;
var treatArrayAsImmutable = false;
var attribute = assembly
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.Equals(_immutableGenerationOptionsAttributeSymbol));
if (attribute != null)
{
foreach (var argument in attribute.NamedArguments)
{
switch (argument.Key)
{
case nameof(ImmutableGenerationOptionsAttribute.GenerateOptionCode):
generateOptionCode = (bool)argument.Value.Value;
break;
case nameof(ImmutableGenerationOptionsAttribute.TreatArrayAsImmutable):
treatArrayAsImmutable = (bool) argument.Value.Value;
break;
}
}
}
return (generateOptionCode, treatArrayAsImmutable);
}
private bool GetShouldGenerateEquality(AttributeData attribute)
{
return attribute.NamedArguments
@ -118,7 +154,7 @@ namespace Uno
var names = baseType.GetSymbolNames();
var isSameNamespace = baseType.ContainingNamespace.Equals(type.ContainingNamespace);
var baseTypeName = isSameNamespace ? names.symbolNameWithGenerics : baseType.ToDisplayString();
var baseTypeName = isSameNamespace ? names.SymbolNameWithGenerics : baseType.ToDisplayString();
var builderBaseType = baseTypeName + ".Builder";
return (true, baseTypeName, builderBaseType, isImmutablePresent);
@ -133,10 +169,14 @@ namespace Uno
{
var defaultMemberName = "Default";
var generateOption = _generateOptionCode && !typeSymbol.IsAbstract;
var classCopyIgnoreRegexes = ExtractCopyIgnoreAttributes(typeSymbol).ToArray();
(IPropertySymbol property, bool isNew)[] properties;
var typeProperties = typeSymbol.GetProperties().ToArray();
if (baseTypeInfo.isBaseType)
{
var baseProperties = typeSymbol.BaseType.GetProperties()
@ -144,16 +184,14 @@ namespace Uno
.Select(x => x.Name)
.ToArray();
properties = typeSymbol
.GetProperties()
properties = typeProperties
.Where(x => x.IsReadOnly && IsAutoProperty(x))
.Select(x => (x, baseProperties.Contains(x.Name))) // remove properties already present in base class
.ToArray();
}
else
{
properties = typeSymbol
.GetProperties()
properties = typeProperties
.Where(x => x.IsReadOnly && IsAutoProperty(x))
.Select(x => (x, false))
.ToArray();
@ -161,26 +199,10 @@ namespace Uno
var builder = new IndentedStringBuilder();
var (symbolName, genericArguments, symbolNameWithGenerics, symbolNameForXml, symbolNameDefinition, resultFileName) =
typeSymbol.GetSymbolNames();
var symbolNames = typeSymbol.GetSymbolNames();
var (symbolName, genericArguments, symbolNameWithGenerics, symbolNameForXml, symbolNameDefinition, resultFileName) = symbolNames;
if (!IsFromPartialDeclaration(typeSymbol))
{
builder.AppendLineInvariant(
$"#warning {nameof(ImmutableGenerator)}: you should add the partial modifier to the class {symbolNameWithGenerics}.");
}
if (typeSymbol.IsValueType)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: Type {symbolNameWithGenerics} **MUST** be a class, not a struct.");
}
if (baseTypeInfo.isBaseType && baseTypeInfo.baseType == null)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: Type {symbolNameWithGenerics} **MUST** derive from an immutable class.");
}
ValidateType(builder, typeSymbol, baseTypeInfo, symbolNames, typeProperties);
var newModifier = baseTypeInfo.isBaseType ? "new " : "";
@ -195,9 +217,17 @@ namespace Uno
using (builder.BlockInvariant($"namespace {typeSymbol.ContainingNamespace}"))
{
var builderTypeNameAndBaseClass = baseTypeInfo.isBaseType
string builderTypeNameAndBaseClass;
if (typeSymbol.IsAbstract)
{
builderTypeNameAndBaseClass = baseTypeInfo.isBaseType ? $"Builder : {baseTypeInfo.builderBaseType}" : $"Builder";
}
else
{
builderTypeNameAndBaseClass = baseTypeInfo.isBaseType
? $"Builder : {baseTypeInfo.builderBaseType}, Uno.IImmutableBuilder<{symbolNameWithGenerics}>"
: $"Builder : global::Uno.IImmutableBuilder<{symbolNameWithGenerics}>";
}
if (baseTypeInfo.isImmutablePresent)
{
@ -215,16 +245,19 @@ namespace Uno
builder.AppendLineInvariant($"[global::Uno.ImmutableBuilder(typeof({symbolNameDefinition}.Builder))] // Other generators can use this to find the builder.");
using (builder.BlockInvariant(
$"{typeSymbol.GetAccessibilityAsCSharpCodeString()} partial class {symbolNameWithGenerics}"))
{
builder.AppendLineInvariant($"/// <summary>");
builder.AppendLineInvariant($"/// {defaultMemberName} instance with only property initializers set.");
builder.AppendLineInvariant($"/// </summary>");
builder.AppendLineInvariant(
$"public static readonly {newModifier}{symbolNameWithGenerics} {defaultMemberName} = new {symbolNameWithGenerics}();");
var abstractClause = typeSymbol.IsAbstract ? "abstract " : "";
builder.AppendLine();
using (builder.BlockInvariant($"{typeSymbol.GetAccessibilityAsCSharpCodeString()} {abstractClause}partial class {symbolNameWithGenerics}"))
{
if(!typeSymbol.IsAbstract)
{
builder.AppendLineInvariant($"/// <summary>");
builder.AppendLineInvariant($"/// {defaultMemberName} instance with only property initializer set.");
builder.AppendLineInvariant($"/// </summary>");
builder.AppendLineInvariant($"public static readonly {newModifier}{symbolNameWithGenerics} {defaultMemberName} = new {symbolNameWithGenerics}();");
builder.AppendLine();
}
var prop1Name = properties.Select(p => p.property.Name).FirstOrDefault() ?? symbolName + "Property";
builder.AppendLineInvariant($"/// <summary>");
@ -235,18 +268,14 @@ namespace Uno
builder.AppendLineInvariant($"/// use the .WithXXX() methods to do it in a fluent way. You can continue to");
builder.AppendLineInvariant($"/// change it even after calling the `.ToImmutable()` method: it will simply");
builder.AppendLineInvariant($"/// generate a new version from the current state.");
builder.AppendLineInvariant(
$"/// **THE BUILDER IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant($"/// **THE BUILDER IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant($"/// </remarks>");
builder.AppendLineInvariant("/// <example>");
builder.AppendLineInvariant($"/// // The following code will create a builder using a .With{prop1Name}() method:");
builder.AppendLineInvariant(
$"/// {symbolNameForXml}.Builder b = my{symbolName}Instance.With{prop1Name}([{prop1Name} value]);");
builder.AppendLineInvariant($"/// {symbolNameForXml}.Builder b = my{symbolName}Instance.With{prop1Name}([{prop1Name} value]);");
builder.AppendLineInvariant("///");
builder.AppendLineInvariant(
$"/// // The following code will use implicit cast to create a new {symbolNameForXml} immutable instance:");
builder.AppendLineInvariant("{0}",
$"/// {symbolNameForXml} my{symbolName}Instance = new {symbolNameForXml}.Builder {{ {prop1Name} = [{prop1Name} value], ... }};");
builder.AppendLineInvariant($"/// // The following code will use implicit cast to create a new {symbolNameForXml} immutable instance:");
builder.AppendLineInvariant("{0}", $"/// {symbolNameForXml} my{symbolName}Instance = new {symbolNameForXml}.Builder {{ {prop1Name} = [{prop1Name} value], ... }};");
builder.AppendLineInvariant("/// </example>");
using (builder.BlockInvariant(
$"{typeSymbol.GetAccessibilityAsCSharpCodeString()} {newModifier}partial class {builderTypeNameAndBaseClass}"))
@ -261,25 +290,33 @@ namespace Uno
builder.AppendLineInvariant("{0}", $"internal readonly {symbolNameWithGenerics} _original;");
builder.AppendLine();
builder.AppendLineInvariant("// Cached version of generated entity (flushed when the builder is updated)");
builder.AppendLineInvariant(
$"protected {symbolNameWithGenerics} _cachedResult = default({symbolNameWithGenerics});");
builder.AppendLineInvariant($"protected {symbolNameWithGenerics} _cachedResult = default({symbolNameWithGenerics});");
builder.AppendLine();
using (builder.BlockInvariant($"public Builder({symbolNameWithGenerics} original)"))
if (typeSymbol.IsAbstract)
{
builder.AppendLineInvariant($"_original = original ?? {symbolNameWithGenerics}.Default;");
using (builder.BlockInvariant($"public Builder({symbolNameWithGenerics} original)"))
{
builder.AppendLineInvariant($"_original = original ?? throw new ArgumentNullException(nameof(original));");
}
}
builder.AppendLine();
using (builder.BlockInvariant($"public Builder()"))
else
{
builder.AppendLineInvariant($"_original = {symbolNameWithGenerics}.Default;");
using (builder.BlockInvariant($"public Builder({symbolNameWithGenerics} original)"))
{
builder.AppendLineInvariant($"_original = original ?? {symbolNameWithGenerics}.Default;");
}
builder.AppendLine();
using (builder.BlockInvariant($"public Builder()"))
{
builder.AppendLineInvariant($"_original = {symbolNameWithGenerics}.Default;");
}
}
}
else
{
using (builder.BlockInvariant(
$"public Builder({symbolNameWithGenerics} original) : base(original ?? {symbolNameWithGenerics}.Default)"))
using (builder.BlockInvariant($"public Builder({symbolNameWithGenerics} original) : base(original ?? {symbolNameWithGenerics}.Default)"))
{
builder.AppendLineInvariant($"// Default constructor, the _original field is assigned in base constructor.");
}
@ -292,7 +329,7 @@ namespace Uno
builder.AppendLine();
var resetNone = _generateOptionCode
var resetNone = generateOption
? $"{Environment.NewLine} _isNone = false;"
: "";
@ -358,22 +395,21 @@ private bool _is{prop.Name}Set = false;
builder.AppendLine();
}
builder.AppendLineInvariant("/// <summary>");
builder.AppendLineInvariant($"/// Create an immutable instance of {symbolNameForXml}.");
builder.AppendLineInvariant("/// </summary>");
builder.AppendLineInvariant("/// <remarks>");
builder.AppendLineInvariant(
"/// Will return original if nothing changed in the builder (and an original was specified).");
builder.AppendLineInvariant(
"/// Application code should prefer the usage of implicit casting which is calling this method.");
builder.AppendLineInvariant(
$"/// **THIS METHOD IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant("/// </remarks>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public {newModifier}{symbolNameWithGenerics} ToImmutable()"))
if(!typeSymbol.IsAbstract)
{
builder.AppendLine(
$@"var cachedResult = _cachedResult as {symbolNameWithGenerics};
builder.AppendLineInvariant("/// <summary>");
builder.AppendLineInvariant($"/// Create an immutable instance of {symbolNameForXml}.");
builder.AppendLineInvariant("/// </summary>");
builder.AppendLineInvariant("/// <remarks>");
builder.AppendLineInvariant("/// Will return original if nothing changed in the builder (and an original was specified).");
builder.AppendLineInvariant("/// Application code should prefer the usage of implicit casting which is calling this method.");
builder.AppendLineInvariant($"/// **THIS METHOD IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant("/// </remarks>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public {newModifier}{symbolNameWithGenerics} ToImmutable()"))
{
builder.AppendLine(
$@"var cachedResult = _cachedResult as {symbolNameWithGenerics};
if(cachedResult != null)
{{
return cachedResult; // already computed, no need to redo this.
@ -388,11 +424,12 @@ if (_isDirty)
}}
}}
return ({symbolNameWithGenerics})(_cachedResult = _original);");
builder.AppendLine();
}
builder.AppendLine();
}
builder.AppendLine();
if (properties.Any())
{
builder.AppendLineInvariant("{0}", $"#region .WithXXX() methods on {symbolNameWithGenerics}.Builder");
@ -404,15 +441,12 @@ return ({symbolNameWithGenerics})(_cachedResult = _original);");
builder.AppendLineInvariant($"/// Set property {prop.Name} in a fluent declaration.");
builder.AppendLineInvariant($"/// </summary>");
builder.AppendLineInvariant($"/// <remarks>");
builder.AppendLineInvariant(
$"/// **THIS METHOD IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant($"/// **THIS METHOD IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant($"/// </remarks>");
builder.AppendLineInvariant("/// <example>");
builder.AppendLineInvariant("{0}",
$"/// var builder = new {symbolNameForXml}.Builder {{ {prop1Name} = xxx, ... }}; // create a builder instance");
builder.AppendLineInvariant("{0}", $"/// var builder = new {symbolNameForXml}.Builder {{ {prop1Name} = xxx, ... }}; // create a builder instance");
builder.AppendLineInvariant($"/// {prop.Type} new{prop.Name}Value = [...];");
builder.AppendLineInvariant(
$"/// {symbolNameForXml} instance = builder.With{prop.Name}(new{prop.Name}Value); // create an immutable instance");
builder.AppendLineInvariant($"/// {symbolNameForXml} instance = builder.With{prop.Name}(new{prop.Name}Value); // create an immutable instance");
builder.AppendLineInvariant("/// </example>");
using (builder.BlockInvariant($"public {newPropertyModifier}Builder With{prop.Name}({prop.Type} value)"))
{
@ -423,23 +457,17 @@ return ({symbolNameWithGenerics})(_cachedResult = _original);");
builder.AppendLine();
builder.AppendLineInvariant($"/// <summary>");
builder.AppendLineInvariant(
$"/// Set property {prop.Name} in a fluent declaration by projecting previous value.");
builder.AppendLineInvariant($"/// Set property {prop.Name} in a fluent declaration by projecting previous value.");
builder.AppendLineInvariant($"/// </summary>");
builder.AppendLineInvariant($"/// <remarks>");
builder.AppendLineInvariant(
$"/// **THIS METHOD IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant(
$"/// The selector will be called immediately. The main usage of this overload is to specify a _method group_.");
builder.AppendLineInvariant($"/// **THIS METHOD IS NOT THREAD-SAFE** (it shouldn't be accessed concurrently from many threads)");
builder.AppendLineInvariant($"/// The selector will be called immediately. The main usage of this overload is to specify a _method group_.");
builder.AppendLineInvariant($"/// </remarks>");
builder.AppendLineInvariant("/// <example>");
builder.AppendLineInvariant("{0}",
$"/// var builder = new {symbolNameForXml}.Builder {{ {prop1Name} = xxx, ... }}; // create a builder instance");
builder.AppendLineInvariant(
$"/// {symbolNameForXml} instance = builder.With{prop.Name}(previous{prop.Name}Value => new {prop.Type}(...)); // create an immutable instance");
builder.AppendLineInvariant("{0}", $"/// var builder = new {symbolNameForXml}.Builder {{ {prop1Name} = xxx, ... }}; // create a builder instance");
builder.AppendLineInvariant($"/// {symbolNameForXml} instance = builder.With{prop.Name}(previous{prop.Name}Value => new {prop.Type}(...)); // create an immutable instance");
builder.AppendLineInvariant("/// </example>");
using (builder.BlockInvariant(
$"public {newPropertyModifier}Builder With{prop.Name}(Func<{prop.Type}, {prop.Type}> valueSelector)"))
using (builder.BlockInvariant($"public {newPropertyModifier}Builder With{prop.Name}(Func<{prop.Type}, {prop.Type}> valueSelector)"))
{
builder.AppendLineInvariant($"{prop.Name} = valueSelector({prop.Name});");
builder.AppendLineInvariant("return this;");
@ -452,13 +480,12 @@ return ({symbolNameWithGenerics})(_cachedResult = _original);");
}
}
if (_generateOptionCode)
if (generateOption)
{
builder.AppendLine();
builder.AppendLineInvariant($"#region Uno.Option<{symbolNameWithGenerics}>'s specific code");
builder.AppendLine();
builder.AppendLineInvariant(
$"public static readonly {newModifier}global::Uno.Option<{symbolNameWithGenerics}> None = global::Uno.Option.None<{symbolNameWithGenerics}>();");
builder.AppendLineInvariant($"public static readonly {newModifier}global::Uno.Option<{symbolNameWithGenerics}> None = global::Uno.Option.None<{symbolNameWithGenerics}>();");
builder.AppendLine();
using (builder.BlockInvariant($"partial class Builder"))
@ -513,10 +540,8 @@ public static implicit operator global::Uno.Option<{symbolNameWithGenerics}>(Bui
builder.AppendLineInvariant($"// Default constructor - to ensure it's not defined by application code.");
builder.AppendLineInvariant($"//");
builder.AppendLineInvariant(
$"// \"error CS0111: Type '{symbolNameWithGenerics}' already defines a member called '.ctor' with the same parameter types\":");
builder.AppendLineInvariant(
$"// => You have this error? it's because you defined a default constructor on your class!");
builder.AppendLineInvariant($"// \"error CS0111: Type '{symbolNameWithGenerics}' already defines a member called '.ctor' with the same parameter types\":");
builder.AppendLineInvariant($"// => You have this error? it's because you defined a default constructor on your class!");
builder.AppendLineInvariant($"//");
builder.AppendLineInvariant($"// New instances should use the builder instead.");
builder.AppendLineInvariant($"// We know, it's not compatible with Newtownsoft's JSON.net: It's by-design.");
@ -524,8 +549,7 @@ public static implicit operator global::Uno.Option<{symbolNameWithGenerics}>(Bui
builder.AppendLineInvariant($"//");
builder.AppendLineInvariant($"// Send your complaints or inquiried to \"architecture [-@-] nventive [.] com\".");
builder.AppendLineInvariant($"//");
builder.AppendLineInvariant("{0}",
$"protected {symbolName}() {{}} // see previous comments if you want this removed (TL;DR: you can't)");
builder.AppendLineInvariant("{0}", $"protected {symbolName}() {{}} // see previous comments if you want this removed (TL;DR: you can't)");
builder.AppendLine();
@ -556,8 +580,10 @@ public static implicit operator global::Uno.Option<{symbolNameWithGenerics}>(Bui
builder.AppendLine();
builder.AppendLine(
$@"// Implicit cast from {symbolNameWithGenerics} to Builder: will simply create a new instance of the builder.
if (!typeSymbol.IsAbstract)
{
builder.AppendLine(
$@"// Implicit cast from {symbolNameWithGenerics} to Builder: will simply create a new instance of the builder.
public static implicit operator Builder({symbolNameWithGenerics} original)
{{
return new Builder(original);
@ -568,13 +594,14 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
{{
return builder.ToImmutable();
}}");
builder.AppendLine();
builder.AppendLine();
}
}
builder.AppendLine();
using (builder.BlockInvariant($"{typeSymbol.GetAccessibilityAsCSharpCodeString()} static partial class {symbolName}Extensions"))
{
if (properties.Any())
if (properties.Any() && !typeSymbol.IsAbstract)
{
var builderName = $"{symbolNameWithGenerics}.Builder";
@ -622,7 +649,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLine();
if (_generateOptionCode)
if (generateOption)
{
builder.AppendLineInvariant("/// <summary>");
builder.AppendLineInvariant($"/// Set property {prop.Name} in a fluent declaration.");
@ -638,7 +665,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"/// Option&lt;{symbolNameForXml}&gt; modified = original.With{prop.Name}(new{prop.Name}Value); // result type is Option.Some");
builder.AppendLineInvariant("/// </example>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this Option<{symbolNameWithGenerics}> optionEntity, {prop.Type} value)"))
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this global::Uno.Option<{symbolNameWithGenerics}> optionEntity, {prop.Type} value)"))
{
builder.AppendLineInvariant($"return {builderName}.FromOption(optionEntity).With{prop.Name}(value);");
}
@ -659,7 +686,7 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
builder.AppendLineInvariant($"/// {symbolNameForXml} modified = original.With{prop.Name}(previous{prop.Name}Value => new {prop.Type}(...)); // create a new modified immutable instance");
builder.AppendLineInvariant("/// </example>");
builder.AppendLineInvariant("[global::System.Diagnostics.Contracts.Pure]");
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this Option<{symbolNameWithGenerics}> optionEntity, Func<{prop.Type}, {prop.Type}> valueSelector)"))
using (builder.BlockInvariant($"public static {builderName} With{prop.Name}{genericArguments}(this global::Uno.Option<{symbolNameWithGenerics}> optionEntity, Func<{prop.Type}, {prop.Type}> valueSelector)"))
{
builder.AppendLineInvariant($"return {builderName}.FromOption(optionEntity).With{prop.Name}(valueSelector);");
}
@ -676,6 +703,113 @@ public static implicit operator {symbolNameWithGenerics}(Builder builder)
_context.AddCompilationUnit(resultFileName, builder.ToString());
}
private void ValidateType(
IIndentedStringBuilder builder,
INamedTypeSymbol typeSymbol,
(bool isBaseType, string baseType, string builderBaseType, bool isImmutablePresent) baseTypeInfo,
SymbolNames symbolNames, IPropertySymbol[] typeProperties)
{
if (!IsFromPartialDeclaration(typeSymbol))
{
builder.AppendLineInvariant(
$"#warning {nameof(ImmutableGenerator)}: you should add the partial modifier to the class {symbolNames.SymbolNameWithGenerics}.");
}
if (typeSymbol.IsValueType)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: Type {symbolNames.SymbolNameWithGenerics} **MUST** be a class, not a struct.");
}
if (baseTypeInfo.isBaseType && baseTypeInfo.baseType == null)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: Type {symbolNames.SymbolNameWithGenerics} **MUST** derive from an immutable class.");
}
void CheckTypeImmutable(ITypeSymbol type, string typeSource)
{
if (type.IsGenericArgument())
{
var typeParameter = type as ITypeParameterSymbol;
if (typeParameter?.ConstraintTypes.Any() ?? false)
{
foreach (var constraintType in typeParameter.ConstraintTypes)
{
CheckTypeImmutable(constraintType, $"{typeSymbol} / Generic type {typeParameter}:{constraintType}");
}
}
else
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: {typeSource} is of generic type {type} which isn't restricted to immutable. You can also make your class abstract.");
}
return; // ok
}
if (type.FindAttribute(_immutableBuilderAttributeSymbol) != null)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: {typeSource} type {type} IS A BUILDER! It cannot be used in an immutable entity.");
}
else if (!type.IsImmutable(_generationOptions.treatArrayAsImmutable))
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: {typeSource} type {type} is not immutable. It cannot be used in an immutable entity.");
}
if(type is INamedTypeSymbol namedType)
{
foreach (var typeArgument in namedType.TypeArguments)
{
CheckTypeImmutable(typeArgument, $"{typeSource} (argument type {typeArgument})");
}
}
}
foreach (var prop in typeProperties)
{
if (prop.IsStatic)
{
continue; // we don't care about static stuff.
}
if (!prop.IsReadOnly)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: Non-static property {symbolNames.SymbolNameWithGenerics}.{prop.Name} cannot have a setter, even a private one. You must remove it for immutable generation.");
}
if (!typeSymbol.IsAbstract)
{
CheckTypeImmutable(prop.Type, $"Property {symbolNames.SymbolNameWithGenerics}.{prop.Name}");
}
}
foreach (var typeArgument in typeSymbol.BaseType.TypeArguments)
{
if (typeSymbol.IsAbstract && typeSymbol is ITypeParameterSymbol)
{
continue;
}
CheckTypeImmutable(typeArgument, $"Type Argument {typeArgument.Name}");
}
foreach (var field in typeSymbol.GetFields())
{
if (field.IsImplicitlyDeclared)
{
continue; // that's from the compiler, it's ok.
}
if (!field.IsStatic)
{
builder.AppendLineInvariant(
$"#error {nameof(ImmutableGenerator)}: Immutable type {symbolNames.SymbolNameWithGenerics} cannot have a non-static field {field.Name}. You must remove it for immutable generation.");
}
}
}
private string GetAttributes(Regex[] classCopyIgnoreRegexes, IPropertySymbol prop)
{
var allAttributesIgnores =

Просмотреть файл

@ -29,5 +29,11 @@ namespace Uno
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class EqualityHashAttribute : Attribute
{
public EqualityMode Mode { get; }
public EqualityHashAttribute(EqualityMode mode = EqualityMode.UseEquality)
{
Mode = mode;
}
}
}

Просмотреть файл

@ -29,5 +29,11 @@ namespace Uno
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class EqualityKeyAttribute : Attribute
{
public EqualityMode Mode { get; }
public EqualityKeyAttribute(EqualityMode mode = EqualityMode.UseEquality)
{
Mode = mode;
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// ******************************************************************
// Copyright <20> 2015-2018 nventive inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ******************************************************************
namespace Uno
{
public enum EqualityMode
{
UseKeyEquality,
UseEquality
}
}

Просмотреть файл

@ -24,7 +24,7 @@ namespace Uno
/// <remarks>
/// Can be put on an assembly, a type or a property.
/// </remarks>
[System.AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Property, Inherited = true, AllowMultiple = true)]
[System.AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public sealed class ImmutableAttributeCopyIgnoreAttribute : Attribute
{
/// <summary>

Просмотреть файл

@ -0,0 +1,28 @@
// ******************************************************************
// Copyright <20> 2015-2018 nventive inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ******************************************************************
using System;
namespace Uno
{
[System.AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
public sealed class ImmutableGenerationOptionsAttribute : Attribute
{
public bool TreatArrayAsImmutable { get; set; } = false;
public bool GenerateOptionCode { get; set; } = true;
}
}