diff --git a/src/Test/TestCases.Workflows/ExpressionTests.cs b/src/Test/TestCases.Workflows/ExpressionTests.cs index 5e9ff173..21dc74f5 100644 --- a/src/Test/TestCases.Workflows/ExpressionTests.cs +++ b/src/Test/TestCases.Workflows/ExpressionTests.cs @@ -8,6 +8,7 @@ using System.Activities.Validation; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace TestCases.Workflows; @@ -47,7 +48,8 @@ static ExpressionTests() // There's no programmatic way (that I know of) to add assembly references when creating workflows like in these tests. // Adding the custom assembly directly to the expression validator to simulate XAML reference. // The null is for testing purposes. - VbExpressionValidator.Instance = new VbExpressionValidator(new() { typeof(ClassWithCollectionProperties).Assembly, null }); + VbExpressionValidator.Instance.AddRequiredAssembly(typeof(ClassWithCollectionProperties).Assembly); + CSharpExpressionValidator.Instance.AddRequiredAssembly(typeof(ClassWithCollectionProperties).Assembly); } [Theory] @@ -424,6 +426,69 @@ public void CSRoslynValidator_ValidatesMoreThan16Arguments() var result = ActivityValidationServices.Validate(sequence, _useValidator); result.Errors.ShouldBeEmpty(); } + [Fact] + public void VB_Multithreaded_NoError() + { + var activities = new List(); + var tasks = new List(); + var results = new List(); + for (var i = 0; i < 20; i++) + { + var seq = new Sequence(); + seq.Variables.Add(new Variable("sum")); + for (var j = 0; j < 1000; j++) + { + seq.Activities.Add(new Assign + { + To = new OutArgument(new VisualBasicReference("sum")), + Value = new InArgument(new VisualBasicValue($"sum + {j}")) + }); + } + activities.Add(seq); + } + foreach (var activity in activities) + { + tasks.Add(Task.Run(() => + { + results.Add(ActivityValidationServices.Validate(activity, _useValidator)); + })); + } + Task.WaitAll(tasks.ToArray()); + + results.All(r => !r.Errors.Any() && !r.Warnings.Any()).ShouldBeTrue(); + } + + [Fact] + public void CS_Multithreaded_NoError() + { + var activities = new List(); + var tasks = new List(); + var results = new List(); + for (var i = 0; i < 20; i++) + { + var seq = new Sequence(); + seq.Variables.Add(new Variable("sum")); + for (var j = 0; j < 1000; j++) + { + seq.Activities.Add(new Assign + { + To = new OutArgument(new CSharpReference("sum")), + Value = new InArgument(new CSharpValue($"sum + {j}")) + }); + } + activities.Add(seq); + } + foreach (var activity in activities) + { + tasks.Add(Task.Run(() => + { + results.Add(ActivityValidationServices.Validate(activity, _useValidator)); + })); + } + Task.WaitAll(tasks.ToArray()); + + results.All(r => !r.Errors.Any() && !r.Warnings.Any()).ShouldBeTrue(); + } [Fact] public void VBReferenceTypeIsChecked() diff --git a/src/Test/TestCases.Workflows/ValidationExtensionsTests.cs b/src/Test/TestCases.Workflows/ValidationExtensionsTests.cs new file mode 100644 index 00000000..9e40f5fe --- /dev/null +++ b/src/Test/TestCases.Workflows/ValidationExtensionsTests.cs @@ -0,0 +1,76 @@ +using Microsoft.CSharp.Activities; +using Shouldly; +using System; +using System.Activities; +using System.Activities.Statements; +using System.Activities.Validation; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace TestCases.Workflows; + +public class ValidationExtensionsTests +{ + private readonly ValidationSettings _useValidator = new() { ForceExpressionCache = false }; + + [Fact] + public void OnlyOneInstanceOfExtensionTypeIsAdded() + { + var seq = new Sequence(); + for (var j = 0; j < 10000; j++) + { + seq.Activities.Add(new ActivityWithValidationExtension()); + } + + var result = ActivityValidationServices.Validate(seq, _useValidator); + result.Errors.Count.ShouldBe(1); + result.Errors.First().Message.ShouldContain(nameof(MockValidationExtension)); + } + + [Fact] + public void ValidationErrorsAreConcatenated() + { + var seq = new Sequence() + { + Activities = + { + new ActivityWithValidationExtension(), + new ActivityWithValidationError(), + new WriteLine { Text = new InArgument(new CSharpValue("var1")) } + } + }; + + var result = ActivityValidationServices.Validate(seq, _useValidator); + result.Errors.Count.ShouldBe(3); + result.Errors.ShouldContain(error => error.Message.Contains(nameof(ActivityWithValidationError))); + result.Errors.ShouldContain(error => error.Message.Contains(nameof(MockValidationExtension))); + result.Errors.ShouldContain(error => error.Message.Contains("The name 'var1' does not exist in the current context")); + } + + class ActivityWithValidationError : CodeActivity + { + protected override void Execute(CodeActivityContext context) => throw new NotImplementedException(); + + protected override void CacheMetadata(CodeActivityMetadata metadata) => metadata.AddValidationError(nameof(ActivityWithValidationError)); + } + + class ActivityWithValidationExtension : CodeActivity + { + protected override void Execute(CodeActivityContext context) => throw new NotImplementedException(); + + protected override void CacheMetadata(CodeActivityMetadata metadata) + { + if (metadata.Environment.IsValidating) + { + metadata.Environment.Extensions.GetOrAdd(() => new MockValidationExtension()); + } + } + } + + class MockValidationExtension : IValidationExtension + { + public IEnumerable PostValidate(Activity activity) => + new List() { new ValidationError(nameof(MockValidationExtension)) }; + } +} \ No newline at end of file diff --git a/src/UiPath.Workflow.Runtime/ActivityLocationReferenceEnvironment.cs b/src/UiPath.Workflow.Runtime/ActivityLocationReferenceEnvironment.cs index 4192fb4a..dde7ecd1 100644 --- a/src/UiPath.Workflow.Runtime/ActivityLocationReferenceEnvironment.cs +++ b/src/UiPath.Workflow.Runtime/ActivityLocationReferenceEnvironment.cs @@ -12,7 +12,10 @@ internal sealed class ActivityLocationReferenceEnvironment : LocationReferenceEn private Dictionary _declarations; private List _unnamedDeclarations; - public ActivityLocationReferenceEnvironment() { } + public ActivityLocationReferenceEnvironment() + { + Extensions = new(); + } public ActivityLocationReferenceEnvironment(LocationReferenceEnvironment parent) { @@ -22,6 +25,7 @@ public ActivityLocationReferenceEnvironment(LocationReferenceEnvironment parent) CompileExpressions = parent.CompileExpressions; IsValidating = parent.IsValidating; InternalRoot = parent.Root; + Extensions = parent.Extensions; } } diff --git a/src/UiPath.Workflow.Runtime/EnvironmentExtensions.cs b/src/UiPath.Workflow.Runtime/EnvironmentExtensions.cs new file mode 100644 index 00000000..a564c24c --- /dev/null +++ b/src/UiPath.Workflow.Runtime/EnvironmentExtensions.cs @@ -0,0 +1,71 @@ +// This file is part of Core WF which is licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +namespace System.Activities +{ + internal class EnvironmentExtensions + { + private readonly Dictionary _extensions = new(); + + /// + /// Gets the specified extension. + /// If the extension does not exist, + /// it will invoke the parameter + /// + /// The type of the extension + /// The factory to create the extension + /// + public TExtension GetOrAdd(Func createExtensionFactory) + where TExtension : class + { + var type = typeof(TExtension); + if (_extensions.TryGetValue(type, out object extension)) + { + return extension as TExtension; + } + + return CreateAndAdd(); + + TExtension CreateAndAdd() + { + var extension = createExtensionFactory(); + if (extension is null) + throw new ArgumentNullException(nameof(extension)); + + _extensions[type] = extension; + return extension; + } + } + + /// + /// Retrieves the extension registered for the given type + /// or null otherwise + /// + /// The type of the extension. + public T Get() where T : class + { + if (_extensions.TryGetValue(typeof(T), out object extension)) + return extension as T; + return null; + } + + /// + /// Adds the specified extension to the list. + /// The extension is treated as a singleton, + /// so if a second extension with the same type is added, it will + /// throw an + /// + /// The type of the extension + /// The extension + /// + public void Add(TExtension extension) where TExtension : class + { + if (_extensions.ContainsKey(typeof(TExtension))) + throw new InvalidOperationException($"Service '{typeof(TExtension).FullName}' already exists"); + + _extensions[typeof(TExtension)] = extension; + } + + internal IReadOnlyCollection All => _extensions.Values; + } +} \ No newline at end of file diff --git a/src/UiPath.Workflow.Runtime/LocationReferenceEnvironment.cs b/src/UiPath.Workflow.Runtime/LocationReferenceEnvironment.cs index 29ec37ea..5a5eb4ef 100644 --- a/src/UiPath.Workflow.Runtime/LocationReferenceEnvironment.cs +++ b/src/UiPath.Workflow.Runtime/LocationReferenceEnvironment.cs @@ -16,6 +16,8 @@ protected LocationReferenceEnvironment() { } /// internal bool IsValidating { get; set; } + internal EnvironmentExtensions Extensions { get; set; } + public abstract Activity Root { get; } public LocationReferenceEnvironment Parent { get; protected set; } diff --git a/src/UiPath.Workflow.Runtime/Validation/ActivityValidationServices.cs b/src/UiPath.Workflow.Runtime/Validation/ActivityValidationServices.cs index 53158ee6..b69f3fbe 100644 --- a/src/UiPath.Workflow.Runtime/Validation/ActivityValidationServices.cs +++ b/src/UiPath.Workflow.Runtime/Validation/ActivityValidationServices.cs @@ -10,8 +10,9 @@ namespace System.Activities.Validation; public static class ActivityValidationServices { - internal static readonly ReadOnlyCollection EmptyChildren = new(Array.Empty()); private static readonly ValidationSettings defaultSettings = new(); + + internal static readonly ReadOnlyCollection EmptyChildren = new(Array.Empty()); internal static ReadOnlyCollection EmptyValidationErrors = new(new List(0)); public static ValidationResults Validate(Activity toValidate) => Validate(toValidate, defaultSettings); @@ -245,7 +246,7 @@ private static string GenerateExceptionString(IList validationE return exceptionString; } - static internal string GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source) + internal static string GenerateValidationErrorPrefix(Activity toValidate, ActivityUtilities.ActivityCallStack parentChain, ProcessActivityTreeOptions options, out Activity source) { bool parentVisible = true; string prefix = ""; @@ -451,9 +452,12 @@ internal ValidationResults InternalValidate() { // We want to add the CacheMetadata errors to our errors collection ActivityUtilities.CacheRootMetadata(_rootToValidate, _environment, _options, new ActivityUtilities.ProcessActivityCallback(ValidateElement), ref _errors); - } + } - return new ValidationResults(_errors); + return new ValidationResults(GetValidationExtensionResults().Concat(_errors ?? Array.Empty()).ToList()); + + IEnumerable GetValidationExtensionResults() => + _environment.Extensions.All.OfType().SelectMany(validationExtension => validationExtension.PostValidate(_rootToValidate)); } private void ValidateElement(ActivityUtilities.ChildActivity childActivity, ActivityUtilities.ActivityCallStack parentChain) diff --git a/src/UiPath.Workflow.Runtime/Validation/IValidationExtension.cs b/src/UiPath.Workflow.Runtime/Validation/IValidationExtension.cs new file mode 100644 index 00000000..0112455a --- /dev/null +++ b/src/UiPath.Workflow.Runtime/Validation/IValidationExtension.cs @@ -0,0 +1,7 @@ +namespace System.Activities.Validation +{ + internal interface IValidationExtension + { + IEnumerable PostValidate(Activity activity); + } +} diff --git a/src/UiPath.Workflow/Activities/CsExpressionValidator.cs b/src/UiPath.Workflow/Activities/CsExpressionValidator.cs deleted file mode 100644 index 4efcd8a6..00000000 --- a/src/UiPath.Workflow/Activities/CsExpressionValidator.cs +++ /dev/null @@ -1,135 +0,0 @@ -// This file is part of Core WF which is licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Scripting; -using Microsoft.CodeAnalysis.Scripting.Hosting; -using Microsoft.CSharp.Activities; -using ReflectionMagic; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace System.Activities; - -/// -/// Validates C# expressions for use in fast design-time expression validation. -/// -public class CsExpressionValidator : RoslynExpressionValidator -{ - private static readonly Lazy s_default = new(); - private static CsExpressionValidator s_instance; - private const string _referenceValidationTemplate = "public static {0} IsLocation() => ({1}) => {2} = default({3});"; - - private static readonly CSharpParseOptions s_csScriptParseOptions = new(kind: SourceCodeKind.Script); - - private static readonly dynamic s_typeOptions = GetTypeOptions(); - private static readonly dynamic s_typeNameFormatter = GetTypeNameFormatter(); - - private static readonly HashSet s_defaultReferencedAssemblies = new() - { - typeof(Collections.ICollection).Assembly, - typeof(ICollection<>).Assembly, - typeof(Enum).Assembly, - typeof(ComponentModel.BrowsableAttribute).Assembly, - typeof(CSharpValue<>).Assembly, - }; - - private Compilation DefaultCompilationUnit; - - /// - /// Singleton instance of the default validator. - /// - public static CsExpressionValidator Instance - { - get => s_instance ?? s_default.Value; - set => s_instance = value; - } - - protected override CompilerHelper CompilerHelper { get; } = new CSharpCompilerHelper(); - - /// - /// Initializes the MetadataReference collection. - /// - public CsExpressionValidator() : this(null) { } - - /// - /// Initializes the MetadataReference collection. - /// - /// - /// Assemblies to seed the collection. - /// - public CsExpressionValidator(HashSet referencedAssemblies) - : base(referencedAssemblies != null - ? new HashSet(s_defaultReferencedAssemblies.Union(referencedAssemblies)) - : s_defaultReferencedAssemblies) - { } - - protected override void UpdateCompilationUnit(ExpressionContainer expressionContainer) - { - var metadataReferences = GetMetadataReferencesForExpression(expressionContainer); - - if (DefaultCompilationUnit == null) - { - var assemblyName = Guid.NewGuid().ToString(); - CSharpCompilationOptions options = new( - OutputKind.DynamicallyLinkedLibrary, - mainTypeName: null, - usings: expressionContainer.ExpressionToValidate.ImportedNamespaces, - optimizationLevel: OptimizationLevel.Debug, - checkOverflow: false, - xmlReferenceResolver: null, - sourceReferenceResolver: SourceFileResolver.Default, - concurrentBuild: !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")), - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default); - expressionContainer.CompilationUnit = DefaultCompilationUnit = - CSharpCompilation.Create(assemblyName, null, metadataReferences, options); - } - else - { - var options = DefaultCompilationUnit.Options as CSharpCompilationOptions; - expressionContainer.CompilationUnit = DefaultCompilationUnit - .WithOptions(options.WithUsings(expressionContainer.ExpressionToValidate.ImportedNamespaces)) - .WithReferences(metadataReferences); - } - } - - protected override string CreateValueCode(string types, string names, string code) - => CompilerHelper.CreateExpressionCode(types, names, code); - - protected override string CreateReferenceCode(string type, string names, string code, string returnType) - { - var actionDefinition = !string.IsNullOrWhiteSpace(type) - ? $"Action<{string.Join(Comma, type)}>" - : "Action"; - return string.Format(_referenceValidationTemplate, actionDefinition, names, code, returnType); - } - - protected override SyntaxTree GetSyntaxTreeForExpression(ExpressionContainer expressionContainer) => - CSharpSyntaxTree.ParseText(expressionContainer.ExpressionToValidate.Code, s_csScriptParseOptions); - - protected override string GetTypeName(Type type) => - (string)s_typeNameFormatter.FormatTypeName(type, s_typeOptions); - - private static object GetTypeOptions() - { - var formatterOptionsType = - typeof(ObjectFormatter).Assembly.GetType( - "Microsoft.CodeAnalysis.Scripting.Hosting.CommonTypeNameFormatterOptions"); - const int arrayBoundRadix = 0; - const bool showNamespaces = true; - return Activator.CreateInstance(formatterOptionsType, arrayBoundRadix, showNamespaces); - } - - private static object GetTypeNameFormatter() - { - return typeof(CSharpScript) - .Assembly - .GetType("Microsoft.CodeAnalysis.CSharp.Scripting.Hosting.CSharpObjectFormatter") - .AsDynamicType() - .s_impl - .TypeNameFormatter; - } -} diff --git a/src/UiPath.Workflow/Activities/Utils/CSharpCompilerHelper.cs b/src/UiPath.Workflow/Activities/Utils/CSharpCompilerHelper.cs index 3e653de7..3bd4071d 100644 --- a/src/UiPath.Workflow/Activities/Utils/CSharpCompilerHelper.cs +++ b/src/UiPath.Workflow/Activities/Utils/CSharpCompilerHelper.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis.CSharp; -using System; using System.Text; using System.Threading; @@ -7,7 +6,7 @@ namespace System.Activities { public sealed class CSharpCompilerHelper : CompilerHelper { - static int crt = 0; + private static int crt = 0; public override int IdentifierKind => (int)SyntaxKind.IdentifierName; @@ -19,12 +18,11 @@ public override string CreateExpressionCode(string types, string names, string c if (arrayType.Length <= 16) // .net defines Func...Funct> CreateExpression() => ({names}) => {code};"; - var (myDelegate, name) = DefineDelegate(types); return $"{myDelegate} \n public static Expression<{name}<{types}>> CreateExpression() => ({names}) => {code};"; } - private static (string, string) DefineDelegate(string types) + public override (string, string) DefineDelegate(string types) { var crtValue = Interlocked.Add(ref crt, 1); var arrayType = types.Split(","); diff --git a/src/UiPath.Workflow/Activities/Utils/CompilerHelper.cs b/src/UiPath.Workflow/Activities/Utils/CompilerHelper.cs index febbafaf..7a537f11 100644 --- a/src/UiPath.Workflow/Activities/Utils/CompilerHelper.cs +++ b/src/UiPath.Workflow/Activities/Utils/CompilerHelper.cs @@ -9,5 +9,7 @@ public abstract class CompilerHelper public abstract StringComparer IdentifierNameComparer { get; } public abstract int IdentifierKind { get; } + + public abstract (string, string) DefineDelegate(string types); } } diff --git a/src/UiPath.Workflow/Activities/Utils/VBCompilerHelper.cs b/src/UiPath.Workflow/Activities/Utils/VBCompilerHelper.cs index 4f7b451e..bfb172da 100644 --- a/src/UiPath.Workflow/Activities/Utils/VBCompilerHelper.cs +++ b/src/UiPath.Workflow/Activities/Utils/VBCompilerHelper.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis.VisualBasic; -using System; using System.Text; using System.Threading; @@ -7,7 +6,7 @@ namespace System.Activities { public sealed class VBCompilerHelper : CompilerHelper { - static int crt = 0; + private static int crt = 0; public override int IdentifierKind => (int)SyntaxKind.IdentifierName; @@ -23,7 +22,7 @@ public override string CreateExpressionCode(string types, string names, string c return $"{myDelegate} \n Public Shared Function CreateExpression() As Expression(Of {name}(Of {types}))\nReturn Function({names}) ({code})\nEnd Function"; } - private static (string, string) DefineDelegate(string types) + public override (string, string) DefineDelegate(string types) { var crtValue = Interlocked.Add(ref crt, 1); diff --git a/src/UiPath.Workflow/Activities/VbExpressionValidator.cs b/src/UiPath.Workflow/Activities/VbExpressionValidator.cs deleted file mode 100644 index a145f257..00000000 --- a/src/UiPath.Workflow/Activities/VbExpressionValidator.cs +++ /dev/null @@ -1,115 +0,0 @@ -// This file is part of Core WF which is licensed under the MIT license. -// See LICENSE file in the project root for full license information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.VisualBasic; -using Microsoft.CodeAnalysis.VisualBasic.Scripting.Hosting; -using Microsoft.VisualBasic.Activities; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; - -namespace System.Activities; - -/// -/// Validates VB.NET expressions for use in fast design-time expression validation. -/// -public class VbExpressionValidator : RoslynExpressionValidator -{ - private static readonly Lazy s_default = new(); - private static VbExpressionValidator s_instance; - - private const string _referenceValidationTemplate = "Public Shared Function IsLocation() As {0}\nReturn Function({1}) as Action \nReturn Sub() {2} = CType(Nothing, {3})\nEnd Function\nEnd Function"; - - private static readonly VisualBasicParseOptions s_vbScriptParseOptions = - new(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest); - - private static readonly HashSet s_defaultReferencedAssemblies = new() - { - typeof(Collections.ICollection).Assembly, - typeof(Enum).Assembly, - typeof(ComponentModel.BrowsableAttribute).Assembly, - typeof(VisualBasicValue<>).Assembly, - }; - - private Compilation DefaultCompilationUnit; - - /// - /// Singleton instance of the default validator. - /// - public static VbExpressionValidator Instance - { - get => s_instance ?? s_default.Value; - set => s_instance = value; - } - - protected override CompilerHelper CompilerHelper { get; } = new VBCompilerHelper(); - - /// - /// Initializes the MetadataReference collection. - /// - public VbExpressionValidator() : this(null) { } - - /// - /// Initializes the MetadataReference collection. - /// - /// - /// Assemblies to seed the collection. - /// - public VbExpressionValidator(HashSet referencedAssemblies) - : base(referencedAssemblies != null - ? new HashSet(s_defaultReferencedAssemblies.Union(referencedAssemblies)) - : s_defaultReferencedAssemblies) - { } - - protected override void UpdateCompilationUnit(ExpressionContainer expressionContainer) - { - var globalImports = GlobalImport.Parse(expressionContainer.ExpressionToValidate.ImportedNamespaces); - var metadataReferences = GetMetadataReferencesForExpression(expressionContainer); - - if (DefaultCompilationUnit == null) - { - var assemblyName = Guid.NewGuid().ToString(); - VisualBasicCompilationOptions options = new( - OutputKind.DynamicallyLinkedLibrary, - mainTypeName: null, - globalImports: globalImports, - rootNamespace: "", - optionStrict: OptionStrict.On, - optionInfer: true, - optionExplicit: true, - optionCompareText: false, - embedVbCoreRuntime: false, - optimizationLevel: OptimizationLevel.Debug, - checkOverflow: true, - xmlReferenceResolver: null, - sourceReferenceResolver: SourceFileResolver.Default, - concurrentBuild: !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))); - expressionContainer.CompilationUnit = DefaultCompilationUnit = - VisualBasicCompilation.Create(assemblyName, null, metadataReferences, options); - } - else - { - var options = DefaultCompilationUnit.Options as VisualBasicCompilationOptions; - var compilation = DefaultCompilationUnit.WithOptions(options!.WithGlobalImports(globalImports)); - expressionContainer.CompilationUnit = compilation.WithReferences(metadataReferences); - } - } - - protected override string CreateValueCode(string types, string names, string code) - => CompilerHelper.CreateExpressionCode(types, names, code); - - protected override string CreateReferenceCode(string types, string names, string code, string returnType) - { - var actionDefinition = !string.IsNullOrWhiteSpace(types) - ? $"Action(Of {string.Join(Comma, types)})" - : "Action"; - return string.Format(_referenceValidationTemplate, actionDefinition, names, code, returnType); - } - - protected override SyntaxTree GetSyntaxTreeForExpression(ExpressionContainer expressionContainer) => - VisualBasicSyntaxTree.ParseText("? " + expressionContainer.ExpressionToValidate.Code, s_vbScriptParseOptions); - - protected override string GetTypeName(Type type) => VisualBasicObjectFormatter.FormatTypeName(type); -} diff --git a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs index 14ce35df..cea28473 100644 --- a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs +++ b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpDesignerHelper.cs @@ -17,6 +17,8 @@ public CSharpHelper(string expressionText, HashSet refAssemNames, protected override JustInTimeCompiler CreateCompiler(HashSet references) => new CSharpJitCompiler(references); + + internal const string Language = "C#"; } internal class CSharpExpressionFactory : ExpressionFactory diff --git a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs index 611b25b9..121afdad 100644 --- a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs +++ b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpReference.cs @@ -5,6 +5,7 @@ using System.Activities; using System.Activities.Expressions; using System.Activities.Internals; +using System.Activities.Validation; using System.ComponentModel; using System.Diagnostics; using System.Linq.Expressions; @@ -14,7 +15,7 @@ namespace Microsoft.CSharp.Activities; [DebuggerStepThrough] [ContentProperty("ExpressionText")] -public class CSharpReference : CodeActivity>, ITextExpression +public class CSharpReference : TextExpressionBase>, ITextExpression { private CompiledExpressionInvoker _invoker; @@ -22,17 +23,17 @@ public class CSharpReference : CodeActivity>, ITextEx public CSharpReference(string expressionText) : this() => ExpressionText = expressionText; - public string ExpressionText { get; set; } + public override string ExpressionText { get; set; } [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public string Language => "C#"; + public override string Language => CSharpHelper.Language; - public Expression GetExpressionTree() => IsMetadataCached ? _invoker.GetExpressionTree() : throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); + public override Expression GetExpressionTree() => IsMetadataCached ? _invoker.GetExpressionTree() : throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); protected override void CacheMetadata(CodeActivityMetadata metadata) { _invoker = new CompiledExpressionInvoker(this, true, metadata); - CsExpressionValidator.Instance.TryValidate(this, metadata, ExpressionText, true); + QueueForValidation(metadata, true); } protected override Location Execute(CodeActivityContext context) => (Location)_invoker.InvokeExpression(context); diff --git a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs index c849674c..044b4b6d 100644 --- a/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs +++ b/src/UiPath.Workflow/Microsoft/CSharp/Activities/CSharpValue.cs @@ -5,6 +5,7 @@ using System.Activities; using System.Activities.Expressions; using System.Activities.Internals; +using System.Activities.Validation; using System.ComponentModel; using System.Diagnostics; using System.Linq.Expressions; @@ -14,7 +15,7 @@ namespace Microsoft.CSharp.Activities; [DebuggerStepThrough] [ContentProperty("ExpressionText")] -public class CSharpValue : CodeActivity, ITextExpression +public class CSharpValue : TextExpressionBase { private CompiledExpressionInvoker _invoker; @@ -22,17 +23,17 @@ public class CSharpValue : CodeActivity, ITextExpression public CSharpValue(string expressionText) : this() => ExpressionText = expressionText; - public string ExpressionText { get; set; } + public override string ExpressionText { get; set; } [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public string Language => "C#"; + public override string Language => CSharpHelper.Language; - public Expression GetExpressionTree() => IsMetadataCached ? _invoker.GetExpressionTree() : throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); + public override Expression GetExpressionTree() => IsMetadataCached ? _invoker.GetExpressionTree() : throw FxTrace.Exception.AsError(new InvalidOperationException(SR.ActivityIsUncached)); protected override void CacheMetadata(CodeActivityMetadata metadata) { _invoker = new CompiledExpressionInvoker(this, false, metadata); - CsExpressionValidator.Instance.TryValidate(this, metadata, ExpressionText); + QueueForValidation(metadata, false); } protected override TResult Execute(CodeActivityContext context) => (TResult) _invoker.InvokeExpression(context); diff --git a/src/UiPath.Workflow/Microsoft/TextExpressionBase.cs b/src/UiPath.Workflow/Microsoft/TextExpressionBase.cs new file mode 100644 index 00000000..6014018e --- /dev/null +++ b/src/UiPath.Workflow/Microsoft/TextExpressionBase.cs @@ -0,0 +1,41 @@ +using System.Activities.Expressions; +using System.Activities.Validation; +using System.Linq.Expressions; + +namespace System.Activities +{ + public abstract class TextExpressionBase : CodeActivity, ITextExpression + { + private static readonly Func _validationFunc = () => new(); + + public abstract string ExpressionText { get; set; } + + public abstract string Language { get; } + + public abstract Expression GetExpressionTree(); + + protected bool QueueForValidation(CodeActivityMetadata metadata, bool isLocation) + { + if (metadata.Environment.CompileExpressions) + { + return true; + } + + if (metadata.Environment.IsValidating) + { + var extension = metadata.Environment.Extensions.GetOrAdd(_validationFunc); + extension.QueueExpressionForValidation(new() + { + Activity = this, + ExpressionText = ExpressionText, + IsLocation = isLocation, + ResultType = typeof(T), + Environment = metadata.Environment + }, Language); + + return true; + } + return false; + } + } +} diff --git a/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicDesignerHelper.cs b/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicDesignerHelper.cs index fcb9d5c7..ae5f71ed 100644 --- a/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicDesignerHelper.cs +++ b/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicDesignerHelper.cs @@ -21,7 +21,7 @@ public VisualBasicHelper(string expressionText, HashSet refAssemNa private VisualBasicHelper(string expressionText) : base(expressionText) { } - internal static string Language => "VB"; + internal const string Language = "VB"; protected override JustInTimeCompiler CreateCompiler(HashSet references) { diff --git a/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicReference.cs b/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicReference.cs index ec286c20..099fde48 100644 --- a/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicReference.cs +++ b/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicReference.cs @@ -7,6 +7,7 @@ using System.Activities.Expressions; using System.Activities.Internals; using System.Activities.Runtime; +using System.Activities.Validation; using System.Activities.XamlIntegration; using System.ComponentModel; using System.Diagnostics; @@ -17,8 +18,8 @@ namespace Microsoft.VisualBasic.Activities; [DebuggerStepThrough] -public sealed class VisualBasicReference : CodeActivity>, IValueSerializableExpression, - IExpressionContainer, ITextExpression +public sealed class VisualBasicReference : TextExpressionBase>, IValueSerializableExpression, + IExpressionContainer { private Expression> _expressionTree; private CompiledExpressionInvoker _invoker; @@ -28,12 +29,12 @@ public sealed class VisualBasicReference : CodeActivity ExpressionText = expressionText; - public string ExpressionText { get; set; } + public override string ExpressionText { get; set; } [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public string Language => VisualBasicHelper.Language; + public override string Language => VisualBasicHelper.Language; - public Expression GetExpressionTree() + public override Expression GetExpressionTree() { if (IsMetadataCached) { @@ -84,7 +85,8 @@ protected override void CacheMetadata(CodeActivityMetadata metadata) { _expressionTree = null; _invoker = new CompiledExpressionInvoker(this, true, metadata); - if (VbExpressionValidator.Instance.TryValidate(this, metadata, ExpressionText, true)) + + if (QueueForValidation(metadata, true)) { return; } diff --git a/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicValue.cs b/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicValue.cs index fabed07c..05d9da5a 100644 --- a/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicValue.cs +++ b/src/UiPath.Workflow/Microsoft/VisualBasic/Activities/VisualBasicValue.cs @@ -7,6 +7,7 @@ using System.Activities.Expressions; using System.Activities.Internals; using System.Activities.Runtime; +using System.Activities.Validation; using System.Activities.XamlIntegration; using System.ComponentModel; using System.Diagnostics; @@ -17,8 +18,8 @@ namespace Microsoft.VisualBasic.Activities; [DebuggerStepThrough] -public sealed class VisualBasicValue : CodeActivity, IValueSerializableExpression, - IExpressionContainer, ITextExpression +public sealed class VisualBasicValue : TextExpressionBase, IValueSerializableExpression, + IExpressionContainer { private Func _compiledExpression; private Expression> _expressionTree; @@ -28,12 +29,12 @@ public sealed class VisualBasicValue : CodeActivity, IValueSer public VisualBasicValue(string expressionText) : this() => ExpressionText = expressionText; - public string ExpressionText { get; set; } + public override string ExpressionText { get; set; } [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public string Language => VisualBasicHelper.Language; + public override string Language => VisualBasicHelper.Language; - public Expression GetExpressionTree() + public override Expression GetExpressionTree() { if (!IsMetadataCached) { @@ -75,7 +76,7 @@ protected override TResult Execute(CodeActivityContext context) { if (_expressionTree == null) { - return (TResult) _invoker.InvokeExpression(context); + return (TResult)_invoker.InvokeExpression(context); } _compiledExpression ??= _expressionTree.Compile(); return _compiledExpression(context); @@ -85,7 +86,8 @@ protected override void CacheMetadata(CodeActivityMetadata metadata) { _expressionTree = null; _invoker = new CompiledExpressionInvoker(this, false, metadata); - if (VbExpressionValidator.Instance.TryValidate(this, metadata, ExpressionText)) + + if (QueueForValidation(metadata, false)) { return; } diff --git a/src/UiPath.Workflow/Validation/CSharpExpressionValidator.cs b/src/UiPath.Workflow/Validation/CSharpExpressionValidator.cs new file mode 100644 index 00000000..44afccab --- /dev/null +++ b/src/UiPath.Workflow/Validation/CSharpExpressionValidator.cs @@ -0,0 +1,139 @@ +// This file is part of Core WF which is licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting.Hosting; +using Microsoft.CSharp.Activities; +using ReflectionMagic; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace System.Activities.Validation; + +/// +/// Validates C# expressions for use in fast design-time expression validation. +/// +public sealed class CSharpExpressionValidator : RoslynExpressionValidator +{ + private static readonly Lazy s_instance = new(() => new(s_defaultReferencedAssemblies)); + private const string _valueValidationTemplate = "public static Expression> CreateExpression{1}() => ({2}) => {3};//activityId:{4}"; + private const string _delegateValueValidationTemplate = "{0}\npublic static Expression<{1}<{2}>> CreateExpression{3}() => ({4}) => {5};//activityId:{6}"; + private const string _referenceValidationTemplate = "public static {0} IsLocation{1}() => ({2}) => {3} = default({4});//activityId:{5}"; + + private static readonly CompilerHelper s_compilerHelper = new CSharpCompilerHelper(); + private static readonly CSharpParseOptions s_csScriptParseOptions = new(kind: SourceCodeKind.Script); + + private static readonly dynamic s_typeOptions = GetTypeOptions(); + private static readonly dynamic s_typeNameFormatter = GetTypeNameFormatter(); + + private static readonly HashSet s_defaultReferencedAssemblies = new() + { + typeof(Collections.ICollection).Assembly, + typeof(ICollection<>).Assembly, + typeof(Enum).Assembly, + typeof(ComponentModel.BrowsableAttribute).Assembly, + typeof(CSharpValue<>).Assembly, + Assembly.Load("netstandard"), + Assembly.Load("System.Runtime") + }; + + private readonly Compilation DefaultCompilationUnit = InitDefaultCompilationUnit(); + + public override string Language => CSharpHelper.Language; + + /// + /// Singleton instance of the default validator. + /// + public static CSharpExpressionValidator Instance => s_instance.Value; + + protected override CompilerHelper CompilerHelper { get; } = new CSharpCompilerHelper(); + + protected override string ActivityIdentifierRegex { get; } = @"(\/\/activityId):(.*)"; + + /// + /// Initializes the MetadataReference collection. + /// + /// + /// Assemblies to seed the collection. + /// + private CSharpExpressionValidator(HashSet referencedAssemblies) + : base(referencedAssemblies != null + ? new HashSet(s_defaultReferencedAssemblies.Union(referencedAssemblies)) + : s_defaultReferencedAssemblies) + { } + + protected override Compilation GetCompilation(IReadOnlyCollection assemblies, IReadOnlyCollection namespaces) + { + var metadataReferences = GetMetadataReferencesForExpression(assemblies); + + var options = DefaultCompilationUnit.Options as CSharpCompilationOptions; + return DefaultCompilationUnit.WithOptions(options.WithUsings(namespaces)).WithReferences(metadataReferences); + } + + private static Compilation InitDefaultCompilationUnit() + { + var assemblyName = Guid.NewGuid().ToString(); + CSharpCompilationOptions options = new( + OutputKind.DynamicallyLinkedLibrary, + mainTypeName: null, + usings: null, + optimizationLevel: OptimizationLevel.Debug, + checkOverflow: false, + xmlReferenceResolver: null, + sourceReferenceResolver: SourceFileResolver.Default, + concurrentBuild: !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")), + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default); + return CSharpCompilation.Create(assemblyName, null, null, options); + } + + protected override string CreateValueCode(string types, string names, string code, string activityId, int index) + { + var arrayType = types.Split(","); + if (arrayType.Length <= 16) // .net defines Func...Funct" + : "Action"; + return string.Format(_referenceValidationTemplate, actionDefinition, index, names, code, returnType, activityId); + } + + protected override SyntaxTree GetSyntaxTreeForExpression(string expressionText) => + CSharpSyntaxTree.ParseText(expressionText, s_csScriptParseOptions); + + protected override SyntaxTree GetSyntaxTreeForValidation(string expressionText) => + GetSyntaxTreeForExpression(expressionText); + + protected override string GetTypeName(Type type) => + (string)s_typeNameFormatter.FormatTypeName(type, s_typeOptions); + + private static object GetTypeOptions() + { + var formatterOptionsType = + typeof(ObjectFormatter).Assembly.GetType( + "Microsoft.CodeAnalysis.Scripting.Hosting.CommonTypeNameFormatterOptions"); + const int arrayBoundRadix = 0; + const bool showNamespaces = true; + return Activator.CreateInstance(formatterOptionsType, arrayBoundRadix, showNamespaces); + } + + private static object GetTypeNameFormatter() + { + return typeof(CSharpScript) + .Assembly + .GetType("Microsoft.CodeAnalysis.CSharp.Scripting.Hosting.CSharpObjectFormatter") + .AsDynamicType() + .s_impl + .TypeNameFormatter; + } +} \ No newline at end of file diff --git a/src/UiPath.Workflow/Validation/ExpressionToValidate.cs b/src/UiPath.Workflow/Validation/ExpressionToValidate.cs new file mode 100644 index 00000000..a1c8a572 --- /dev/null +++ b/src/UiPath.Workflow/Validation/ExpressionToValidate.cs @@ -0,0 +1,15 @@ + +namespace System.Activities.Validation; + +internal sealed class ExpressionToValidate +{ + public Activity Activity { get; init; } + + public string ExpressionText { get; init; } + + public LocationReferenceEnvironment Environment { get; init; } + + public Type ResultType { get; init; } + + public bool IsLocation { get; init; } +} diff --git a/src/UiPath.Workflow/Activities/RoslynExpressionValidator.cs b/src/UiPath.Workflow/Validation/RoslynExpressionValidator.cs similarity index 59% rename from src/UiPath.Workflow/Activities/RoslynExpressionValidator.cs rename to src/UiPath.Workflow/Validation/RoslynExpressionValidator.cs index 6496a6ce..3ca65fd5 100644 --- a/src/UiPath.Workflow/Activities/RoslynExpressionValidator.cs +++ b/src/UiPath.Workflow/Validation/RoslynExpressionValidator.cs @@ -2,18 +2,18 @@ // See LICENSE file in the project root for full license information. using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; using System.Activities.Expressions; -using System.Activities.Validation; -using System.Activities.XamlIntegration; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; +using static System.Activities.JitCompilerHelper; -namespace System.Activities; +namespace System.Activities.Validation; /// /// A base class for validating text expressions using the Microsoft.CodeAnalysis (Roslyn) package. @@ -27,12 +27,15 @@ public abstract class RoslynExpressionValidator }; private const string ErrorRegex = "((\\(.*\\)).*error )(.*)"; + protected abstract string ActivityIdentifierRegex { get; } private readonly Lazy> _metadataReferences; private readonly object _lockRequiredAssemblies = new(); protected const string Comma = ", "; protected abstract CompilerHelper CompilerHelper { get; } + + public abstract string Language { get; } /// /// Initializes the MetadataReference collection. /// @@ -59,7 +62,6 @@ protected RoslynExpressionValidator(HashSet seedAssemblies = null) /// protected IReadOnlySet RequiredAssemblies { get; private set; } - /// /// Adds an assembly to the set. /// /// assembly @@ -84,39 +86,13 @@ public void AddRequiredAssembly(Assembly assembly) } } - /// - /// Validates the activity if the environment.IsValidating is set to true - /// - /// - /// - /// - /// - /// - internal bool TryValidate(Activity activity, CodeActivityMetadata metadata, string expressionText, bool isLocation = false) - { - var environment = metadata.Environment; - if (environment.CompileExpressions) - { - return true; - } - if (!environment.IsValidating) - { - return false; - } - foreach (var validationError in Validate(activity, environment, expressionText, isLocation)) - { - activity.AddTempValidationError(validationError); - } - return true; - } - /// /// Gets the MetadataReference objects for all of the referenced assemblies that expression requires. /// - /// expression container + /// The list of assemblies /// MetadataReference objects for all required assemblies - protected IEnumerable GetMetadataReferencesForExpression(ExpressionContainer expressionContainer) => - expressionContainer.RequiredAssemblies.Select(asm => TryGetMetadataReference(asm)).Where(mr => mr is not null); + protected IEnumerable GetMetadataReferencesForExpression(IReadOnlyCollection assemblies) => + assemblies.Select(asm => TryGetMetadataReference(asm)).Where(mr => mr is not null); /// /// Gets the type name, which can be language-specific. @@ -131,31 +107,34 @@ protected IEnumerable GetMetadataReferencesForExpression(Expr /// list of parameter types in comma-separated string /// list of parameter names in comma-separated string /// expression code - /// determines if the expression is a location / reference + /// The index of the current expression /// expression wrapped in a method or function that returns a LambdaExpression - protected string CreateValidationCode(IEnumerable types, string returnType, string names, string code, bool isLocation) + protected string CreateValidationCode(IEnumerable types, string returnType, string names, string code, bool isLocation, string activityId, int index) { return isLocation - ? CreateReferenceCode(string.Join(Comma, types), names, code, returnType) - : CreateValueCode(string.Join(Comma, types.Concat(new[] { returnType })), names, code); + ? CreateReferenceCode(string.Join(Comma, types), names, code, activityId, returnType, index) + : CreateValueCode(string.Join(Comma, types.Concat(new[] { returnType })), names, code, activityId, index); } - protected abstract string CreateValueCode(string types, string names, string code); + protected abstract string CreateValueCode(string types, string names, string code, string activityId, int index); - protected abstract string CreateReferenceCode(string types, string names, string code, string returnType); + protected abstract string CreateReferenceCode(string types, string names, string code, string activityId, string returnType, int index); /// /// Updates the object for the expression. /// - /// expression container - protected abstract void UpdateCompilationUnit(ExpressionContainer expressionContainer); + /// The list of assemblies + /// The list of namespaces + protected abstract Compilation GetCompilation(IReadOnlyCollection assemblies, IReadOnlyCollection namespaces); /// /// Gets the for the expression. /// - /// contains the text expression + /// The expression text /// a syntax tree to use in the - protected abstract SyntaxTree GetSyntaxTreeForExpression(ExpressionContainer expressionContainer); + protected abstract SyntaxTree GetSyntaxTreeForExpression(string expressionText); + + protected abstract SyntaxTree GetSyntaxTreeForValidation(string expressionText); /// /// Convert diagnostic messages from the compilation into ValidationError objects that can be added to the activity's @@ -163,82 +142,73 @@ protected string CreateValidationCode(IEnumerable types, string returnTy /// /// expression container /// ValidationError objects that will be added to current activity's metadata - protected virtual IEnumerable ProcessDiagnostics(ExpressionContainer expressionContainer) + private IList ProcessDiagnostics(ImmutableArray diagnostics, string text, ValidationScope validationScope) { var errors = new List(); - foreach (var diagnostic in expressionContainer.Diagnostics) + if (diagnostics.Any()) { - var match = Regex.Match(diagnostic.Message, ErrorRegex); - if (match.Success) + foreach (var diagnostic in diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) { - errors.Add(new ValidationError(match.Groups[3].Value, diagnostic.IsWarning)); - } - else - { - errors.Add(new ValidationError(diagnostic.Message, diagnostic.IsWarning)); + var match = Regex.Match(diagnostic.ToString(), ErrorRegex); + ValidationError error; + if (match.Success) + { + var activity = GetErrorActivity(text.Split('\n'), diagnostic, validationScope); + error = new ValidationError(match.Groups[3].Value, false, activity); + } + else + { + error = new ValidationError(diagnostic.ToString(), false); + } + errors.Add(error); } } return errors; } - /// - /// Validates an expression and returns any validation errors. - /// - /// Expression return type - /// activity containing the expression - /// location reference environment - /// expression text - /// validation errors - /// - /// Handles common steps for validating expressions with Roslyn. Can be reused for multiple expressions in the same - /// language. - /// - public virtual IEnumerable Validate(Activity currentActivity, LocationReferenceEnvironment environment, - string expressionText, bool isLocation) + private Activity GetErrorActivity(string[] textLines, Diagnostic diagnostic, ValidationScope validationScope) { - var requiredAssemblies = new HashSet(RequiredAssemblies); - var resultType = typeof(TResult); - var expressionContainer = new ExpressionContainer() + var diagnosticLineNumber = diagnostic.Location.GetMappedLineSpan().StartLinePosition.Line; + var lineText = textLines[diagnosticLineNumber]; + var lineMatch = Regex.Match(lineText, ActivityIdentifierRegex); + if (lineMatch.Success) { - ResultType = resultType, - CurrentActivity = currentActivity, - Environment = environment, - IsLocation = isLocation - }; + var activityId = lineMatch.Groups[2].Value.TrimEnd('\r'); + return validationScope.GetExpression(activityId).Activity; + } + return null; + } - JitCompilerHelper.GetAllImportReferences(currentActivity, true, out var localNamespaces, out var localAssemblies); - requiredAssemblies.UnionWith(localAssemblies.Where(aref => aref is not null).Select(aref => aref.Assembly ?? LoadAssemblyFromReference(aref))); - expressionContainer.RequiredAssemblies = requiredAssemblies; + internal IList Validate(Activity currentActivity, ValidationScope validationScope) + { + if (validationScope is null) + { + return Array.Empty(); + } + + var requiredAssemblies = new HashSet(RequiredAssemblies); + GetAllImportReferences(currentActivity, true, out var localNamespaces, out var localAssemblies); + requiredAssemblies.UnionWith(localAssemblies.Where(aref => aref is not null).Select(aref => aref.Assembly ?? LoadAssemblyFromReference(aref))); localNamespaces.AddRange(_defaultNamespaces); - var scriptAndTypeScope = new JitCompilerHelper.ScriptAndTypeScope(environment); - expressionContainer.ExpressionToValidate = - new ExpressionToCompile(expressionText, localNamespaces, scriptAndTypeScope.FindVariable, resultType); - - EnsureAssembliesLoaded(expressionContainer); - UpdateCompilationUnit(expressionContainer); - EnsureReturnTypeReferenced(expressionContainer); - - var syntaxTree = GetSyntaxTreeForExpression(expressionContainer); - expressionContainer.CompilationUnit = expressionContainer.CompilationUnit.AddSyntaxTrees(syntaxTree); - PrepValidation(expressionContainer); - - ModifyPreppedCompilationUnit(expressionContainer); - var diagnostics = expressionContainer - .CompilationUnit - .GetDiagnostics() - .Where(d => d.Severity == DiagnosticSeverity.Error) - .Select(diagnostic => - new TextExpressionCompilerError - { - SourceLineNumber = diagnostic.Location.GetMappedLineSpan().StartLinePosition.Line, - Number = diagnostic.Id, - Message = diagnostic.ToString(), - IsWarning = diagnostic.Severity < DiagnosticSeverity.Error - }); - expressionContainer.Diagnostics = diagnostics; - return ProcessDiagnostics(expressionContainer); + EnsureAssembliesLoaded(requiredAssemblies); + var compilation = GetCompilation(requiredAssemblies, localNamespaces); + var expressionsTextBuilder = new StringBuilder(); + int index = 0; + foreach (var expressionToValidate in validationScope.GetAllExpressions()) + { + EnsureReturnTypeReferenced(expressionToValidate.ResultType, ref compilation); + PrepValidation(expressionToValidate, expressionsTextBuilder, index++); + } + + compilation = compilation.AddSyntaxTrees(GetSyntaxTreeForValidation(expressionsTextBuilder.ToString())); + + var diagnostics = compilation + .GetDiagnostics(); + var errors = ProcessDiagnostics(diagnostics, expressionsTextBuilder.ToString(), validationScope); + validationScope.Clear(); + return errors; } /// @@ -281,19 +251,6 @@ protected virtual MetadataReference GetMetadataReferenceForAssembly(Assembly ass return null; } - /// - /// After all compilation options and syntax trees have been prepared, this method can be - /// overridden to make modifications before diagnostics are retrieved. - /// - /// expression container - /// - /// Compilation object should have all imports, references, and compilation options set - /// and should have the first syntax tree set to the method with the expression. Use the - /// property to get or set the - /// Compilation object. - /// - protected virtual void ModifyPreppedCompilationUnit(ExpressionContainer expressionContainer) { } - /// /// If is null, loads the assembly. Default is to /// call . @@ -306,31 +263,28 @@ protected virtual Assembly LoadAssemblyFromReference(AssemblyReference assemblyR return assemblyReference.Assembly; } - private void PrepValidation(ExpressionContainer expressionContainer) + private void PrepValidation(ExpressionToValidate expressionToValidate, StringBuilder expressionBuilder, int index) { - var syntaxTree = expressionContainer.CompilationUnit.SyntaxTrees.First(); + var syntaxTree = GetSyntaxTreeForExpression(expressionToValidate.ExpressionText); var identifiers = syntaxTree.GetRoot().DescendantNodesAndSelf().Where(n => n.RawKind == CompilerHelper.IdentifierKind) .Select(n => n.ToString()).Distinct(CompilerHelper.IdentifierNameComparer); var resolvedIdentifiers = identifiers - .Select(name => (Name: name, Type: expressionContainer.ExpressionToValidate.VariableTypeGetter(name))) + .Select(name => (Name: name, Type: new ScriptAndTypeScope(expressionToValidate.Environment).FindVariable(name))) .Where(var => var.Type != null) .ToArray(); var names = string.Join(Comma, resolvedIdentifiers.Select(var => var.Name)); var types = resolvedIdentifiers.Select(var => var.Type).Select(GetTypeName); - var returnType = GetTypeName(expressionContainer.ResultType); - var lambdaFuncCode = CreateValidationCode(types, returnType, names, expressionContainer.ExpressionToValidate.Code, expressionContainer.IsLocation); - - var sourceText = SourceText.From(lambdaFuncCode); - var newSyntaxTree = syntaxTree.WithChangedText(sourceText); - expressionContainer.CompilationUnit = expressionContainer.CompilationUnit.ReplaceSyntaxTree(syntaxTree, newSyntaxTree); + var returnType = GetTypeName(expressionToValidate.ResultType); + var lambdaFuncCode = CreateValidationCode(types, returnType, names, expressionToValidate.ExpressionText, expressionToValidate.IsLocation, expressionToValidate.Activity.Id, index); + expressionBuilder.AppendLine(lambdaFuncCode); } - private void EnsureReturnTypeReferenced(ExpressionContainer expressionContainer) + private void EnsureReturnTypeReferenced(Type resultType, ref Compilation compilation) { HashSet allBaseTypes = null; - JitCompilerHelper.EnsureTypeReferenced(expressionContainer.ResultType, ref allBaseTypes); + JitCompilerHelper.EnsureTypeReferenced(resultType, ref allBaseTypes); Lazy> newReferences = new(); foreach (var baseType in allBaseTypes) { @@ -350,9 +304,9 @@ private void EnsureReturnTypeReferenced(ExpressionContainer expressionContainer) } } - if (newReferences.IsValueCreated && expressionContainer.CompilationUnit != null) + if (newReferences.IsValueCreated && compilation != null) { - expressionContainer.CompilationUnit = expressionContainer.CompilationUnit.AddReferences(newReferences.Value); + compilation = compilation.AddReferences(newReferences.Value); } } @@ -374,9 +328,9 @@ private MetadataReference TryGetMetadataReference(Assembly assembly) private bool CanCache(Assembly assembly) => !assembly.IsCollectible && !assembly.IsDynamic; - private void EnsureAssembliesLoaded(ExpressionContainer expressionContainer) + private void EnsureAssembliesLoaded(IReadOnlyCollection assemblies) { - foreach (var assembly in expressionContainer.RequiredAssemblies) + foreach (var assembly in assemblies) { TryGetMetadataReference(assembly); } diff --git a/src/UiPath.Workflow/Validation/VBExpressionValidator.cs b/src/UiPath.Workflow/Validation/VBExpressionValidator.cs new file mode 100644 index 00000000..cace4c0c --- /dev/null +++ b/src/UiPath.Workflow/Validation/VBExpressionValidator.cs @@ -0,0 +1,119 @@ +// This file is part of Core WF which is licensed under the MIT license. +// See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Scripting.Hosting; +using Microsoft.VisualBasic.Activities; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace System.Activities.Validation; +/// +/// Validates VB.NET expressions for use in fast design-time expression validation. +/// +public sealed class VbExpressionValidator : RoslynExpressionValidator +{ + private static readonly Lazy s_instance = new(() => new(s_defaultReferencedAssemblies)); + private static readonly CompilerHelper s_compilerHelper = new VBCompilerHelper(); + + private const string _valueValidationTemplate = "Public Shared Function CreateExpression{0}() As Expression(Of Func(Of {1}))'activityId:{4}\nReturn Function({2}) ({3})'activityId:{4}\nEnd Function"; + private const string _delegateValueValidationTemplate = "{0}\nPublic Shared Function CreateExpression{1}() As Expression(Of {2}(Of {3}))'activityId:{6}\nReturn Function({4}) ({5})'activityId:{6}\nEnd Function"; + private const string _referenceValidationTemplate = "Public Shared Function IsLocation{0}() As {1}'activityId:{4}\nReturn Function({2}) as Action'activityId:{3}\nReturn Sub() {3} = CType(Nothing, {4})'activityId:{5}\nEnd Function'activityId:{4}\nEnd Function"; + + private static readonly VisualBasicParseOptions s_vbScriptParseOptions = + new(kind: SourceCodeKind.Script, languageVersion: LanguageVersion.Latest); + + private static readonly HashSet s_defaultReferencedAssemblies = new() + { + typeof(Collections.ICollection).Assembly, + typeof(Enum).Assembly, + typeof(ComponentModel.BrowsableAttribute).Assembly, + typeof(VisualBasicValue<>).Assembly, + Assembly.Load("netstandard"), + Assembly.Load("System.Runtime") + }; + + private readonly Compilation DefaultCompilationUnit = InitDefaultCompilationUnit(); + + /// + /// Initializes the MetadataReference collection. + /// + /// + /// Assemblies to seed the collection. + /// + private VbExpressionValidator(HashSet referencedAssemblies) + : base(referencedAssemblies != null + ? new HashSet(s_defaultReferencedAssemblies.Union(referencedAssemblies)) + : s_defaultReferencedAssemblies) + { } + + public override string Language => VisualBasicHelper.Language; + + /// + /// Singleton instance of the default validator. + /// + public static VbExpressionValidator Instance => s_instance.Value; + + protected override CompilerHelper CompilerHelper { get; } = new VBCompilerHelper(); + + protected override string ActivityIdentifierRegex { get; } = "('activityId):(.*)"; + + protected override Compilation GetCompilation(IReadOnlyCollection assemblies, IReadOnlyCollection namespaces) + { + var globalImports = GlobalImport.Parse(namespaces); + var metadataReferences = GetMetadataReferencesForExpression(assemblies); + + var options = DefaultCompilationUnit.Options as VisualBasicCompilationOptions; + return DefaultCompilationUnit.WithOptions(options!.WithGlobalImports(globalImports)).WithReferences(metadataReferences); + } + + protected override SyntaxTree GetSyntaxTreeForExpression(string expressionText) => + VisualBasicSyntaxTree.ParseText("? " + expressionText, s_vbScriptParseOptions); + + protected override SyntaxTree GetSyntaxTreeForValidation(string expressionText) => + VisualBasicSyntaxTree.ParseText(expressionText, s_vbScriptParseOptions); + + protected override string GetTypeName(Type type) => VisualBasicObjectFormatter.FormatTypeName(type); + + private static Compilation InitDefaultCompilationUnit() + { + var assemblyName = Guid.NewGuid().ToString(); + VisualBasicCompilationOptions options = new( + OutputKind.DynamicallyLinkedLibrary, + mainTypeName: null, + globalImports: null, + rootNamespace: "", + optionStrict: OptionStrict.On, + optionInfer: true, + optionExplicit: true, + optionCompareText: false, + embedVbCoreRuntime: false, + optimizationLevel: OptimizationLevel.Debug, + checkOverflow: true, + xmlReferenceResolver: null, + sourceReferenceResolver: SourceFileResolver.Default, + concurrentBuild: !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER"))); + return VisualBasicCompilation.Create(assemblyName, null, null, options); + } + + protected override string CreateValueCode(string types, string names, string code, string activityId, int index) + { + var arrayType = types.Split(","); + if (arrayType.Length <= 16) // .net defines Func...Funct PostValidate(Activity activity) + { + var validator = GetValidator(Scope.Language); + return validator.Validate(activity, Scope); + } + + private static RoslynExpressionValidator GetValidator(string language) + { + return language switch + { + CSharpHelper.Language => CSharpExpressionValidator.Instance, + VisualBasicHelper.Language => VbExpressionValidator.Instance, + _ => throw new ArgumentException(language, nameof(language)) + }; + } + + public void QueueExpressionForValidation(ExpressionToValidate expressionToValidate, string language) + { + Scope.AddExpression(expressionToValidate, language); + } + + public ValidationScope Scope { get; } = new(); + } +} diff --git a/src/UiPath.Workflow/Validation/ValidationScope.cs b/src/UiPath.Workflow/Validation/ValidationScope.cs new file mode 100644 index 00000000..8b4903f7 --- /dev/null +++ b/src/UiPath.Workflow/Validation/ValidationScope.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace System.Activities.Validation +{ + internal sealed class ValidationScope + { + private readonly Dictionary _expressionsToValidate = new(); + private string _language; + + internal void AddExpression(ExpressionToValidate expressionToValidate, string language) + { + _language ??= language; + if (_language != language) + { + expressionToValidate.Activity.AddTempValidationError(new ValidationError(SR.DynamicActivityMultipleExpressionLanguages(language), expressionToValidate.Activity)); + return; + } + _expressionsToValidate.Add(expressionToValidate.Activity.Id, expressionToValidate); + } + + internal string Language => _language; + + internal ExpressionToValidate GetExpression(string activityId) => _expressionsToValidate[activityId]; + + internal ImmutableArray GetAllExpressions() => _expressionsToValidate.Values.ToImmutableArray(); + + internal void Clear() => _expressionsToValidate.Clear(); + } +} \ No newline at end of file