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));
}
}
}
}
}