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