using System; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; namespace Ariadne.Generator { /// /// Reports script mistakes that concern the class as a whole, rather than one passage. /// public static class ScriptChecks { /// /// This field holds the Arc members only generated code may use. /// public static readonly HashSet Plumbing = new HashSet { "__Mark", "__Enter", "__Exit", "__ResumeKey", "__Resume", "__SaveLocals", "__SaveFields", "__CreateFrame", "__CurrentFrame", }; private static void ReportPlumbing(ScriptClass script, List diagnostics) { INamedTypeSymbol arcDefinition = script.ArcBase.OriginalDefinition; foreach (SyntaxReference reference in script.Type.DeclaringSyntaxReferences) { SyntaxNode declaration = reference.GetSyntax(); SemanticModel model = script.ModelFor(declaration); foreach (SimpleNameSyntax name in declaration.DescendantNodes().OfType()) { if (!Plumbing.Contains(name.Identifier.ValueText)) { continue; } ISymbol symbol = model.GetSymbolInfo(name).Symbol; if (symbol != null && SymbolEqualityComparer.Default.Equals( symbol.ContainingType?.OriginalDefinition, arcDefinition)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.GeneratedCodePlumbing, name.GetLocation(), name.Identifier.ValueText)); } } } } // Only the copy of a passage is handed the tick's time, and only this arc's own name for it is rewritten. private static void ReportTimeOutsidePassages(ScriptClass script, List diagnostics) { foreach (SyntaxReference reference in script.Type.DeclaringSyntaxReferences) { SyntaxNode declaration = reference.GetSyntax(); SemanticModel model = script.ModelFor(declaration); foreach (SimpleNameSyntax name in declaration.DescendantNodes().OfType()) { if (script.TimeMemberOf(model.GetSymbolInfo(name).Symbol) == null) { continue; } var method = name.FirstAncestorOrSelf(); IMethodSymbol symbol = method == null ? null : model.GetDeclaredSymbol(method); var access = name.Parent as MemberAccessExpressionSyntax; bool onThisArc = access == null || access.Name != name || access.Expression is ThisExpressionSyntax || access.Expression is BaseExpressionSyntax; if (symbol == null || !script.Passages.Contains(symbol) || !onThisArc) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.TimeOutsideAPassage, name.GetLocation(), name.Identifier.ValueText)); } } } } // A passage shares its name with its saved keys and frame fields, which a base class's passages use too. private static void ReportOverloadedPassages(ScriptClass script, List diagnostics) { var inheritedNames = new HashSet(StringComparer.Ordinal); foreach (ScriptClass inherited in script.InheritedScripts()) { // A derived class's Run overrides its base's, so they're one passage to a caller. inheritedNames.UnionWith(inherited.Passages .Where(passage => !SymbolEqualityComparer.Default.Equals(passage, inherited.Run)) .Select(passage => passage.Name)); } // An override is the passage it overrides, not another by the same name. foreach (IGrouping name in script.Passages .Where(passage => !(passage.IsOverride && script.IsPassage(passage.OverriddenMethod))) .GroupBy(passage => passage.Name)) { if (name.Count() < 2 && !inheritedNames.Contains(name.Key)) { continue; } foreach (IMethodSymbol passage in name) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.OverloadedPassage, passage.Locations.FirstOrDefault(), passage.Name)); } } } private static void ReportRefReturningPassages(ScriptClass script, List diagnostics) { foreach (IMethodSymbol passage in script.Passages) { if (passage.ReturnsByRef || passage.ReturnsByRefReadonly) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.PassageShape, passage.Locations.FirstOrDefault(), passage.Name)); } } } // C# 13 lets params take any collection a collection expression can build, which the generator can't make once // and save across the pause the way it can an array. private static void ReportParamsCollections(ScriptClass script, List diagnostics) { foreach (IMethodSymbol passage in script.Passages) { foreach (IParameterSymbol parameter in passage.Parameters) { if (parameter.IsParams && !(parameter.Type is IArrayTypeSymbol)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.ParamsCollection, parameter.Locations.FirstOrDefault(), parameter.Name)); } } } } private static bool InPassageBody(SyntaxNode node, ScriptClass script) { MethodDeclarationSyntax method = node.FirstAncestorOrSelf(); if (method == null || method.Body == null) { return false; } var symbol = script.ModelFor(method).GetDeclaredSymbol(method) as IMethodSymbol; return symbol != null && script.Passages.Contains(symbol); } // PassageWriter reports misplaced pauses inside Passages. These are the rest, eg: in a constructor, property // or expression-bodied method, which would otherwise compile and then throw. private static void ReportPausesOutsidePassages(ScriptClass script, List diagnostics) { foreach (SyntaxReference reference in script.Type.DeclaringSyntaxReferences) { foreach (InvocationExpressionSyntax invocation in reference.GetSyntax().DescendantNodes().OfType()) { if (InPassageBody(invocation, script)) { continue; } if (script.IsYield(invocation)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.MisplacedYield, invocation.GetLocation())); continue; } if (script.IsWait(invocation)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.MisplacedWait, invocation.GetLocation(), "a member that isn't a passage", "Move it into a method with a block body.")); continue; } IMethodSymbol passage = script.AnyPassageCalled(invocation); if (passage != null) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.MisplacedPassageCall, invocation.GetLocation(), passage.Name)); } } } } // A passage is entered afresh on every tick, so its ref and out arguments are worked out each time. Only a // name stays the same variable from one tick to the next. private static void ReportMovingReferences(ScriptClass script, List diagnostics) { foreach (SyntaxReference reference in script.Type.DeclaringSyntaxReferences) { foreach (InvocationExpressionSyntax invocation in reference.GetSyntax().DescendantNodes().OfType()) { IMethodSymbol passage = script.AnyPassageCalled(invocation); if (passage == null || !InPassageBody(invocation, script)) { continue; } var operation = script.ModelFor(invocation).GetOperation(invocation) as IInvocationOperation; foreach (ArgumentSyntax argument in invocation.ArgumentList.Arguments) { // A ref readonly parameter takes an in argument or a plain value as well as a ref one. IParameterSymbol parameter = operation?.Arguments .FirstOrDefault(each => each.Syntax == argument)?.Parameter; if ((argument.RefKindKeyword.IsKind(SyntaxKind.RefKeyword) || argument.RefKindKeyword.IsKind(SyntaxKind.OutKeyword) || parameter?.RefKind == RefKind.RefReadOnlyParameter) && !ScriptClass.IsPlainTarget(argument.Expression) && !(argument.Expression is DeclarationExpressionSyntax)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.MovingReference, argument.GetLocation(), passage.Name)); } } } } } private static bool IsIterator(MethodDeclarationSyntax method) { return method.Body != null && method.Body.DescendantNodes(node => !(node is AnonymousFunctionExpressionSyntax) && !(node is LocalFunctionStatementSyntax)) .OfType().Any(); } // A virtual method is a passage only with a block body, so an override of one with an expression body would // look like it should be allowed to pause. private static string DescribeNonPassageOverridden(IMethodSymbol overridden) { bool expressionBodied = overridden.DeclaringSyntaxReferences .Select(reference => reference.GetSyntax()) .OfType() .Any(declaration => declaration.ExpressionBody != null); if (expressionBodied) { return "an override of '" + overridden.Name + "', which has an expression body and so isn't a " + "passage. Give it a block body"; } return "an override of a method that isn't a passage"; } // The generated copy is a plain private instance method, so each of these would be lost or break in it. private static void ReportPassageModifiers(ScriptClass script, List diagnostics) { foreach (IMethodSymbol passage in script.Passages) { MethodDeclarationSyntax syntax = script.Methods[passage]; var unsupported = new List(); if (passage.IsStatic) { unsupported.Add("static"); } if (passage.IsAsync) { unsupported.Add("async"); } if (syntax.Modifiers.Any(SyntaxKind.UnsafeKeyword)) { unsupported.Add("unsafe"); } if (IsIterator(syntax)) { unsupported.Add("an iterator"); } // Run overrides Arc.Run, and an override of a passage is dispatched like the passage it overrides. if (passage.IsOverride && !SymbolEqualityComparer.Default.Equals(passage, script.Run) && !script.IsPassage(passage.OverriddenMethod)) { unsupported.Add(DescribeNonPassageOverridden(passage.OverriddenMethod)); } foreach (string description in unsupported) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.UnsupportedPassageModifier, passage.Locations.FirstOrDefault(), passage.Name, description)); } } } /// /// Reports each Yield() in , an arc class with no passages, which can only mean /// one in a member that can't pause. /// public static void ReportPausesInNonScript(INamedTypeSymbol type, Compilation compilation, List diagnostics) { INamedTypeSymbol arcDefinition = compilation.GetTypeByMetadataName(ScriptClass.ArcMetadataName); IMethodSymbol yieldDefinition = arcDefinition.GetMembers("Yield").OfType().First(); foreach (SyntaxReference reference in type.DeclaringSyntaxReferences) { SyntaxNode declaration = reference.GetSyntax(); SemanticModel model = compilation.GetSemanticModel(declaration.SyntaxTree); foreach (InvocationExpressionSyntax invocation in declaration.DescendantNodes().OfType()) { var target = model.GetSymbolInfo(invocation).Symbol as IMethodSymbol; if (target != null && SymbolEqualityComparer.Default.Equals(target.OriginalDefinition, yieldDefinition)) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.MisplacedYield, invocation.GetLocation(), "a member that isn't a method with a block body", "Only a method with a block body can pause.")); } } } } /// /// Returns true if is built from one of without being /// that parameter itself. /// public static bool BuiltFrom(ITypeSymbol type, IReadOnlyList parameters) { if (type is ITypeParameterSymbol) { return false; } var pending = new Stack(); pending.Push(type); while (pending.Count > 0) { ITypeSymbol next = pending.Pop(); var parameter = next as ITypeParameterSymbol; if (parameter != null) { if (parameters.Contains(parameter, SymbolEqualityComparer.Default)) { return true; } continue; } var array = next as IArrayTypeSymbol; if (array != null) { pending.Push(array.ElementType); } var named = next as INamedTypeSymbol; if (named != null) { foreach (ITypeSymbol argument in named.TypeArguments) { pending.Push(argument); } } } return false; } // Returns the name of the class whose Run a call by name in a run of the passage stays on, or null if the run // reaches none. A class built into another assembly has no bodies to read, so its copy says. private static string FindRunByName(ScriptClass script, IMethodSymbol passage, HashSet seen) { if (script.FromMetadata) { return script.RunByNameMark(passage); } if (!seen.Add(passage) || script.Methods[passage].Body == null) { return null; } foreach (InvocationExpressionSyntax invocation in script.Methods[passage].Body.DescendantNodes().OfType()) { if (script.CallsRunByName(invocation)) { return invocation.FirstAncestorOrSelf().Identifier.ValueText; } IMethodSymbol callee = script.PassageCalled(invocation); if (callee != null) { string found = FindRunByName(script.OwnerOf(callee), callee, seen); if (found != null) { return found; } } } return null; } /// /// Returns the name of the class whose Run a call by name in a run of stays on, or /// null if the run reaches none. /// public static string RunByNameReachedFrom(ScriptClass script, IMethodSymbol passage) { return FindRunByName(script, passage, new HashSet(SymbolEqualityComparer.Default)); } // A passage copy calls Run in the class that wrote the call, so a derived class that overrides Run and runs // that code gets the base's Run where C# would get its own. private static void ReportRunByNameBehindAnOverride(ScriptClass script, List diagnostics) { if (script.Run == null) { return; } foreach (IMethodSymbol passage in script.Passages.Where(each => script.Methods[each].Body != null)) { foreach (InvocationExpressionSyntax invocation in script.Methods[passage].Body.DescendantNodes().OfType()) { IMethodSymbol callee = script.PassageCalled(invocation); if (callee == null || script.OwnerOf(callee) == script) { continue; } string reached = RunByNameReachedFrom(script.OwnerOf(callee), callee); if (reached != null) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.RunByNameBehindAnOverride, invocation.GetLocation(), callee.Name, reached)); } } } } // The whole class is walked, not only the passages, since a delegate made in a constructor or a helper would // still hold a passage whose placeholder Yield throws when it runs. private static void ReportPassageAsDelegate(ScriptClass script, List diagnostics) { foreach (SyntaxReference reference in script.Type.DeclaringSyntaxReferences) { SyntaxNode declaration = reference.GetSyntax(); SemanticModel model = script.ModelFor(declaration); foreach (SimpleNameSyntax name in declaration.DescendantNodes().OfType()) { SyntaxNode use = name; if ((name.Parent is MemberAccessExpressionSyntax access && access.Name == name) || (name.Parent is MemberBindingExpressionSyntax binding && binding.Name == name)) { use = name.Parent; } if (use.Parent is InvocationExpressionSyntax invocation && invocation.Expression == use) { continue; } SymbolInfo info = model.GetSymbolInfo(name); var method = (info.Symbol ?? info.CandidateSymbols.FirstOrDefault()) as IMethodSymbol; if (method == null || !script.IsPassage(method)) { continue; } // nameof(Act) names the passage without using it. if (name.FirstAncestorOrSelf(each => each.Expression is IdentifierNameSyntax caller && caller.Identifier.ValueText == "nameof" && model.GetSymbolInfo(each).Symbol == null) != null) { continue; } diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.PassageAsDelegate, use.GetLocation(), method.Name)); } } } // A virtual passage's locals live in the arc's frame, which holds one run of it at a time. private static void ReportVirtualPassageFrames(ScriptClass script, List diagnostics) { foreach (IMethodSymbol passage in script.Passages.Where(script.IsVirtualPassage)) { string reason = null; if (passage.IsGenericMethod) { reason = "has nowhere to keep the type arguments of a generic passage."; } else if (script.OwnsFrame(passage)) { reason = "can't hold a second run of it while the first is paused, and it reaches itself."; } if (reason != null) { diagnostics.Add(Diagnostic.Create(ScriptDiagnostics.VirtualPassageNeedsOwnFrame, passage.Locations.FirstOrDefault(), passage.Name, reason)); } } } public static void Report(ScriptClass script, List diagnostics) { ReportPlumbing(script, diagnostics); ReportTimeOutsidePassages(script, diagnostics); ReportOverloadedPassages(script, diagnostics); ReportRefReturningPassages(script, diagnostics); ReportParamsCollections(script, diagnostics); ReportPausesOutsidePassages(script, diagnostics); ReportMovingReferences(script, diagnostics); ReportPassageModifiers(script, diagnostics); ReportRunByNameBehindAnOverride(script, diagnostics); ReportPassageAsDelegate(script, diagnostics); ReportVirtualPassageFrames(script, diagnostics); } } }