aspnetcore/test/Microsoft.VisualStudio.Edit.../RazorSyntaxTreePartialParse...

632 lines
29 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Text;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class RazorSyntaxTreePartialParserTest
{
public static TheoryData TagHelperPartialParseRejectData
{
get
{
return new TheoryData<TestEdit>
{
CreateInsertionChange("<p></p>", 2, " "),
CreateInsertionChange("<p></p>", 6, " "),
CreateInsertionChange("<p some-attr></p>", 12, " "),
CreateInsertionChange("<p some-attr></p>", 12, "ibute"),
CreateInsertionChange("<p some-attr></p>", 2, " before"),
};
}
}
[Theory]
[MemberData(nameof(TagHelperPartialParseRejectData))]
public void TagHelperTagBodiesRejectPartialChanges(object objectEdit)
{
// Arrange
var edit = (TestEdit)objectEdit;
var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly");
builder.SetTypeName("PTagHelper");
builder.TagMatchingRule(rule => rule.TagName = "p");
var descriptors = new[]
{
builder.Build()
};
var templateEngine = CreateTemplateEngine(tagHelpers: descriptors);
var document = TestRazorCodeDocument.Create(
TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()),
new[] { templateEngine.Options.DefaultImports });
templateEngine.Engine.Process(document);
var syntaxTree = document.GetSyntaxTree();
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
// Act
var result = parser.Parse(edit.Change);
// Assert
Assert.Equal(PartialParseResultInternal.Rejected, result);
}
public static TheoryData TagHelperAttributeAcceptData
{
get
{
var factory = new SpanFactory();
// change, (Block)expectedDocument, partialParseResult
return new TheoryData<TestEdit, PartialParseResultInternal>
{
{
CreateInsertionChange("<p str-attr='@DateTime'></p>", 22, "."),
PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
},
{
CreateInsertionChange("<p obj-attr='DateTime'></p>", 21, "."),
PartialParseResultInternal.Accepted
},
{
CreateInsertionChange("<p obj-attr='1 + DateTime'></p>", 25, "."),
PartialParseResultInternal.Accepted
},
{
CreateInsertionChange("<p before-attr str-attr='@DateTime' after-attr></p>", 34, "."),
PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
},
{
CreateInsertionChange("<p str-attr='before @DateTime after'></p>", 29, "."),
PartialParseResultInternal.Accepted | PartialParseResultInternal.Provisional
},
};
}
}
[Theory]
[MemberData(nameof(TagHelperAttributeAcceptData))]
public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly(object editObject, object partialParseResultObject)
{
// Arrange
var edit = (TestEdit)editObject;
var partialParseResult = (PartialParseResultInternal)partialParseResultObject;
var builder = TagHelperDescriptorBuilder.Create("PTagHelper", "Test");
builder.SetTypeName("PTagHelper");
builder.TagMatchingRule(rule => rule.TagName = "p");
builder.BindAttribute(attribute =>
{
attribute.Name = "obj-attr";
attribute.TypeName = typeof(object).FullName;
attribute.SetPropertyName("ObjectAttribute");
});
builder.BindAttribute(attribute =>
{
attribute.Name = "str-attr";
attribute.TypeName = typeof(string).FullName;
attribute.SetPropertyName("StringAttribute");
});
var descriptors = new[] { builder.Build() };
var templateEngine = CreateTemplateEngine(tagHelpers: descriptors);
var document = TestRazorCodeDocument.Create(
TestRazorSourceDocument.Create(edit.OldSnapshot.GetText()),
new[] { templateEngine.Options.DefaultImports });
templateEngine.Engine.Process(document);
var syntaxTree = document.GetSyntaxTree();
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
// Act
var result = parser.Parse(edit.Change);
// Assert
Assert.Equal(partialParseResult, result);
}
[Fact]
public void ImplicitExpressionAcceptsInnerInsertionsInStatementBlock()
{
// Arrange
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ " @DateTime..Now" + Environment.NewLine
+ "}");
var old = new StringTextSnapshot("@{" + Environment.NewLine
+ " @DateTime.Now" + Environment.NewLine
+ "}");
// Act and Assert
RunPartialParseTest(new TestEdit(17, 0, old, 1, changed, "."),
new MarkupBlock(
factory.EmptyHtml(),
new StatementBlock(
factory.CodeTransition(),
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
factory.Code(Environment.NewLine + " ")
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime..Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Code(Environment.NewLine).AsStatement(),
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
factory.EmptyHtml()));
}
[Fact]
public void ImplicitExpressionAcceptsInnerInsertions()
{
// Arrange
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @DateTime..Now baz");
var old = new StringTextSnapshot("foo @DateTime.Now baz");
// Act and Assert
RunPartialParseTest(new TestEdit(13, 0, old, 1, changed, "."),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime..Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")), additionalFlags: PartialParseResultInternal.Provisional);
}
[Fact]
public void ImplicitExpressionAcceptsWholeIdentifierReplacement()
{
// Arrange
var factory = new SpanFactory();
var old = new StringTextSnapshot("foo @date baz");
var changed = new StringTextSnapshot("foo @DateTime baz");
// Act and Assert
RunPartialParseTest(new TestEdit(5, 4, old, 8, changed, "DateTime"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionRejectsWholeIdentifierReplacementToKeyword()
{
// Arrange
var old = new StringTextSnapshot("foo @date baz");
var changed = new StringTextSnapshot("foo @if baz");
var edit = new TestEdit(5, 4, old, 2, changed, "if");
// Act & Assert
RunPartialParseRejectionTest(edit);
}
[Fact]
public void ImplicitExpressionRejectsWholeIdentifierReplacementToDirective()
{
// Arrange
var old = new StringTextSnapshot("foo @date baz");
var changed = new StringTextSnapshot("foo @inherits baz");
var edit = new TestEdit(5, 4, old, 8, changed, "inherits");
// Act & Assert
RunPartialParseRejectionTest(edit, PartialParseResultInternal.SpanContextChanged);
}
[Fact]
public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_SingleSymbol()
{
// Arrange
var factory = new SpanFactory();
var old = new StringTextSnapshot("foo @dTime baz");
var changed = new StringTextSnapshot("foo @DateTime baz");
// Act and Assert
RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionAcceptsPrefixIdentifierReplacements_MultipleSymbols()
{
// Arrange
var factory = new SpanFactory();
var old = new StringTextSnapshot("foo @dTime.Now baz");
var changed = new StringTextSnapshot("foo @DateTime.Now baz");
// Act and Assert
RunPartialParseTest(new TestEdit(5, 1, old, 4, changed, "Date"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_SingleSymbol()
{
// Arrange
var factory = new SpanFactory();
var old = new StringTextSnapshot("foo @Datet baz");
var changed = new StringTextSnapshot("foo @DateTime baz");
// Act and Assert
RunPartialParseTest(new TestEdit(9, 1, old, 4, changed, "Time"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionAcceptsSuffixIdentifierReplacements_MultipleSymbols()
{
// Arrange
var factory = new SpanFactory();
var old = new StringTextSnapshot("foo @DateTime.n baz");
var changed = new StringTextSnapshot("foo @DateTime.Now baz");
// Act and Assert
RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime.Now").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionAcceptsSurroundedIdentifierReplacements()
{
// Arrange
var factory = new SpanFactory();
var old = new StringTextSnapshot("foo @DateTime.n.ToString() baz");
var changed = new StringTextSnapshot("foo @DateTime.Now.ToString() baz");
// Act and Assert
RunPartialParseTest(new TestEdit(14, 1, old, 3, changed, "Now"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("DateTime.Now.ToString()").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionProvisionallyAcceptsDeleteOfIdentifierPartsIfDotRemains()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @User. baz");
var old = new StringTextSnapshot("foo @User.Name baz");
RunPartialParseTest(new TestEdit(10, 4, old, 0, changed, string.Empty),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")),
additionalFlags: PartialParseResultInternal.Provisional);
}
[Fact]
public void ImplicitExpressionAcceptsDeleteOfIdentifierPartsIfSomeOfIdentifierRemains()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @Us baz");
var old = new StringTextSnapshot("foo @User baz");
RunPartialParseTest(new TestEdit(7, 2, old, 0, changed, string.Empty),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("Us").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionProvisionallyAcceptsMultipleInsertionIfItCausesIdentifierExpansionAndTrailingDot()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @User. baz");
var old = new StringTextSnapshot("foo @U baz");
RunPartialParseTest(new TestEdit(6, 0, old, 4, changed, "ser."),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("User.").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")),
additionalFlags: PartialParseResultInternal.Provisional);
}
[Fact]
public void ImplicitExpressionAcceptsMultipleInsertionIfItOnlyCausesIdentifierExpansion()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @barbiz baz");
var old = new StringTextSnapshot("foo @bar baz");
RunPartialParseTest(new TestEdit(8, 0, old, 3, changed, "biz"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("barbiz").AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" baz")));
}
[Fact]
public void ImplicitExpressionAcceptsIdentifierExpansionAtEndOfNonWhitespaceCharacters()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ " @food" + Environment.NewLine
+ "}");
var old = new StringTextSnapshot("@{" + Environment.NewLine
+ " @foo" + Environment.NewLine
+ "}");
RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "d"),
new MarkupBlock(
factory.EmptyHtml(),
new StatementBlock(
factory.CodeTransition(),
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
factory.Code(Environment.NewLine + " ")
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("food")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Code(Environment.NewLine).AsStatement(),
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
factory.EmptyHtml()));
}
[Fact]
public void ImplicitExpressionAcceptsIdentifierAfterDotAtEndOfNonWhitespaceCharacters()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ " @foo.d" + Environment.NewLine
+ "}");
var old = new StringTextSnapshot("@{" + Environment.NewLine
+ " @foo." + Environment.NewLine
+ "}");
RunPartialParseTest(new TestEdit(11 + Environment.NewLine.Length, 0, old, 1, changed, "d"),
new MarkupBlock(
factory.EmptyHtml(),
new StatementBlock(
factory.CodeTransition(),
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
factory.Code(Environment.NewLine + " ")
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("foo.d")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Code(Environment.NewLine).AsStatement(),
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
factory.EmptyHtml()));
}
[Fact]
public void ImplicitExpressionAcceptsDotAtEndOfNonWhitespaceCharacters()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{" + Environment.NewLine
+ " @foo." + Environment.NewLine
+ "}");
var old = new StringTextSnapshot("@{" + Environment.NewLine
+ " @foo" + Environment.NewLine
+ "}");
RunPartialParseTest(new TestEdit(10 + Environment.NewLine.Length, 0, old, 1, changed, "."),
new MarkupBlock(
factory.EmptyHtml(),
new StatementBlock(
factory.CodeTransition(),
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
factory.Code(Environment.NewLine + " ")
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code(@"foo.")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Code(Environment.NewLine).AsStatement(),
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
factory.EmptyHtml()));
}
[Fact]
public void ImplicitExpressionProvisionallyAcceptsDotAfterIdentifierInMarkup()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @foo. bar");
var old = new StringTextSnapshot("foo @foo bar");
RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "."),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("foo.")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" bar")),
additionalFlags: PartialParseResultInternal.Provisional);
}
[Fact]
public void ImplicitExpressionAcceptsAdditionalIdentifierCharactersIfEndOfSpanIsIdentifier()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("foo @foob bar");
var old = new StringTextSnapshot("foo @foo bar");
RunPartialParseTest(new TestEdit(8, 0, old, 1, changed, "b"),
new MarkupBlock(
factory.Markup("foo "),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("foob")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.Markup(" bar")));
}
[Fact]
public void ImplicitExpressionAcceptsAdditionalIdentifierStartCharactersIfEndOfSpanIsDot()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{@foo.b}");
var old = new StringTextSnapshot("@{@foo.}");
RunPartialParseTest(new TestEdit(7, 0, old, 1, changed, "b"),
new MarkupBlock(
factory.EmptyHtml(),
new StatementBlock(
factory.CodeTransition(),
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
factory.EmptyCSharp()
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("foo.b")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.EmptyCSharp().AsStatement(),
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
factory.EmptyHtml()));
}
[Fact]
public void ImplicitExpressionAcceptsDotIfTrailingDotsAreAllowed()
{
var factory = new SpanFactory();
var changed = new StringTextSnapshot("@{@foo.}");
var old = new StringTextSnapshot("@{@foo}");
RunPartialParseTest(new TestEdit(6, 0, old, 1, changed, "."),
new MarkupBlock(
factory.EmptyHtml(),
new StatementBlock(
factory.CodeTransition(),
factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
factory.EmptyCSharp()
.AsStatement()
.AutoCompleteWith(autoCompleteString: null),
new ExpressionBlock(
factory.CodeTransition(),
factory.Code("foo.")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharactersInternal.NonWhiteSpace)),
factory.EmptyCSharp().AsStatement(),
factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
factory.EmptyHtml()));
}
private void RunPartialParseRejectionTest(TestEdit edit, PartialParseResultInternal additionalFlags = 0)
{
var templateEngine = CreateTemplateEngine();
var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText());
templateEngine.Engine.Process(document);
var syntaxTree = document.GetSyntaxTree();
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
var result = parser.Parse(edit.Change);
Assert.Equal(PartialParseResultInternal.Rejected | additionalFlags, result);
}
private static void RunPartialParseTest(TestEdit edit, Block expectedTree, PartialParseResultInternal additionalFlags = 0)
{
var templateEngine = CreateTemplateEngine();
var document = TestRazorCodeDocument.Create(edit.OldSnapshot.GetText());
templateEngine.Engine.Process(document);
var syntaxTree = document.GetSyntaxTree();
var parser = new RazorSyntaxTreePartialParser(syntaxTree);
var result = parser.Parse(edit.Change);
Assert.Equal(PartialParseResultInternal.Accepted | additionalFlags, result);
ParserTestBase.EvaluateParseTree(expectedTree, syntaxTree.Root);
}
private static TestEdit CreateInsertionChange(string initialText, int insertionLocation, string insertionText)
{
var changedText = initialText.Insert(insertionLocation, insertionText);
var sourceChange = new SourceChange(insertionLocation, 0, insertionText);
var oldSnapshot = new StringTextSnapshot(initialText);
var changedSnapshot = new StringTextSnapshot(changedText);
return new TestEdit
{
Change = sourceChange,
OldSnapshot = oldSnapshot,
NewSnapshot = changedSnapshot,
};
}
private static RazorTemplateEngine CreateTemplateEngine(
string path = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml",
IEnumerable<TagHelperDescriptor> tagHelpers = null)
{
var engine = RazorEngine.CreateDesignTime(builder =>
{
RazorExtensions.Register(builder);
if (tagHelpers != null)
{
builder.AddTagHelpers(tagHelpers);
}
});
// GetImports on RazorTemplateEngine will at least check that the item exists, so we need to pretend
// that it does.
var items = new List<RazorProjectItem>();
items.Add(new TestRazorProjectItem(path));
var project = new TestRazorProject(items);
var templateEngine = new RazorTemplateEngine(engine, project);
templateEngine.Options.DefaultImports = RazorSourceDocument.Create("@addTagHelper *, Test", "_TestImports.cshtml");
return templateEngine;
}
private class TestEdit
{
public TestEdit()
{
}
public TestEdit(int position, int oldLength, ITextSnapshot oldSnapshot, int newLength, ITextSnapshot newSnapshot, string newText)
{
Change = new SourceChange(position, oldLength, newText);
OldSnapshot = oldSnapshot;
NewSnapshot = newSnapshot;
}
public SourceChange Change { get; set; }
public ITextSnapshot OldSnapshot { get; set; }
public ITextSnapshot NewSnapshot { get; set; }
}
}
}