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