Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compile-time type imports #11298

Merged
merged 30 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c3cabe1
Rename Import* syntax kinds to Provider*
jeskew Jun 14, 2023
cb03a8d
Add support for compile-time imports to parser
jeskew Jun 27, 2023
594547f
Add feature flag for compile-time imports
jeskew Jun 27, 2023
1ef9d68
Add @export() decorator for types
jeskew Jun 29, 2023
e4e489e
Integrate imports into type system and module dispatcher
jeskew Jul 5, 2023
df81fb6
Tweak merge
jeskew Jul 17, 2023
232a832
Add support for LS completions
jeskew Jul 19, 2023
41fe9dd
Emit imported type symbols in compiled template
jeskew Jul 24, 2023
de138d7
Try to break import closure info calculation into separate steps
jeskew Jul 25, 2023
25e4f49
Move inner classes into separate files
jeskew Jul 25, 2023
0318593
Ensure @export() is targeting top-level type statement
jeskew Jul 25, 2023
4d0a2e2
Merge branch 'main' into jeskew/imports
jeskew Jul 25, 2023
2308e26
Improve compile-time import diagnostic messaging
jeskew Jul 26, 2023
d4bdc01
Implement go to definition for import symbols
jeskew Jul 26, 2023
928ccba
Regenerate baselines and fix failing tests
jeskew Jul 26, 2023
ecbb177
Augment compile time import completions
jeskew Jul 26, 2023
7206eb4
Fixup message formatting
jeskew Jul 27, 2023
b6eabe8
Fix failing tests
jeskew Jul 27, 2023
aa0f8f5
Use an incrementing counter rather than hashes to create unique names…
jeskew Jul 27, 2023
2a691da
Augment hover support on imported symbols and properties thereof
jeskew Jul 27, 2023
9a6329d
Ensure registry module restoration works as expected for `import` and…
jeskew Jul 27, 2023
700884d
Add ARM source template support for go-to-definition on imported symbols
jeskew Jul 27, 2023
69d84ac
Merge branch 'main' into jeskew/imports
jeskew Jul 30, 2023
e6cfd34
Inject import origin info into type definition metadata for imported …
jeskew Jul 30, 2023
c4b435f
Merge branch 'main' into jeskew/imports
jeskew Aug 1, 2023
9224888
Merge branch 'main' into jeskew/imports
jeskew Aug 2, 2023
6bf9e25
Merge branch 'main' into jeskew/imports
jeskew Aug 2, 2023
838d75c
Merge branch 'main' into jeskew/imports
jeskew Aug 3, 2023
0b292b2
Use ToImmutableDictionary instead of CreateRange
jeskew Aug 4, 2023
41e11e0
Merge branch 'main' into jeskew/imports
jeskew Aug 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Integrate imports into type system and module dispatcher
  • Loading branch information
jeskew committed Jul 24, 2023
commit e4e489e78eca95959f6966ae246482189039b0c0
17 changes: 16 additions & 1 deletion src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1473,7 +1473,7 @@ public ErrorDiagnostic UnknownModuleReferenceScheme(string badScheme, ImmutableA
"BCP250",
$"Parameter \"{identifier}\" is assigned multiple times. Remove or rename the duplicates.");

public ErrorDiagnostic TemplatePathHasNotBeenSpecified() => new(
public ErrorDiagnostic UsingPathHasNotBeenSpecified() => new(
TextSpan,
"BCP256",
"The using declaration is missing a bicep template file path reference.");
Expand Down Expand Up @@ -1990,6 +1990,21 @@ public ErrorDiagnostic RuntimeValueNotAllowedInFunctionDeclaration(string? acces
TextSpan,
"BCP354",
$@"Using compile-time imports requires enabling EXPERIMENTAL feature ""{nameof(ExperimentalFeaturesEnabled.CompileTimeImports)}"".");

public ErrorDiagnostic PathHasNotBeenSpecified() => new(
TextSpan,
"BCP355",
"This declaration is missing a template file path reference.");

public ErrorDiagnostic CompileTimeImportDeclarationMustReferenceTemplate() => new(
TextSpan,
"BCP356",
"A compile-time import can only reference a Bicep file, an ARM template, a registry artifact, or a template spec.");

public ErrorDiagnostic ImportedSymbolNotFound(string symbolName) => new(
TextSpan,
"BCP357",
$"The '{symbolName}' symbol was not found in the imported template.");
}

public static DiagnosticBuilderInternal ForPosition(TextSpan span)
Expand Down
12 changes: 12 additions & 0 deletions src/Bicep.Core/Navigation/IForeignTemplateReference.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Bicep.Core.Syntax;

namespace Bicep.Core.Navigation;

public interface IForeignTemplateReference
{
public SyntaxBase ReferenceSourceSyntax { get; }

public StringSyntax? TryGetPath();
}
6 changes: 2 additions & 4 deletions src/Bicep.Core/Registry/IModuleDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using Bicep.Core.Diagnostics;
using Bicep.Core.Modules;
using Bicep.Core.Syntax;
using Bicep.Core.Navigation;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
Expand All @@ -19,9 +19,7 @@ public interface IModuleDispatcher

bool TryGetModuleReference(string reference, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder);

bool TryGetModuleReference(ModuleDeclarationSyntax module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder);

bool TryGetModuleReference(TestDeclarationSyntax module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder);
bool TryGetModuleReference(IForeignTemplateReference module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder);

RegistryCapabilities GetRegistryCapabilities(ModuleReference moduleReference);

Expand Down
20 changes: 3 additions & 17 deletions src/Bicep.Core/Registry/ModuleDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Bicep.Core.Configuration;
using Bicep.Core.Diagnostics;
using Bicep.Core.Modules;
using Bicep.Core.Navigation;
using Bicep.Core.Semantics;
using Bicep.Core.Syntax;
using System;
Expand Down Expand Up @@ -89,25 +90,10 @@ public bool TryGetModuleReference(string reference, Uri parentModuleUri, [NotNul
}
}

public bool TryGetModuleReference(ModuleDeclarationSyntax module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder)
public bool TryGetModuleReference(IForeignTemplateReference module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder)
{
var moduleReferenceString = SyntaxHelper.TryGetModulePath(module, out var getModulePathFailureBuilder);
if (moduleReferenceString is null)
if (!SyntaxHelper.TryGetForeignTemplatePath(module, out var moduleReferenceString, out failureBuilder))
{
failureBuilder = getModulePathFailureBuilder ?? throw new InvalidOperationException($"Expected {nameof(SyntaxHelper.TryGetModulePath)} to provide failure diagnostics.");
moduleReference = null;
return false;
}

return this.TryGetModuleReference(moduleReferenceString, parentModuleUri, out moduleReference, out failureBuilder);
}

public bool TryGetModuleReference(TestDeclarationSyntax module, Uri parentModuleUri, [NotNullWhen(true)] out ModuleReference? moduleReference, [NotNullWhen(false)] out DiagnosticBuilder.ErrorBuilderDelegate? failureBuilder)
{
var moduleReferenceString = SyntaxHelper.TryGetModulePath(module, out var getModulePathFailureBuilder);
if (moduleReferenceString is null)
{
failureBuilder = getModulePathFailureBuilder ?? throw new InvalidOperationException($"Expected {nameof(SyntaxHelper.TryGetModulePath)} to provide failure diagnostics.");
moduleReference = null;
return false;
}
Expand Down
45 changes: 44 additions & 1 deletion src/Bicep.Core/Semantics/ArmTemplateSemanticModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using Azure.Deployments.Templates.Exceptions;
using Azure.Deployments.Templates.Extensions;
using Bicep.Core.Diagnostics;
using Bicep.Core.Parsing;
using Bicep.Core.Resources;
using Bicep.Core.Semantics.Metadata;
using Bicep.Core.TypeSystem;
Expand All @@ -29,6 +28,8 @@ public class ArmTemplateSemanticModel : ISemanticModel

private readonly Lazy<ImmutableDictionary<string, ParameterMetadata>> parametersLazy;

private readonly Lazy<ImmutableDictionary<string, ExportedTypeMetadata>> exportedTypesLazy;

private readonly Lazy<ImmutableArray<OutputMetadata>> outputsLazy;

public ArmTemplateSemanticModel(ArmTemplateFile sourceFile)
Expand Down Expand Up @@ -85,6 +86,23 @@ public ArmTemplateSemanticModel(ArmTemplateFile sourceFile)
LanguageConstants.IdentifierComparer);
});

// TODO: we have discussed exporting variables and functions in addition to types, but ARM template semantics allow a variable and a type to use the
// same name, whereas Bicep requires symbols to be unique within a template. How should we handle naming conflicts on exported members?
this.exportedTypesLazy = new(() =>
{
if (SourceFile.Template?.Definitions is not {} typeDefinitions)
{
return ImmutableDictionary<string, ExportedTypeMetadata>.Empty;
}

return typeDefinitions.Where(typeDefinition => IsExported(typeDefinition.Value))
.ToImmutableDictionary(typeDefinition => typeDefinition.Key,
typeDefinition => new ExportedTypeMetadata(typeDefinition.Key,
GetType(typeDefinition.Value),
GetMostSpecificDescription(typeDefinition.Value)),
LanguageConstants.IdentifierComparer);
});

this.outputsLazy = new(() =>
{
if (this.SourceFile.Template?.Outputs is null)
Expand All @@ -109,6 +127,8 @@ public ArmTemplateSemanticModel(ArmTemplateFile sourceFile)

public ImmutableDictionary<string, ParameterMetadata> Parameters => this.parametersLazy.Value;

public ImmutableDictionary<string, ExportedTypeMetadata> ExportedTypes => exportedTypesLazy.Value;

public ImmutableArray<OutputMetadata> Outputs => this.outputsLazy.Value;

public bool HasErrors()
Expand Down Expand Up @@ -432,5 +452,28 @@ private static bool TryCreateUnboundResourceTypeParameter(JToken? metadataToken,
}
return null;
}

/// <summary>
/// Determines if the provided type definition should be allowlisted for use in <c>import</c> statements
/// </summary>
/// <remarks>
/// This method does not use <see cref="GetMetadata"/> because <see cref="GetMetadata"/> merges metadata across $refs.
/// We only want to look at the metadata explicitly applied to this type.
/// E.g., in the following, `public` should match the predicate and `private` should not:
/// <code>
/// {
/// "public": {"type": "string", "metadata": {"exported": true}},
/// "private": {"$ref": "#/definitions/public"}
/// }
/// </code>
/// The above would be compiled from the following Bicep:
/// <code>
/// @export()
/// type public = string
/// type private = public
/// </code>
/// </remarks>
private static bool IsExported(TemplateTypeDefinition typeDefinition)
=> typeDefinition.Metadata?.Value is JObject metadataDict && metadataDict["exported"] is JValue { Value: true };
}
}
20 changes: 19 additions & 1 deletion src/Bicep.Core/Semantics/DeclarationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private DeclarationVisitor(INamespaceProvider namespaceProvider, IFeatureProvide
this.targetScope = targetScope;
this.context = context;
this.localScopes = localScopes;
this.sourceFileKind=sourceFileKind;
this.sourceFileKind = sourceFileKind;
}

// Returns the list of top level declarations as well as top level scopes.
Expand Down Expand Up @@ -265,6 +265,24 @@ public override void VisitForSyntax(ForSyntax syntax)
this.PopScope();
}

public override void VisitCompileTimeImportDeclarationSyntax(CompileTimeImportDeclarationSyntax syntax)
{
base.VisitCompileTimeImportDeclarationSyntax(syntax);

switch (syntax.ImportExpression)
{
case WildcardImportSyntax wildcardImport:
DeclareSymbol(new WildcardImportSymbol(context, wildcardImport));
break;
case ImportedSymbolsListSyntax importedSymbolsList:
foreach (var item in importedSymbolsList.ImportedSymbols)
{
DeclareSymbol(new ImportedTypeSymbol(context, item));
}
break;
}
}

private void DeclareSymbol(DeclaredSymbol symbol)
{
if (this.activeScopes.TryPeek(out var current))
Expand Down
13 changes: 11 additions & 2 deletions src/Bicep.Core/Semantics/FileSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public FileSymbol(
this.FileKind = sourceFile.FileKind;
this.LocalScopes = fileScope.ChildScopes;

// TODO: Avoid looping 10 times?
// TODO: Avoid looping 12 times?
this.DeclarationsBySyntax = fileScope.Declarations.ToImmutableDictionary(x => x.DeclaringSyntax);
this.ProviderDeclarations = fileScope.Declarations.OfType<ProviderNamespaceSymbol>().ToImmutableArray();
this.MetadataDeclarations = fileScope.Declarations.OfType<MetadataSymbol>().ToImmutableArray();
Expand All @@ -49,6 +49,9 @@ public FileSymbol(
this.AssertDeclarations = fileScope.Declarations.OfType<AssertSymbol>().ToImmutableArray();
this.ParameterAssignments = fileScope.Declarations.OfType<ParameterAssignmentSymbol>().ToImmutableArray();
this.TestDeclarations = fileScope.Declarations.OfType<TestSymbol>().ToImmutableArray();
this.TypeImports = fileScope.Declarations.OfType<ImportedTypeSymbol>().ToImmutableArray();
this.WildcardImports = fileScope.Declarations.OfType<WildcardImportSymbol>().ToImmutableArray();

this.declarationsByName = this.Declarations.ToLookup(decl => decl.Name, LanguageConstants.IdentifierComparer);

this.usingDeclarationLazy = new Lazy<UsingDeclarationSyntax?>(() => this.Syntax.Children.OfType<UsingDeclarationSyntax>().FirstOrDefault());
Expand All @@ -67,7 +70,9 @@ public FileSymbol(
.Concat(this.ModuleDeclarations)
.Concat(this.OutputDeclarations)
.Concat(this.AssertDeclarations)
.Concat(this.ParameterAssignments);
.Concat(this.ParameterAssignments)
.Concat(this.TypeImports)
.Concat(this.WildcardImports);

public IEnumerable<Symbol> Namespaces =>
this.NamespaceResolver.BuiltIns.Values
Expand Down Expand Up @@ -111,6 +116,10 @@ public FileSymbol(

public ImmutableArray<ParameterAssignmentSymbol> ParameterAssignments { get; }

public ImmutableArray<ImportedTypeSymbol> TypeImports { get; }

public ImmutableArray<WildcardImportSymbol> WildcardImports { get; }

public UsingDeclarationSyntax? UsingDeclarationSyntax => this.usingDeclarationLazy.Value;

public Uri FileUri { get; }
Expand Down
2 changes: 2 additions & 0 deletions src/Bicep.Core/Semantics/ISemanticModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public interface ISemanticModel

ImmutableDictionary<string, ParameterMetadata> Parameters { get; }

ImmutableDictionary<string, ExportedTypeMetadata> ExportedTypes { get; }

ImmutableArray<OutputMetadata> Outputs { get; }

bool HasErrors();
Expand Down
22 changes: 22 additions & 0 deletions src/Bicep.Core/Semantics/ImportedTypeSymbol.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Bicep.Core.Syntax;

namespace Bicep.Core.Semantics;

public class ImportedTypeSymbol : DeclaredSymbol
{
public ImportedTypeSymbol(ISymbolContext context, ImportedSymbolsListItemSyntax declaringSyntax)
: base(context, declaringSyntax.Name.IdentifierName, declaringSyntax, declaringSyntax.Name)
{
}

public ImportedSymbolsListItemSyntax DeclaringImportedSymbolsListItem => (ImportedSymbolsListItemSyntax)DeclaringSyntax;

public override void Accept(SymbolVisitor visitor)
{
visitor.VisitImportedTypeSymbol(this);
}

public override SymbolKind Kind => SymbolKind.TypeAlias;
}
7 changes: 7 additions & 0 deletions src/Bicep.Core/Semantics/Metadata/ExportedTypeMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Bicep.Core.TypeSystem;

namespace Bicep.Core.Semantics.Metadata;

public record ExportedTypeMetadata(string Name, ITypeReference TypeReference, string? Description) {}
33 changes: 5 additions & 28 deletions src/Bicep.Core/Semantics/ModuleSymbol.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Bicep.Core.Diagnostics;
using Bicep.Core.Syntax;
using Bicep.Core.TypeSystem;
using Bicep.Core.Workspaces;

namespace Bicep.Core.Semantics
{
Expand All @@ -24,32 +22,11 @@ public ModuleSymbol(ISymbolContext context, string name, ModuleDeclarationSyntax
public override SymbolKind Kind => SymbolKind.Module;

public bool TryGetSemanticModel([NotNullWhen(true)] out ISemanticModel? semanticModel, [NotNullWhen(false)] out ErrorDiagnostic? failureDiagnostic)
{
if (Context.Compilation.SourceFileGrouping.TryGetErrorDiagnostic(this.DeclaringModule) is {} errorBuilder)
{
semanticModel = null;
failureDiagnostic = errorBuilder(DiagnosticBuilder.ForPosition(DeclaringModule.Path));
return false;
}

// SourceFileGroupingBuilder should have already visited every module declaration and either recorded a failure or mapped it to a syntax tree.
// So it is safe to assume that this lookup will succeed without throwing an exception.
var sourceFile = Context.Compilation.SourceFileGrouping.TryGetSourceFile(this.DeclaringModule) ?? throw new InvalidOperationException($"Failed to find source file for module");

// when we inevitably add a third language ID,
// the inclusion list style below will prevent the new language ID from being
// automatically allowed to be referenced via module declarations
if (sourceFile is not BicepFile and not ArmTemplateFile and not TemplateSpecFile)
{
semanticModel = null;
failureDiagnostic = DiagnosticBuilder.ForPosition(DeclaringModule.Path).ModuleDeclarationMustReferenceBicepModule();
return false;
}

failureDiagnostic = null;
semanticModel = Context.Compilation.GetSemanticModel(sourceFile);
return true;
}
=> SemanticModelHelper.TryGetSemanticModelForForeignTemplateReference(Context.Compilation,
DeclaringModule,
b => b.ModuleDeclarationMustReferenceBicepModule(),
out semanticModel,
out failureDiagnostic);

public override IEnumerable<Symbol> Descendants
{
Expand Down
18 changes: 16 additions & 2 deletions src/Bicep.Core/Semantics/SemanticModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Bicep.Core.Features;
using Bicep.Core.FileSystem;
using Bicep.Core.Semantics.Metadata;
using Bicep.Core.Semantics.Namespaces;
using Bicep.Core.Syntax;
using Bicep.Core.Syntax.Visitors;
using Bicep.Core.Text;
Expand All @@ -29,6 +30,7 @@ public class SemanticModel : ISemanticModel
private readonly Lazy<SymbolHierarchy> symbolHierarchyLazy;
private readonly Lazy<ResourceAncestorGraph> resourceAncestorsLazy;
private readonly Lazy<ImmutableDictionary<string, ParameterMetadata>> parametersLazy;
private readonly Lazy<ImmutableDictionary<string, ExportedTypeMetadata>> exportedTypesLazy;
private readonly Lazy<ImmutableArray<OutputMetadata>> outputsLazy;
private readonly Lazy<IApiVersionProvider> apiVersionProviderLazy;

Expand Down Expand Up @@ -57,9 +59,10 @@ public SemanticModel(Compilation compilation, BicepSourceFile sourceFile, IFileR
this.SymbolContext = symbolContext;
this.Binder = new Binder(compilation.NamespaceProvider, features, sourceFile, this.SymbolContext);
this.apiVersionProviderLazy = new Lazy<IApiVersionProvider>(() => new ApiVersionProvider(features, this.Binder.NamespaceResolver.GetAvailableResourceTypes()));
this.TypeManager = new TypeManager(features, this.Binder, fileResolver, this.ParsingErrorLookup, this.SourceFile.FileKind);
this.TypeManager = new TypeManager(features, Binder, fileResolver, this.ParsingErrorLookup, this.SourceFile.FileKind, Compilation);

// name binding is done, allow type queries now
// name binding is done
// allow type queries now
symbolContext.Unlock();

this.emitLimitationInfoLazy = new Lazy<EmitLimitationInfo>(() => EmitLimitationCalculator.Calculate(this));
Expand Down Expand Up @@ -108,6 +111,15 @@ public SemanticModel(Compilation compilation, BicepSourceFile sourceFile, IFileR
return parameters.ToImmutable();
});

this.exportedTypesLazy = new(() => ImmutableDictionary.CreateRange(Root.TypeDeclarations.DistinctBy(t => t.Name)
// skip over any type without an `@export()` decorator
.Where(t => SemanticModelHelper.TryGetDecoratorInNamespace(this,
t.DeclaringType,
SystemNamespaceType.BuiltInName,
LanguageConstants.ExportPropertyName) is not null)
.Select(t => new KeyValuePair<string, ExportedTypeMetadata>(t.Name,
new(t.Name, t.Type, DescriptionHelper.TryGetFromDecorator(this, t.DeclaringType))))));
jeskew marked this conversation as resolved.
Show resolved Hide resolved

this.outputsLazy = new Lazy<ImmutableArray<OutputMetadata>>(() =>
{
var outputs = new List<OutputMetadata>();
Expand Down Expand Up @@ -224,6 +236,8 @@ static IEnumerable<string> getExperimentalFeatures(RootConfiguration configurati

public ImmutableDictionary<string, ParameterMetadata> Parameters => this.parametersLazy.Value;

public ImmutableDictionary<string, ExportedTypeMetadata> ExportedTypes => exportedTypesLazy.Value;

public ImmutableArray<OutputMetadata> Outputs => this.outputsLazy.Value;

/// <summary>
Expand Down
Loading