Skip to content

Commit 007bd39

Browse files
committed
parameters on yarn functions and commands are now validated.
1 parent 7d8c02e commit 007bd39

File tree

6 files changed

+205
-15
lines changed

6 files changed

+205
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2222
- has one field which is the name the enum as declared in your Yarn
2323
- Intended to allow you to hint to the VSCode extension to offer enum based suggestions
2424
- this is temporary until forward declaration of Yarn enums into C# works
25+
- Incorrectly defined parameters for commands and functions will now generate diagnostics on the C# implementation, warnings exist for:
26+
- invalid types
27+
- incorrectly attributed parameters
2528

2629
### Changed
2730

Editor/Analysis/Action.cs

Lines changed: 151 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,17 +303,20 @@ public string ToJSON()
303303
}
304304
else if (isAnEnum)
305305
{
306-
paramObject["type"] = "enum";
307-
306+
var subtype = "any";
308307
var attribute = p.Attributes?.Where(a => a.AttributeClass?.Name == "YarnEnumParameterAttribute").First();
309308
if (attribute != null && attribute.ConstructorArguments.Count() > 0)
310309
{
311310
var enumType = attribute.ConstructorArguments[0];
312311
if (enumType.Type?.SpecialType == SpecialType.System_String)
313312
{
314-
paramObject["subtype"] = enumType.Value as string ?? p.YarnTypeString;
313+
subtype = enumType.Value as string ?? p.YarnTypeString;
315314
}
316315
}
316+
317+
paramObject["type"] = "enum";
318+
paramObject["subtype"] = subtype;
319+
317320
}
318321
else
319322
{
@@ -339,8 +342,9 @@ public string ToJSON()
339342
return Yarn.Unity.Editor.Json.Serialize(result);
340343
}
341344

342-
public List<Microsoft.CodeAnalysis.Diagnostic> Validate(Compilation compilation)
345+
public List<Microsoft.CodeAnalysis.Diagnostic> Validate(Compilation compilation, ILogger? logger)
343346
{
347+
logger?.WriteLine($"Beginning validation");
344348
var diagnostics = new List<Microsoft.CodeAnalysis.Diagnostic>();
345349
if (this.MethodDeclarationSyntax == null)
346350
{
@@ -432,11 +436,11 @@ public string ToJSON()
432436
break;
433437

434438
case ActionType.Command:
435-
diagnostics.AddRange(ValidateCommand(compilation));
439+
diagnostics.AddRange(ValidateCommand(compilation, logger));
436440
break;
437441

438442
case ActionType.Function:
439-
diagnostics.AddRange(ValidateFunction(compilation));
443+
diagnostics.AddRange(ValidateFunction(compilation, logger));
440444
break;
441445

442446
default:
@@ -447,9 +451,8 @@ public string ToJSON()
447451
return diagnostics;
448452
}
449453

450-
private IEnumerable<Diagnostic> ValidateFunction(Compilation compilation)
454+
private IEnumerable<Diagnostic> ValidateFunction(Compilation compilation, ILogger? logger)
451455
{
452-
453456
string identifier;
454457
Location returnTypeLocation;
455458
Location identifierLocation;
@@ -484,6 +487,12 @@ private IEnumerable<Diagnostic> ValidateFunction(Compilation compilation)
484487
yield return Diagnostic.Create(Diagnostics.YS1006YarnFunctionsMustBeStatic, identifierLocation);
485488
}
486489

490+
var paramDiags = ValidateParameters(compilation, logger);
491+
foreach (var p in paramDiags)
492+
{
493+
yield return p;
494+
}
495+
487496
// Functions must return a number, string, or bool
488497
var returnTypeSymbol = this.MethodSymbol.ReturnType;
489498

@@ -510,7 +519,135 @@ private IEnumerable<Diagnostic> ValidateFunction(Compilation compilation)
510519
}
511520
}
512521

513-
private IEnumerable<Diagnostic> ValidateCommand(Compilation compilation)
522+
// validates the parameters are correct
523+
private List<Diagnostic> ValidateParameters(Compilation compilation, ILogger? logger)
524+
{
525+
List<Diagnostic> diagnostics = new List<Diagnostic>();
526+
ParameterListSyntax? parameterList = null;
527+
Location? actionLocation = null;
528+
string? identifier = null;
529+
530+
if (this.MethodDeclarationSyntax is MethodDeclarationSyntax methodDeclaration)
531+
{
532+
identifier = methodDeclaration.Identifier.ToString();
533+
logger?.WriteLine($"Validating {identifier} as a method");
534+
parameterList = methodDeclaration.ParameterList;
535+
actionLocation = methodDeclaration.GetLocation();
536+
}
537+
else if (this.MethodDeclarationSyntax is LocalFunctionStatementSyntax localFunctionStatement)
538+
{
539+
identifier = localFunctionStatement.Identifier.ToString();
540+
logger?.WriteLine($"Validating {identifier} as a local function");
541+
parameterList = localFunctionStatement.ParameterList;
542+
actionLocation = localFunctionStatement.GetLocation();
543+
}
544+
else if(this.MethodDeclarationSyntax is LambdaExpressionSyntax lambdaExpression)
545+
{
546+
logger?.WriteLine("The action is a lambda.");
547+
actionLocation = lambdaExpression.GetLocation();
548+
549+
if (lambdaExpression is SimpleLambdaExpressionSyntax)
550+
{
551+
logger?.WriteLine("The action is a simple lambda, validations do not apply here, skipping this action.");
552+
diagnostics.Add(Diagnostic.Create(Diagnostics.YS1012ActionIsALambda, actionLocation));
553+
return diagnostics;
554+
}
555+
556+
if (lambdaExpression is ParenthesizedLambdaExpressionSyntax pls)
557+
{
558+
logger?.WriteLine("The action is a parenthesized lambda, can perform some validation.");
559+
560+
identifier = "(lambda expression)";
561+
parameterList = pls.ParameterList;
562+
563+
diagnostics.Add(Diagnostic.Create(Diagnostics.YS1012ActionIsALambda, actionLocation));
564+
}
565+
}
566+
567+
if (parameterList == null || parameterList.Parameters.Count() == 0)
568+
{
569+
logger?.WriteLine($"{identifier} has no parameters, ignoring");
570+
return diagnostics;
571+
}
572+
573+
logger?.WriteLine($"Will be checking {parameterList.Parameters.Count()} parameters");
574+
foreach (var parameter in parameterList.Parameters)
575+
{
576+
if (parameter.Type == null)
577+
{
578+
logger?.WriteLine($"{parameter.ToFullString()} has no type, ignoring validation?");
579+
continue;
580+
}
581+
582+
var model = compilation.GetSemanticModel(parameter.SyntaxTree);
583+
var typeInfo = model.GetTypeInfo(parameter.Type).Type;
584+
585+
var parameterName = model.GetDeclaredSymbol(parameter)?.Name ?? "(UNKNOWN)";
586+
logger?.WriteLine($"Validating {parameterName}");
587+
588+
if (typeInfo == null)
589+
{
590+
logger?.WriteLine($"Unable to determine typeinfo of {parameterName} ignoring validation?");
591+
continue;
592+
}
593+
594+
var symbol = model.GetDeclaredSymbol(parameter);
595+
if (symbol == null)
596+
{
597+
logger?.WriteLine($"Unable to determine the declared symbol for {parameterName}, skipping validation");
598+
continue;
599+
}
600+
601+
if (symbol.IsParams)
602+
{
603+
if (symbol.Type is IArrayTypeSymbol arrayTypeSymbol)
604+
{
605+
var subtype = arrayTypeSymbol.ElementType;
606+
logger?.WriteLine($"{parameterName} is a params array made up of {subtype}:_{subtype.Name}_:-{subtype.GetYarnTypeString()}-");
607+
if (subtype.GetYarnTypeString() == "any")
608+
{
609+
logger?.WriteLine($"{parameterName} is a parameter array of non Yarn compatible types!");
610+
diagnostics.Add(Diagnostic.Create(Diagnostics.YS1008ActionsParamsArraysMustBeOfYarnTypes, parameter.GetLocation(), parameterName, subtype.Name));
611+
}
612+
}
613+
}
614+
else
615+
{
616+
if (typeInfo.GetYarnTypeString() == "any" && typeInfo.BaseType?.Name != "Component")
617+
{
618+
// we have an invalid type
619+
logger?.WriteLine($"{parameterName} is an invalid type for use in a Yarn action");
620+
diagnostics.Add(Diagnostic.Create(Diagnostics.YS1011ActionsParameterIsAnIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name));
621+
}
622+
}
623+
624+
625+
foreach (var attribute in symbol.GetAttributes())
626+
{
627+
// this attribute is an enum parameter
628+
if (attribute.AttributeClass?.Name == "YarnEnumParameterAttribute")
629+
{
630+
if (typeInfo.GetYarnTypeString() == "any")
631+
{
632+
logger?.WriteLine($"{parameterName} is attributed as an enum but isn't a Yarn compatible type!");
633+
diagnostics.Add(Diagnostic.Create(Diagnostics.YS1009ActionsEnumAttributedParameterIsOfIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name));
634+
}
635+
}
636+
if (attribute.AttributeClass?.Name == "YarnNodeParameterAttribute")
637+
{
638+
if (typeInfo.GetYarnTypeString() != "string")
639+
{
640+
logger?.WriteLine($"{parameterName} is attributed as a node but isn't a string!");
641+
diagnostics.Add(Diagnostic.Create(Diagnostics.YS1010ActionsNodeAttributedParameterIsOfIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name));
642+
}
643+
}
644+
}
645+
}
646+
647+
return diagnostics;
648+
}
649+
650+
private IEnumerable<Diagnostic> ValidateCommand(Compilation compilation, ILogger? logger)
514651
{
515652
if (MethodSymbol == null)
516653
{
@@ -571,6 +708,11 @@ private IEnumerable<Diagnostic> ValidateCommand(Compilation compilation)
571708
throw new InvalidOperationException($"Expected decl for {this.Name} ({this.SourceFileName}) was of unexpected type {this.MethodDeclarationSyntax?.GetType().Name ?? "null"}");
572709
}
573710

711+
var paramDiags = ValidateParameters(compilation, logger);
712+
foreach (var p in paramDiags)
713+
{
714+
yield return p;
715+
}
574716

575717
var typeIsKnownValid = validCommandReturnTypes.Contains(returnTypeSymbol)
576718
|| validTaskTypes.Contains(returnTypeSymbol);

Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void Execute(GeneratorExecutionContext context)
220220
continue;
221221
}
222222

223-
var diagnostics = action.Validate(compilation);
223+
var diagnostics = action.Validate(compilation, output);
224224
foreach (var diagnostic in diagnostics)
225225
{
226226
context.ReportDiagnostic(diagnostic);

Editor/Analysis/Analyser.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public Analyser(string sourcePath)
5858
public IEnumerable<string> SourceFiles => GetSourceFiles(SourcePath);
5959
public string SourcePath { get; set; }
6060

61-
public IEnumerable<Action> GetActions(IEnumerable<string>? assemblyPaths = null, bool onlyValid = false)
61+
public IEnumerable<Action> GetActions(IEnumerable<string>? assemblyPaths = null, ILogger? logger = null)
6262
{
6363
var trees = SourceFiles
6464
.Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), path: path))
@@ -111,17 +111,17 @@ static string GetLocationOfAssemblyWithType(string typeName)
111111
{
112112
foreach (var tree in trees)
113113
{
114-
output.AddRange(GetActions(compilation, tree));
114+
output.AddRange(GetActions(compilation, tree, logger));
115115
}
116116
}
117117
catch (Exception e)
118118
{
119119
throw new AnalyserException(e.Message, e, diagnostics);
120120
}
121121

122-
if (onlyValid)
122+
foreach (var action in output)
123123
{
124-
output = output.Where(a => a.Validate(compilation).Count == 0).ToList();
124+
action.Validate(compilation, logger);
125125
}
126126

127127
return output;

Editor/Analysis/Diagnostics.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,57 @@ public static class Diagnostics
6464
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
6565

6666
public static readonly DiagnosticDescriptor YS1007ActionsMustBeInPublicTypes = new DiagnosticDescriptor(
67-
"YS1006",
67+
"YS1007",
6868
title: $"Yarn action methods must be in a public type",
6969
messageFormat: "Yarn actions must be in a publicly accessible type. {0}'s containing type, {1}, is {2}.",
7070
category: "Yarn Spinner",
7171
defaultSeverity: DiagnosticSeverity.Warning,
7272
isEnabledByDefault: true,
7373
helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
74+
75+
public static readonly DiagnosticDescriptor YS1008ActionsParamsArraysMustBeOfYarnTypes = new DiagnosticDescriptor(
76+
"YS1008",
77+
title: "Params arrays must be of a Yarn compatible type",
78+
messageFormat: "Params arrays must be of a Yarn compatible type, but {0} is of type \"{1}\"",
79+
category: "Yarn Spinner",
80+
defaultSeverity: DiagnosticSeverity.Warning,
81+
isEnabledByDefault: true,
82+
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
83+
84+
public static readonly DiagnosticDescriptor YS1009ActionsEnumAttributedParameterIsOfIncompatibleType = new DiagnosticDescriptor(
85+
"YS1009",
86+
title: "Yarn Enum attributed parameters must be of a Yarn compatible type",
87+
messageFormat: "Yarn Enum attributed parameters must be of a Yarn compatible type, but {0} is of type \"{1}\"",
88+
category: "Yarn Spinner",
89+
defaultSeverity: DiagnosticSeverity.Warning,
90+
isEnabledByDefault: true,
91+
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
92+
93+
public static readonly DiagnosticDescriptor YS1010ActionsNodeAttributedParameterIsOfIncompatibleType = new DiagnosticDescriptor(
94+
"YS1010",
95+
title: "Yarn Node attributed parameters must be a string",
96+
messageFormat: "Yarn Node attributed parameters must be a string, but {0} is of type \"{1}\"",
97+
category: "Yarn Spinner",
98+
defaultSeverity: DiagnosticSeverity.Warning,
99+
isEnabledByDefault: true,
100+
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
101+
102+
public static readonly DiagnosticDescriptor YS1011ActionsParameterIsAnIncompatibleType = new DiagnosticDescriptor(
103+
"YS1011",
104+
title: "Yarn action parameters must be of a Yarn compatible type",
105+
messageFormat: "Yarn action parameters must be of a Yarn compatible type, but {0} is of type \"{1}\"",
106+
category: "Yarn Spinner",
107+
defaultSeverity: DiagnosticSeverity.Warning,
108+
isEnabledByDefault: true,
109+
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
110+
111+
public static readonly DiagnosticDescriptor YS1012ActionIsALambda = new DiagnosticDescriptor(
112+
"YS1012",
113+
title: "Yarn actions can be lambdas but this generally isn't recommended.",
114+
messageFormat: "Yarn actions can be lambdas but this generally isn't recommended.",
115+
category: "Yarn Spinner",
116+
defaultSeverity: DiagnosticSeverity.Info,
117+
isEnabledByDefault: true,
118+
helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions");
74119

75120
}
7 KB
Binary file not shown.

0 commit comments

Comments
 (0)