using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Vellum.Generator
{
///
/// Reports a call that saves or loads a value Vellum can't: a type argument of a method some assembly names with
/// [assembly: RegisterSaveMethod].
///
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SavedTypeAnalyzer : DiagnosticAnalyzer
{
public const string RegisterSaveMethodName = "Vellum.RegisterSaveMethodAttribute";
public override ImmutableArray SupportedDiagnostics
{
get
{
return ImmutableArray.Create(VellumDiagnostics.UnsaveableType);
}
}
///
/// Returns each method every [assembly: RegisterSaveMethod] in the compilation and its references names, by
/// its documentation ID, with the types it saves.
///
public static Dictionary> Registered(Compilation compilation)
{
var returnMe = new Dictionary>();
var assemblies = new List { compilation.Assembly };
assemblies.AddRange(compilation.SourceModule.ReferencedAssemblySymbols);
foreach (IAssemblySymbol assembly in assemblies)
{
foreach (AttributeData attribute in assembly.GetAttributes())
{
if (attribute.AttributeClass?.ToDisplayString() != RegisterSaveMethodName
|| attribute.ConstructorArguments.Length != 2)
{
continue;
}
var method = attribute.ConstructorArguments[0].Value as string;
var saved = attribute.ConstructorArguments[1].Value as ITypeSymbol;
if (method == null || saved == null)
{
continue;
}
List types;
if (!returnMe.TryGetValue(method, out types))
{
types = new List();
returnMe.Add(method, types);
}
types.Add(saved);
}
}
return returnMe;
}
///
/// Returns what returns, keyed by the method each ID names. An ID that names nothing
/// the compilation can see is left out.
///
public static Dictionary> RegisteredBySymbol(Compilation compilation)
{
var returnMe = new Dictionary>(SymbolEqualityComparer.Default);
foreach (KeyValuePair> registered in Registered(compilation))
{
ISymbol symbol = DocumentationCommentId.GetFirstSymbolForDeclarationId(registered.Key, compilation);
if (symbol == null)
{
continue;
}
List types;
if (!returnMe.TryGetValue(symbol.OriginalDefinition, out types))
{
types = new List();
returnMe.Add(symbol.OriginalDefinition, types);
}
types.AddRange(registered.Value);
}
return returnMe;
}
///
/// Returns what the type parameters of and the types around it stand for, outermost
/// type first.
///
public static ImmutableArray ClassArgumentsOf(INamedTypeSymbol type)
{
var chain = new List();
for (INamedTypeSymbol current = type; current != null; current = current.ContainingType)
{
chain.Add(current);
}
ImmutableArray.Builder returnMe = ImmutableArray.CreateBuilder();
for (int i = chain.Count - 1; i >= 0; --i)
{
returnMe.AddRange(chain[i].TypeArguments);
}
return returnMe.ToImmutable();
}
// A method has type arguments to check if it, a method it overrides, or a type around either has type
// parameters. An override in a plain class can still override a method of a generic one.
private static bool HasTypeArguments(IMethodSymbol method)
{
for (IMethodSymbol current = method; current != null; current = current.ConstructedFrom.OverriddenMethod)
{
if (current.IsGenericMethod)
{
return true;
}
for (INamedTypeSymbol type = current.ContainingType; type != null; type = type.ContainingType)
{
if (type.Arity > 0)
{
return true;
}
}
}
return false;
}
// A template that nests deeper than a type can is left unchecked, which also bounds the recursion.
private static ITypeSymbol Fill(Compilation compilation, ITypeSymbol template,
ImmutableArray arguments, ImmutableArray classArguments, int depth)
{
if (depth > SaveTemplate.MaxDepth)
{
return null;
}
int number = SaveTemplate.StandInNumber(template);
if (number >= 0)
{
return number < arguments.Length ? arguments[number] : null;
}
int classNumber = SaveTemplate.ClassStandInNumber(template);
if (classNumber >= 0)
{
return classNumber < classArguments.Length ? classArguments[classNumber] : null;
}
var array = template as IArrayTypeSymbol;
if (array != null)
{
ITypeSymbol element = Fill(compilation, array.ElementType, arguments, classArguments, depth + 1);
if (element == null)
{
return null;
}
return ReferenceEquals(element, array.ElementType)
? template
: compilation.CreateArrayTypeSymbol(element, array.Rank);
}
var named = template as INamedTypeSymbol;
if (named == null)
{
return template;
}
// A type nested in a generic one carries the outer type's arguments too.
INamedTypeSymbol shape = named.OriginalDefinition;
bool changed = false;
INamedTypeSymbol container = named.ContainingType;
if (container != null && container.IsGenericType)
{
var filledContainer =
Fill(compilation, container, arguments, classArguments, depth + 1) as INamedTypeSymbol;
shape = filledContainer?.GetTypeMembers(named.Name, named.Arity).FirstOrDefault();
if (shape == null)
{
return null;
}
changed = !ReferenceEquals(filledContainer, container);
}
var filled = new ITypeSymbol[named.Arity];
for (int i = 0; i < filled.Length; ++i)
{
filled[i] = Fill(compilation, named.TypeArguments[i], arguments, classArguments, depth + 1);
if (filled[i] == null)
{
return null;
}
changed |= !ReferenceEquals(filled[i], named.TypeArguments[i]);
}
if (!changed)
{
return template;
}
return named.Arity == 0 ? shape : shape.Construct(filled);
}
///
/// Returns with each stand-in type replaced by the type argument it stands for, or
/// null if one stands for an argument the call doesn't have.
///
/// What the method's own type parameters stand for.
/// What the type parameters of the type holding the method stand for.
public static ITypeSymbol Fill(Compilation compilation, ITypeSymbol template,
ImmutableArray arguments, ImmutableArray classArguments)
{
return Fill(compilation, template, arguments, classArguments, 1);
}
///
/// Returns filled for a method of a type that has no type parameters.
///
public static ITypeSymbol Fill(Compilation compilation, ITypeSymbol template,
ImmutableArray arguments)
{
return Fill(compilation, template, arguments, ImmutableArray.Empty, 1);
}
// Callers name what they save with a constant first argument, eg: a field's name. A property or event is
// named for itself, and a creation for its type.
private static string SavedName(IOperation use)
{
var invocation = use as IInvocationOperation;
if (invocation == null)
{
IMethodSymbol called = SaveMethods.Called(use)[0];
return called.AssociatedSymbol?.Name
?? (called.MethodKind == MethodKind.Constructor ? called.ContainingType.Name : called.Name);
}
if (invocation.Arguments.Length == 0)
{
return invocation.TargetMethod.Name;
}
IOperation first = invocation.Arguments[0].Value;
if (first.ConstantValue.HasValue && first.ConstantValue.Value is string)
{
return (string)first.ConstantValue.Value;
}
return first.Syntax.ToString();
}
private static void Check(OperationAnalysisContext context,
Dictionary> registered, HashSet converted,
Dictionary> families, Dictionary> saveableArguments)
{
// A class and the interface it implements are both registered, so one type can come up twice.
HashSet checkedTypes = null;
ITypeSymbol receiver = SaveMethods.ReceiverOf(context.Operation);
foreach (IMethodSymbol method in SaveMethods.Called(context.Operation))
{
if (!HasTypeArguments(method))
{
continue;
}
foreach (IMethodSymbol candidate in SaveMethods.Candidates(method, receiver))
{
List templates;
if (!registered.TryGetValue(candidate.OriginalDefinition, out templates))
{
continue;
}
// A candidate is seen through the type arguments of its own containing type, which an interface or
// a base class may order differently from the class being called.
ImmutableArray classArguments = ClassArgumentsOf(candidate.ContainingType);
foreach (ITypeSymbol template in templates)
{
// A type parameter is left to the callers of the generic code, and an error type to its own
// error.
ITypeSymbol saved = Fill(context.Compilation, template, method.TypeArguments, classArguments);
if (checkedTypes == null)
{
checkedTypes = new HashSet();
}
if (saved == null || !checkedTypes.Add(saved.ToDisplayString()))
{
continue;
}
TypeShape shape = TypeShape.Of(saved);
int unsaveable = shape.FindUnsaveable(converted, families, saveableArguments);
if (unsaveable < 0)
{
continue;
}
context.ReportDiagnostic(shape.UnsaveableDiagnostic(context.Operation.Syntax.GetLocation(),
SavedName(context.Operation), unsaveable));
}
}
}
}
private static void Start(CompilationStartAnalysisContext context)
{
Dictionary> registered = RegisteredBySymbol(context.Compilation);
if (registered.Count == 0)
{
return;
}
// The generator already lists every converter and reports their mistakes.
ConverterList converters = ConverterScan.FromSavedWith(context.Compilation);
HashSet converted = converters.ConvertedNames();
Dictionary> families = converters.FamilyBaseNames();
Dictionary> saveableArguments = converters.SaveableArguments();
context.RegisterOperationAction(
operation => Check(operation, registered, converted, families, saveableArguments),
SaveMethods.UseKinds);
}
public override void Initialize(AnalysisContext context)
{
// Saving code is often generated, eg: by Ariadne.Generator, which maps it back to the lines it came from.
context.ConfigureGeneratedCodeAnalysis(
GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(Start);
}
}
}