using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Ariadne.Generator { /// /// A field or auto-property an arc saves. /// public class SavedMember { /// /// This field holds the field or autoproperty's name. /// public readonly string Key; public readonly ITypeSymbol Type; /// /// This field holds the field or auto-property, as the script declares it. /// public readonly ISymbol Declared; public SavedMember(string key, ITypeSymbol type, ISymbol declared) { Key = key; Type = type; Declared = declared; } /// /// Gets whether the script gives the member a value where it declares it, which a load from a save that lacks /// the member can fall back on. /// public bool HasInitializer { get { foreach (SyntaxReference reference in Declared.DeclaringSyntaxReferences) { SyntaxNode syntax = reference.GetSyntax(); if (syntax is VariableDeclaratorSyntax variable && variable.Initializer != null) { return true; } if (syntax is PropertyDeclarationSyntax property && property.Initializer != null) { return true; } } return false; } } /// /// Gets the expression generated code reads and assigns the member through. /// public string Access { get { return "this." + SavedFields.Identifier(Key); } } } /// /// Finds the fields and auto-properties an arc class saves: every instance one, readonly included, not marked /// [Scratch]. /// public static class SavedFields { public const string ScratchAttributeName = "Ariadne.ScratchAttribute"; /// /// Returns as C# can write it, with an @ if it's a keyword. /// public static string Identifier(string name) { if (SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None) { return "@" + name; } return name; } private static bool IsScratch(ISymbol symbol) { return symbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == ScratchAttributeName); } private static int DeclarationOrder(SavedMember member) { Location location = member.Declared.Locations.FirstOrDefault(); if (location == null || !location.IsInSource) { return 0; } return location.SourceSpan.Start; } private static string DeclarationFile(SavedMember member) { return member.Declared.Locations.FirstOrDefault()?.SourceTree?.FilePath ?? ""; } /// /// Returns the members itself declares that it saves, in declaration order. /// public static List Of(INamedTypeSymbol type) { var returnMe = new List(); foreach (IFieldSymbol field in type.GetMembers().OfType()) { ISymbol declared = field.AssociatedSymbol ?? field; if (field.IsStatic || field.IsConst || IsScratch(field) || IsScratch(declared)) { continue; } // Only an auto-property's backing field is named for something a script declared. if (field.IsImplicitlyDeclared && !(declared is IPropertySymbol)) { continue; } returnMe.Add(new SavedMember(declared.Name, field.Type, declared)); } return returnMe.OrderBy(DeclarationFile, System.StringComparer.Ordinal).ThenBy(DeclarationOrder).ToList(); } /// /// Reports each field-like event itself declares that isn't marked [Scratch]. /// public static void ReportUnsavableEvents(INamedTypeSymbol type, List diagnostics) { foreach (IEventSymbol declared in type.GetMembers().OfType()) { // Roslyn doesn't list a field-like event's backing field among the members, so go by the syntax. An // event with its own accessors keeps its subscribers in a field the script declared. bool isFieldLike = declared.DeclaringSyntaxReferences.Any( reference => reference.GetSyntax() is VariableDeclaratorSyntax); if (isFieldLike && !declared.IsStatic && !IsScratch(declared)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.UnsavableEvent, declared.Locations.FirstOrDefault(), declared.Name)); } } } /// /// Reports each of whose key a base class of already saves. /// /// A base compiled elsewhere shows only its public and protected members. public static void ReportDuplicateKeys(INamedTypeSymbol type, INamedTypeSymbol arcDefinition, List members, List diagnostics) { var baseKeys = new HashSet(System.StringComparer.Ordinal); for (INamedTypeSymbol baseType = type.BaseType; baseType != null && !SymbolEqualityComparer.Default.Equals(baseType.OriginalDefinition, arcDefinition); baseType = baseType.BaseType) { baseKeys.UnionWith(Of(baseType).Select(member => member.Key)); } foreach (SavedMember member in members) { if (baseKeys.Contains(member.Key)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.DuplicateSaveKey, member.Declared.Locations.FirstOrDefault(), member.Key, type.Name)); } } } } }