提交 8faa6685 编写于 作者: T Tomas Matousek

Fix syntax associated with bound block of foreach

上级 1b93a724
...@@ -125,6 +125,9 @@ private BoundStatement RewriteEnumeratorForEachStatement(BoundForEachStatement n ...@@ -125,6 +125,9 @@ private BoundStatement RewriteEnumeratorForEachStatement(BoundForEachStatement n
// V v = (V)(T)e.Current; // V v = (V)(T)e.Current;
// /* node.Body */ // /* node.Body */
// } // }
var rewrittenBodyBlock = CreateBlockDeclaringIterationVariable(iterationVar, iterationVarDecl, rewrittenBody, forEachSyntax);
BoundStatement whileLoop = RewriteWhileStatement( BoundStatement whileLoop = RewriteWhileStatement(
syntax: forEachSyntax, syntax: forEachSyntax,
rewrittenCondition: BoundCall.Synthesized( rewrittenCondition: BoundCall.Synthesized(
...@@ -132,9 +135,7 @@ private BoundStatement RewriteEnumeratorForEachStatement(BoundForEachStatement n ...@@ -132,9 +135,7 @@ private BoundStatement RewriteEnumeratorForEachStatement(BoundForEachStatement n
receiverOpt: boundEnumeratorVar, receiverOpt: boundEnumeratorVar,
method: enumeratorInfo.MoveNextMethod), method: enumeratorInfo.MoveNextMethod),
conditionSequencePointSpan: forEachSyntax.InKeyword.Span, conditionSequencePointSpan: forEachSyntax.InKeyword.Span,
rewrittenBody: new BoundBlock(rewrittenBody.Syntax, rewrittenBody: rewrittenBodyBlock,
statements: ImmutableArray.Create<BoundStatement>(iterationVarDecl, rewrittenBody),
locals: ImmutableArray.Create<LocalSymbol>(iterationVar)),
breakLabel: node.BreakLabel, breakLabel: node.BreakLabel,
continueLabel: node.ContinueLabel, continueLabel: node.ContinueLabel,
hasErrors: false); hasErrors: false);
...@@ -458,9 +459,8 @@ private BoundStatement RewriteStringForEachStatement(BoundForEachStatement node) ...@@ -458,9 +459,8 @@ private BoundStatement RewriteStringForEachStatement(BoundForEachStatement node)
AddForEachIterationVariableSequencePoint(forEachSyntax, ref iterationVarDecl); AddForEachIterationVariableSequencePoint(forEachSyntax, ref iterationVarDecl);
// { V v = (V)s.Chars[p]; /*node.Body*/ } // { V v = (V)s.Chars[p]; /*node.Body*/ }
BoundStatement loopBody = new BoundBlock(forEachSyntax,
locals: ImmutableArray.Create<LocalSymbol>(iterationVar), BoundStatement loopBody = CreateBlockDeclaringIterationVariable(iterationVar, iterationVarDecl, rewrittenBody, forEachSyntax);
statements: ImmutableArray.Create<BoundStatement>(iterationVarDecl, rewrittenBody));
// for (string s = /*node.Expression*/, int p = 0; p < s.Length; p = p + 1) { // for (string s = /*node.Expression*/, int p = 0; p < s.Length; p = p + 1) {
// V v = (V)s.Chars[p]; // V v = (V)s.Chars[p];
...@@ -484,6 +484,27 @@ private BoundStatement RewriteStringForEachStatement(BoundForEachStatement node) ...@@ -484,6 +484,27 @@ private BoundStatement RewriteStringForEachStatement(BoundForEachStatement node)
return result; return result;
} }
private static BoundBlock CreateBlockDeclaringIterationVariable(
LocalSymbol iterationVariable,
BoundStatement iteratorVariableInitialization,
BoundStatement rewrittenBody,
ForEachStatementSyntax forEachSyntax)
{
// The scope of the iteration variable is the embedded statement syntax.
// However consider the following foreach statement:
//
// foreach (int x in ...) { int y = ...; F(() => x); F(() => y));
//
// We currently generate 2 closures. One containing variable x, the other variable y.
// The EnC source mapping infrastructure requires each closure within a method body
// to have a unique syntax offset. Hence we associate the bound block declaring the
// iteration variable with the foreach statement, not the embedded statement.
return new BoundBlock(
forEachSyntax,
locals: ImmutableArray.Create(iterationVariable),
statements: ImmutableArray.Create(iteratorVariableInitialization, rewrittenBody));
}
/// <summary> /// <summary>
/// Lower a foreach loop that will enumerate a single-dimensional array. /// Lower a foreach loop that will enumerate a single-dimensional array.
/// ///
...@@ -583,10 +604,9 @@ private BoundStatement RewriteSingleDimensionalArrayForEachStatement(BoundForEac ...@@ -583,10 +604,9 @@ private BoundStatement RewriteSingleDimensionalArrayForEachStatement(BoundForEac
BoundStatement positionIncrement = MakePositionIncrement(forEachSyntax, boundPositionVar, intType); BoundStatement positionIncrement = MakePositionIncrement(forEachSyntax, boundPositionVar, intType);
// { V v = (V)a[p]; /* node.Body */ } // { V v = (V)a[p]; /* node.Body */ }
BoundStatement loopBody = new BoundBlock(forEachSyntax,
locals: ImmutableArray.Create<LocalSymbol>(iterationVar),
statements: ImmutableArray.Create<BoundStatement>(iterationVariableDecl, rewrittenBody));
BoundStatement loopBody = CreateBlockDeclaringIterationVariable(iterationVar, iterationVariableDecl, rewrittenBody, forEachSyntax);
// for (A[] a = /*node.Expression*/, int p = 0; p < a.Length; p = p + 1) { // for (A[] a = /*node.Expression*/, int p = 0; p < a.Length; p = p + 1) {
// V v = (V)a[p]; // V v = (V)a[p];
// /*node.Body*/ // /*node.Body*/
...@@ -714,9 +734,8 @@ private BoundStatement RewriteMultiDimensionalArrayForEachStatement(BoundForEach ...@@ -714,9 +734,8 @@ private BoundStatement RewriteMultiDimensionalArrayForEachStatement(BoundForEach
AddForEachIterationVariableSequencePoint(forEachSyntax, ref iterationVarDecl); AddForEachIterationVariableSequencePoint(forEachSyntax, ref iterationVarDecl);
// { V v = (V)a[p_0, p_1, ...]; /* node.Body */ } // { V v = (V)a[p_0, p_1, ...]; /* node.Body */ }
BoundStatement innermostLoopBody = new BoundBlock(forEachSyntax,
locals: ImmutableArray.Create(iterationVar), BoundStatement innermostLoopBody = CreateBlockDeclaringIterationVariable(iterationVar, iterationVarDecl, rewrittenBody, forEachSyntax);
statements: ImmutableArray.Create(iterationVarDecl, rewrittenBody));
// work from most-nested to least-nested // work from most-nested to least-nested
// for (int p_0 = a.GetLowerBound(0); p_0 <= q_0; p_0 = p_0 + 1) // for (int p_0 = a.GetLowerBound(0); p_0 <= q_0; p_0 = p_0 + 1)
......
...@@ -5132,6 +5132,8 @@ static void Main(string[] args) ...@@ -5132,6 +5132,8 @@ static void Main(string[] args)
CompileAndVerify(source); CompileAndVerify(source);
} }
#endregion
[Fact] [Fact]
public void LambdaInQuery_Let() public void LambdaInQuery_Let()
{ {
...@@ -5172,6 +5174,37 @@ public void F(int[] array) ...@@ -5172,6 +5174,37 @@ public void F(int[] array)
CompileAndVerify(source, new[] { SystemCoreRef }); CompileAndVerify(source, new[] { SystemCoreRef });
} }
#endregion [Fact]
public void EmbeddedStatementClosures1()
{
var source = @"
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class C
{
public void G<T>(Func<T> f) {}
public void F()
{
for (int x = 1, y = 2; x < 10; x++) G(() => x + y);
for (int x = 1, y = 2; x < 10; x++) { G(() => x + y); }
foreach (var x in new[] { 1, 2, 3 }) G(() => x);
foreach (var x in new[] { 1, 2, 3 }) { G(() => x); }
foreach (var x in new[,] { {1}, {2}, {3} }) G(() => x);
foreach (var x in new[,] { {1}, {2}, {3} }) { G(() => x); }
foreach (var x in ""123"") G(() => x);
foreach (var x in ""123"") { G(() => x); }
foreach (var x in new List<string>()) G(() => x);
foreach (var x in new List<string>()) { G(() => x); }
using (var x = new MemoryStream()) G(() => x);
using (var x = new MemoryStream()) G(() => x);
}
}";
CompileAndVerify(source, new[] { SystemCoreRef });
}
} }
} }
...@@ -1009,7 +1009,7 @@ public void CatchUpdate() ...@@ -1009,7 +1009,7 @@ public void CatchUpdate()
var edits = GetMethodEdits(src1, src2); var edits = GetMethodEdits(src1, src2);
edits.VerifyEdits( edits.VerifyEdits(
"Update [catch (Exception e) { }]@10 -> [catch (IOException e) { }]@10"); "Update [(Exception e)]@16 -> [(IOException e)]@16");
} }
[Fact] [Fact]
...@@ -1022,6 +1022,7 @@ public void CatchInsert() ...@@ -1022,6 +1022,7 @@ public void CatchInsert()
edits.VerifyEdits( edits.VerifyEdits(
"Insert [catch (IOException e) { /*3*/ }]@16", "Insert [catch (IOException e) { /*3*/ }]@16",
"Insert [(IOException e)]@22",
"Insert [{ /*3*/ }]@38"); "Insert [{ /*3*/ }]@38");
} }
...@@ -1047,6 +1048,7 @@ public void CatchDelete() ...@@ -1047,6 +1048,7 @@ public void CatchDelete()
edits.VerifyEdits( edits.VerifyEdits(
"Delete [catch (Exception e) { }]@36", "Delete [catch (Exception e) { }]@36",
"Delete [(Exception e)]@42",
"Delete [{ }]@56"); "Delete [{ }]@56");
} }
...@@ -1073,7 +1075,7 @@ public void CatchReorder2() ...@@ -1073,7 +1075,7 @@ public void CatchReorder2()
edits.VerifyEdits( edits.VerifyEdits(
"Reorder [catch (Exception e) { }]@36 -> @26", "Reorder [catch (Exception e) { }]@36 -> @26",
"Reorder [catch { }]@60 -> @10", "Reorder [catch { }]@60 -> @10",
"Update [catch { }]@60 -> [catch (A e) { }]@10"); "Insert [(A e)]@16");
} }
[Fact] [Fact]
...@@ -1105,8 +1107,10 @@ public void CatchInsertDelete() ...@@ -1105,8 +1107,10 @@ public void CatchInsertDelete()
edits.VerifyEdits( edits.VerifyEdits(
"Insert [catch (E e) { /*1*/ }]@79", "Insert [catch (E e) { /*1*/ }]@79",
"Insert [(E e)]@85",
"Move [{ /*1*/ }]@29 -> @91", "Move [{ /*1*/ }]@29 -> @91",
"Delete [catch (E e) { /*1*/ }]@17"); "Delete [catch (E e) { /*1*/ }]@17",
"Delete [(E e)]@23");
} }
[Fact] [Fact]
...@@ -1119,7 +1123,8 @@ public void Catch_DeleteHeader1() ...@@ -1119,7 +1123,8 @@ public void Catch_DeleteHeader1()
edits.VerifyEdits( edits.VerifyEdits(
"Move [{ /*3*/ }]@52 -> @39", "Move [{ /*3*/ }]@52 -> @39",
"Delete [catch (E2 e) { /*3*/ }]@39"); "Delete [catch (E2 e) { /*3*/ }]@39",
"Delete [(E2 e)]@45");
} }
[Fact] [Fact]
...@@ -1132,6 +1137,7 @@ public void Catch_InsertHeader1() ...@@ -1132,6 +1137,7 @@ public void Catch_InsertHeader1()
edits.VerifyEdits( edits.VerifyEdits(
"Insert [catch (E2 e) { /*3*/ }]@39", "Insert [catch (E2 e) { /*3*/ }]@39",
"Insert [(E2 e)]@45",
"Move [{ /*3*/ }]@39 -> @52"); "Move [{ /*3*/ }]@39 -> @52");
} }
...@@ -1182,7 +1188,7 @@ public void Catch_InsertFilter2() ...@@ -1182,7 +1188,7 @@ public void Catch_InsertFilter2()
var edits = GetMethodEdits(src1, src2); var edits = GetMethodEdits(src1, src2);
edits.VerifyEdits( edits.VerifyEdits(
"Update [catch when (e == null) { /*2*/ }]@16 -> [catch (E1 e) when (e == null) { /*2*/ }]@16"); "Insert [(E1 e)]@22");
} }
[Fact] [Fact]
...@@ -1194,7 +1200,7 @@ public void Catch_InsertFilter3() ...@@ -1194,7 +1200,7 @@ public void Catch_InsertFilter3()
var edits = GetMethodEdits(src1, src2); var edits = GetMethodEdits(src1, src2);
edits.VerifyEdits( edits.VerifyEdits(
"Update [catch { /*2*/ }]@16 -> [catch (E1 e) when (e == null) { /*2*/ }]@16", "Insert [(E1 e)]@22",
"Insert [when (e == null)]@29"); "Insert [when (e == null)]@29");
} }
...@@ -1207,7 +1213,7 @@ public void Catch_DeleteDeclaration1() ...@@ -1207,7 +1213,7 @@ public void Catch_DeleteDeclaration1()
var edits = GetMethodEdits(src1, src2); var edits = GetMethodEdits(src1, src2);
edits.VerifyEdits( edits.VerifyEdits(
"Update [catch (E1 e) { /*2*/ }]@16 -> [catch { /*2*/ }]@16"); "Delete [(E1 e)]@22");
} }
[Fact] [Fact]
...@@ -1231,7 +1237,7 @@ public void Catch_DeleteFilter2() ...@@ -1231,7 +1237,7 @@ public void Catch_DeleteFilter2()
var edits = GetMethodEdits(src1, src2); var edits = GetMethodEdits(src1, src2);
edits.VerifyEdits( edits.VerifyEdits(
"Update [catch (E1 e) when (e == null) { /*2*/ }]@16 -> [catch when (e == null) { /*2*/ }]@16"); "Delete [(E1 e)]@22");
} }
[Fact] [Fact]
...@@ -1243,7 +1249,7 @@ public void Catch_DeleteFilter3() ...@@ -1243,7 +1249,7 @@ public void Catch_DeleteFilter3()
var edits = GetMethodEdits(src1, src2); var edits = GetMethodEdits(src1, src2);
edits.VerifyEdits( edits.VerifyEdits(
"Update [catch (E1 e) when (e == null) { /*2*/ }]@16 -> [catch { /*2*/ }]@16", "Delete [(E1 e)]@22",
"Delete [when (e == null)]@29"); "Delete [when (e == null)]@29");
} }
...@@ -2678,6 +2684,382 @@ void F() ...@@ -2678,6 +2684,382 @@ void F()
Diagnostic(RudeEditKind.DeleteLambdaWithMultiScopeCapture, "x3", CSharpFeaturesResources.Lambda, "this", "x3")); Diagnostic(RudeEditKind.DeleteLambdaWithMultiScopeCapture, "x3", CSharpFeaturesResources.Lambda, "this", "x3"));
} }
[Fact]
public void Lambdas_Insert_ForEach1()
{
var src1 = @"
using System;
class C
{
void G(Func<int, int> f) {}
void F()
{
foreach (int x0 in new[] { 1 }) // Group #0
{ // Group #1
int x1 = 0;
G(a => x0);
G(a => x1);
}
}
}
";
var src2 = @"
using System;
class C
{
void G(Func<int, int> f) {}
void F()
{
foreach (int x0 in new[] { 1 }) // Group #0
{ // Group #1
int x1 = 0;
G(a => x0);
G(a => x1);
G(a => x0 + x1); // error: connecting previously disconnected closures
}
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics(
Diagnostic(RudeEditKind.InsertLambdaWithMultiScopeCapture, "x1", "lambda", "x0", "x1"));
}
[Fact]
public void Lambdas_Insert_ForEach2()
{
var src1 = @"
using System;
class C
{
void G(Func<int, int> f1, Func<int, int> f2, Func<int, int> f3) {}
void F()
{
int x0 = 0; // Group #0
foreach (int x1 in new[] { 1 }) // Group #1
G(a => x0, a => x1, null);
}
}
";
var src2 = @"
using System;
class C
{
void G(Func<int, int> f1, Func<int, int> f2, Func<int, int> f3) {}
void F()
{
int x0 = 0; // Group #0
foreach (int x1 in new[] { 1 }) // Group #1
G(a => x0, a => x1, a => x0 + x1); // error: connecting previously disconnected closures
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics(
Diagnostic(RudeEditKind.InsertLambdaWithMultiScopeCapture, "x1", "lambda", "x0", "x1"));
}
[Fact]
public void Lambdas_Insert_For1()
{
var src1 = @"
using System;
class C
{
bool G(Func<int, int> f) => true;
void F()
{
for (int x0 = 0, x1 = 0; G(a => x0) && G(a => x1);)
{
int x2 = 0;
G(a => x2);
}
}
}
";
var src2 = @"
using System;
class C
{
bool G(Func<int, int> f) => true;
void F()
{
for (int x0 = 0, x1 = 0; G(a => x0) && G(a => x1);)
{
int x2 = 0;
G(a => x2);
G(a => x0 + x1); // ok
G(a => x0 + x2); // error: connecting previously disconnected closures
}
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics(
Diagnostic(RudeEditKind.InsertLambdaWithMultiScopeCapture, "x2", "lambda", "x0", "x2"));
}
[Fact]
public void Lambdas_Insert_Switch1()
{
var src1 = @"
using System;
class C
{
bool G(Func<int> f) => true;
int a = 1;
void F()
{
int x2 = 1;
G(() => x2);
switch (a)
{
case 1:
int x0 = 1;
G(() => x0);
break;
case 2:
int x1 = 1;
G(() => x1);
break;
}
}
}
";
var src2 = @"
using System;
class C
{
bool G(Func<int> f) => true;
int a = 1;
void F()
{
int x2 = 1;
G(() => x2);
switch (a)
{
case 1:
int x0 = 1;
G(() => x0);
goto case 2;
case 2:
int x1 = 1;
G(() => x1);
goto default;
default:
x0 = 1;
x1 = 2;
G(() => x0 + x1); // ok
G(() => x0 + x2); // error
break;
}
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics(
Diagnostic(RudeEditKind.InsertLambdaWithMultiScopeCapture, "x0", "lambda", "x2", "x0"));
}
[Fact]
public void Lambdas_Insert_Using1()
{
var src1 = @"
using System;
class C
{
static bool G<T>(Func<T> f) => true;
static int F(object a, object b) => 1;
static IDisposable D() => null;
static void F()
{
using (IDisposable x0 = D(), y0 = D())
{
int x1 = 1;
G(() => x0);
G(() => y0);
G(() => x1);
}
}
}
";
var src2 = @"
using System;
class C
{
static bool G<T>(Func<T> f) => true;
static int F(object a, object b) => 1;
static IDisposable D() => null;
static void F()
{
using (IDisposable x0 = D(), y0 = D())
{
int x1 = 1;
G(() => x0);
G(() => y0);
G(() => x1);
G(() => F(x0, y0)); // ok
G(() => F(x0, x1)); // error
}
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics(
Diagnostic(RudeEditKind.InsertLambdaWithMultiScopeCapture, "x1", "lambda", "x0", "x1"));
}
[Fact]
public void Lambdas_Insert_Catch1()
{
var src1 = @"
using System;
class C
{
static bool G<T>(Func<T> f) => true;
static int F(object a, object b) => 1;
static void F()
{
try
{
}
catch (Exception x0)
{
int x1 = 1;
G(() => x0);
G(() => x1);
}
}
}
";
var src2 = @"
using System;
class C
{
static bool G<T>(Func<T> f) => true;
static int F(object a, object b) => 1;
static void F()
{
try
{
}
catch (Exception x0)
{
int x1 = 1;
G(() => x0);
G(() => x1);
G(() => x0); //ok
G(() => F(x0, x1)); //error
}
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics(
Diagnostic(RudeEditKind.InsertLambdaWithMultiScopeCapture, "x1", "lambda", "x0", "x1"));
}
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/1504"), WorkItem(1504)]
public void Lambdas_Insert_CatchFilter1()
{
var src1 = @"
using System;
class C
{
static bool G<T>(Func<T> f) => true;
static void F()
{
Exception x1 = null;
try
{
G(() => x1);
}
catch (Exception x0) when (G(() => x0))
{
}
}
}
";
var src2 = @"
using System;
class C
{
static bool G<T>(Func<T> f) => true;
static void F()
{
Exception x1 = null;
try
{
G(() => x1);
}
catch (Exception x0) when (G(() => x0) &&
G(() => x0) && // ok
G(() => x0 != x1)) // error
{
G(() => x0); // ok
}
}
}
";
var edits = GetTopEdits(src1, src2);
edits.VerifySemanticDiagnostics();
}
[Fact] [Fact]
public void Lambdas_Update_CeaseCapture_This() public void Lambdas_Update_CeaseCapture_This()
{ {
......
...@@ -1079,6 +1079,7 @@ internal static TextSpan GetDiagnosticSpanImpl(SyntaxKind kind, SyntaxNode node, ...@@ -1079,6 +1079,7 @@ internal static TextSpan GetDiagnosticSpanImpl(SyntaxKind kind, SyntaxNode node,
case SyntaxKind.CatchClause: case SyntaxKind.CatchClause:
return ((CatchClauseSyntax)node).CatchKeyword.Span; return ((CatchClauseSyntax)node).CatchKeyword.Span;
case SyntaxKind.CatchDeclaration:
case SyntaxKind.CatchFilterClause: case SyntaxKind.CatchFilterClause:
return node.Span; return node.Span;
...@@ -1343,6 +1344,7 @@ internal static string GetStatementDisplayNameImpl(SyntaxNode node) ...@@ -1343,6 +1344,7 @@ internal static string GetStatementDisplayNameImpl(SyntaxNode node)
return CSharpFeaturesResources.TryBlock; return CSharpFeaturesResources.TryBlock;
case SyntaxKind.CatchClause: case SyntaxKind.CatchClause:
case SyntaxKind.CatchDeclaration:
return CSharpFeaturesResources.CatchClause; return CSharpFeaturesResources.CatchClause;
case SyntaxKind.CatchFilterClause: case SyntaxKind.CatchFilterClause:
......
...@@ -157,6 +157,7 @@ internal enum Label ...@@ -157,6 +157,7 @@ internal enum Label
TryStatement, TryStatement,
CatchClause, // tied to parent CatchClause, // tied to parent
CatchDeclaration, // tied to parent
CatchFilterClause, // tied to parent CatchFilterClause, // tied to parent
FinallyClause, // tied to parent FinallyClause, // tied to parent
ForStatement, ForStatement,
...@@ -223,6 +224,7 @@ private static int TiedToAncestor(Label label) ...@@ -223,6 +224,7 @@ private static int TiedToAncestor(Label label)
case Label.BreakContinueStatement: case Label.BreakContinueStatement:
case Label.ElseClause: case Label.ElseClause:
case Label.CatchClause: case Label.CatchClause:
case Label.CatchDeclaration:
case Label.CatchFilterClause: case Label.CatchFilterClause:
case Label.FinallyClause: case Label.FinallyClause:
case Label.ForStatementPart: case Label.ForStatementPart:
...@@ -366,6 +368,10 @@ internal static Label Classify(SyntaxKind kind, SyntaxNode nodeOpt, out bool isL ...@@ -366,6 +368,10 @@ internal static Label Classify(SyntaxKind kind, SyntaxNode nodeOpt, out bool isL
case SyntaxKind.CatchClause: case SyntaxKind.CatchClause:
return Label.CatchClause; return Label.CatchClause;
case SyntaxKind.CatchDeclaration:
// the declarator of the exception variable
return Label.CatchDeclaration;
case SyntaxKind.CatchFilterClause: case SyntaxKind.CatchFilterClause:
return Label.CatchFilterClause; return Label.CatchFilterClause;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册