未验证 提交 a5a9e4fe 编写于 作者: I Ivan Basov 提交者: GitHub

Reparse type after async (#32983)

上级 530518c2
......@@ -2276,124 +2276,139 @@ private MemberDeclarationSyntax ParseMemberDeclarationOrStatementCore(SyntaxKind
// indexers, and non-conversion operators -- starts with a type
// (possibly void). Parse that.
TypeSyntax type = ParseReturnType();
var sawRef = type.Kind == SyntaxKind.RefType;
// Check for misplaced modifiers. if we see any, then consider this member
// terminated and restart parsing.
if (GetModifier(this.CurrentToken) != DeclarationModifiers.None &&
this.CurrentToken.ContextualKind != SyntaxKind.PartialKeyword &&
this.CurrentToken.ContextualKind != SyntaxKind.AsyncKeyword &&
IsComplete(type))
{
var misplacedModifier = this.CurrentToken;
type = this.AddError(
type,
type.FullWidth + misplacedModifier.GetLeadingTriviaWidth(),
misplacedModifier.Width,
ErrorCode.ERR_BadModifierLocation,
misplacedModifier.Text);
return _syntaxFactory.IncompleteMember(attributes, modifiers.ToList(), type);
}
var afterTypeResetPoint = this.GetResetPoint();
parse_member_name:;
// If we've seen the ref keyword, we know we must have an indexer, method, or property.
if (!sawRef)
try
{
// Check here for operators
// Allow old-style implicit/explicit casting operator syntax, just so we can give a better error
if (IsOperatorKeyword())
var sawRef = type.Kind == SyntaxKind.RefType;
// Check for misplaced modifiers. if we see any, then consider this member
// terminated and restart parsing.
if (GetModifier(this.CurrentToken) != DeclarationModifiers.None &&
this.CurrentToken.ContextualKind != SyntaxKind.PartialKeyword &&
this.CurrentToken.ContextualKind != SyntaxKind.AsyncKeyword &&
IsComplete(type))
{
return this.ParseOperatorDeclaration(attributes, modifiers, type);
var misplacedModifier = this.CurrentToken;
type = this.AddError(
type,
type.FullWidth + misplacedModifier.GetLeadingTriviaWidth(),
misplacedModifier.Width,
ErrorCode.ERR_BadModifierLocation,
misplacedModifier.Text);
return _syntaxFactory.IncompleteMember(attributes, modifiers.ToList(), type);
}
if (IsFieldDeclaration(isEvent: false))
parse_member_name:;
// If we've seen the ref keyword, we know we must have an indexer, method, or property.
if (!sawRef)
{
if (acceptStatement)
// Check here for operators
// Allow old-style implicit/explicit casting operator syntax, just so we can give a better error
if (IsOperatorKeyword())
{
// if we are script at top-level then statements can occur
_termState |= TerminatorState.IsPossibleStatementStartOrStop;
return this.ParseOperatorDeclaration(attributes, modifiers, type);
}
return this.ParseNormalFieldDeclaration(attributes, modifiers, type, parentKind);
if (IsFieldDeclaration(isEvent: false))
{
if (acceptStatement)
{
// if we are script at top-level then statements can occur
_termState |= TerminatorState.IsPossibleStatementStartOrStop;
}
return this.ParseNormalFieldDeclaration(attributes, modifiers, type, parentKind);
}
}
}
// At this point we can either have indexers, methods, or
// properties (or something unknown). Try to break apart
// the following name and determine what to do from there.
ExplicitInterfaceSpecifierSyntax explicitInterfaceOpt;
SyntaxToken identifierOrThisOpt;
TypeParameterListSyntax typeParameterListOpt;
this.ParseMemberName(out explicitInterfaceOpt, out identifierOrThisOpt, out typeParameterListOpt, isEvent: false);
// At this point we can either have indexers, methods, or
// properties (or something unknown). Try to break apart
// the following name and determine what to do from there.
ExplicitInterfaceSpecifierSyntax explicitInterfaceOpt;
SyntaxToken identifierOrThisOpt;
TypeParameterListSyntax typeParameterListOpt;
this.ParseMemberName(out explicitInterfaceOpt, out identifierOrThisOpt, out typeParameterListOpt, isEvent: false);
// First, check if we got absolutely nothing. If so, then
// We need to consume a bad member and try again.
if (explicitInterfaceOpt == null && identifierOrThisOpt == null && typeParameterListOpt == null)
{
if (attributes.Count == 0 && modifiers.Count == 0 && type.IsMissing && !sawRef)
// First, check if we got absolutely nothing. If so, then
// We need to consume a bad member and try again.
if (explicitInterfaceOpt == null && identifierOrThisOpt == null && typeParameterListOpt == null)
{
// we haven't advanced, the caller needs to consume the tokens ahead
return null;
if (attributes.Count == 0 && modifiers.Count == 0 && type.IsMissing && !sawRef)
{
// we haven't advanced, the caller needs to consume the tokens ahead
return null;
}
var incompleteMember = _syntaxFactory.IncompleteMember(attributes, modifiers.ToList(), type.IsMissing ? null : type);
if (incompleteMember.ContainsDiagnostics)
{
return incompleteMember;
}
else if (parentKind == SyntaxKind.NamespaceDeclaration ||
parentKind == SyntaxKind.CompilationUnit && !IsScript)
{
return this.AddErrorToLastToken(incompleteMember, ErrorCode.ERR_NamespaceUnexpected);
}
else
{
//the error position should indicate CurrentToken
return this.AddError(
incompleteMember,
incompleteMember.FullWidth + this.CurrentToken.GetLeadingTriviaWidth(),
this.CurrentToken.Width,
ErrorCode.ERR_InvalidMemberDecl,
this.CurrentToken.Text);
}
}
var incompleteMember = _syntaxFactory.IncompleteMember(attributes, modifiers.ToList(), type.IsMissing ? null : type);
if (incompleteMember.ContainsDiagnostics)
// If the modifiers did not include "async", and the type we got was "async", and there was an
// error in the identifier or its type parameters, then the user is probably in the midst of typing
// an async method. In that case we reconsider "async" to be a modifier, and treat the identifier
// (with the type parameters) as the type (with type arguments). Then we go back to looking for
// the member name again.
// For example, if we get
// async Task<
// then we want async to be a modifier and Task<MISSING> to be a type.
if (!sawRef &&
identifierOrThisOpt != null &&
(typeParameterListOpt != null && typeParameterListOpt.ContainsDiagnostics
|| this.CurrentToken.Kind != SyntaxKind.OpenParenToken && this.CurrentToken.Kind != SyntaxKind.OpenBraceToken && this.CurrentToken.Kind != SyntaxKind.EqualsGreaterThanToken) &&
ReconsiderTypeAsAsyncModifier(ref modifiers, type, identifierOrThisOpt))
{
return incompleteMember;
this.Reset(ref afterTypeResetPoint);
explicitInterfaceOpt = null;
identifierOrThisOpt = default;
typeParameterListOpt = null;
type = ParseReturnType();
goto parse_member_name;
}
else if (parentKind == SyntaxKind.NamespaceDeclaration ||
parentKind == SyntaxKind.CompilationUnit && !IsScript)
Debug.Assert(identifierOrThisOpt != null);
if (identifierOrThisOpt.Kind == SyntaxKind.ThisKeyword)
{
return this.AddErrorToLastToken(incompleteMember, ErrorCode.ERR_NamespaceUnexpected);
return this.ParseIndexerDeclaration(attributes, modifiers, type, explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt);
}
else
{
//the error position should indicate CurrentToken
return this.AddError(
incompleteMember,
incompleteMember.FullWidth + this.CurrentToken.GetLeadingTriviaWidth(),
this.CurrentToken.Width,
ErrorCode.ERR_InvalidMemberDecl,
this.CurrentToken.Text);
}
}
// If the modifiers did not include "async", and the type we got was "async", and there was an
// error in the identifier or its type parameters, then the user is probably in the midst of typing
// an async method. In that case we reconsider "async" to be a modifier, and treat the identifier
// (with the type parameters) as the type (with type arguments). Then we go back to looking for
// the member name again.
// For example, if we get
// async Task<
// then we want async to be a modifier and Task<MISSING> to be a type.
if (!sawRef &&
identifierOrThisOpt != null &&
(typeParameterListOpt != null && typeParameterListOpt.ContainsDiagnostics
|| this.CurrentToken.Kind != SyntaxKind.OpenParenToken && this.CurrentToken.Kind != SyntaxKind.OpenBraceToken && this.CurrentToken.Kind != SyntaxKind.EqualsGreaterThanToken) &&
ReconsiderTypeAsAsyncModifier(ref modifiers, ref type, ref explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt))
{
goto parse_member_name;
}
Debug.Assert(identifierOrThisOpt != null);
switch (this.CurrentToken.Kind)
{
case SyntaxKind.OpenBraceToken:
case SyntaxKind.EqualsGreaterThanToken:
return this.ParsePropertyDeclaration(attributes, modifiers, type, explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt);
if (identifierOrThisOpt.Kind == SyntaxKind.ThisKeyword)
{
return this.ParseIndexerDeclaration(attributes, modifiers, type, explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt);
default:
// treat anything else as a method.
return this.ParseMethodDeclaration(attributes, modifiers, type, explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt);
}
}
}
else
finally
{
switch (this.CurrentToken.Kind)
{
case SyntaxKind.OpenBraceToken:
case SyntaxKind.EqualsGreaterThanToken:
return this.ParsePropertyDeclaration(attributes, modifiers, type, explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt);
default:
// treat anything else as a method.
return this.ParseMethodDeclaration(attributes, modifiers, type, explicitInterfaceOpt, identifierOrThisOpt, typeParameterListOpt);
}
this.Release(ref afterTypeResetPoint);
}
}
finally
......@@ -2409,10 +2424,8 @@ private MemberDeclarationSyntax ParseMemberDeclarationOrStatementCore(SyntaxKind
// type parameter list
private bool ReconsiderTypeAsAsyncModifier(
ref SyntaxListBuilder modifiers,
ref TypeSyntax type,
ref ExplicitInterfaceSpecifierSyntax explicitInterfaceOpt,
SyntaxToken identifierOrThisOpt,
TypeParameterListSyntax typeParameterListOpt)
TypeSyntax type,
SyntaxToken identifierOrThisOpt)
{
if (type.Kind != SyntaxKind.IdentifierName) return false;
if (identifierOrThisOpt.Kind != SyntaxKind.IdentifierToken) return false;
......@@ -2426,61 +2439,9 @@ private MemberDeclarationSyntax ParseMemberDeclarationOrStatementCore(SyntaxKind
}
modifiers.Add(ConvertToKeyword(identifier));
SimpleNameSyntax newType = typeParameterListOpt == null
? (SimpleNameSyntax)_syntaxFactory.IdentifierName(identifierOrThisOpt)
: _syntaxFactory.GenericName(identifierOrThisOpt, TypeArgumentFromTypeParameters(typeParameterListOpt));
type = (explicitInterfaceOpt == null)
? (TypeSyntax)newType
: _syntaxFactory.QualifiedName(explicitInterfaceOpt.Name, explicitInterfaceOpt.DotToken, newType);
explicitInterfaceOpt = null;
identifierOrThisOpt = default(SyntaxToken);
typeParameterListOpt = default(TypeParameterListSyntax);
return true;
}
private TypeArgumentListSyntax TypeArgumentFromTypeParameters(TypeParameterListSyntax typeParameterList)
{
var types = _pool.AllocateSeparated<TypeSyntax>();
foreach (var p in typeParameterList.Parameters.GetWithSeparators())
{
switch ((SyntaxKind)p.RawKind)
{
case SyntaxKind.TypeParameter:
var typeParameter = (TypeParameterSyntax)p;
var typeArgument = _syntaxFactory.IdentifierName(typeParameter.Identifier);
// NOTE: reverse order of variance keyword and attributes list so they come out in the right order.
if (typeParameter.VarianceKeyword != null)
{
// This only happens in error scenarios, so don't bother to produce a diagnostic about
// having a variance keyword on a type argument.
typeArgument = AddLeadingSkippedSyntax(typeArgument, typeParameter.VarianceKeyword);
}
if (typeParameter.AttributeLists.Node != null)
{
// This only happens in error scenarios, so don't bother to produce a diagnostic about
// having an attribute on a type argument.
typeArgument = AddLeadingSkippedSyntax(typeArgument, typeParameter.AttributeLists.Node);
}
types.Add(typeArgument);
break;
case SyntaxKind.CommaToken:
types.AddSeparator((SyntaxToken)p);
break;
default:
throw ExceptionUtilities.UnexpectedValue(p.RawKind);
}
}
var result = _syntaxFactory.TypeArgumentList(typeParameterList.LessThanToken, types.ToList(), typeParameterList.GreaterThanToken);
_pool.Free(types);
return result;
}
//private bool ReconsiderTypeAsAsyncModifier(ref SyntaxListBuilder modifiers, ref type, ref identifierOrThisOpt, ref typeParameterListOpt))
// {
// goto parse_member_name;
// }
private bool IsFieldDeclaration(bool isEvent)
{
if (this.CurrentToken.Kind != SyntaxKind.IdentifierToken)
......@@ -6569,9 +6530,15 @@ private StatementSyntax ParseStatementCore()
private StatementSyntax ParsePossibleDeclarationOrBadAwaitStatement()
{
ResetPoint resetPointBeforeStatement = this.GetResetPoint();
StatementSyntax result = ParsePossibleDeclarationOrBadAwaitStatement(ref resetPointBeforeStatement);
this.Release(ref resetPointBeforeStatement);
return result;
try
{
StatementSyntax result = ParsePossibleDeclarationOrBadAwaitStatement(ref resetPointBeforeStatement);
return result;
}
finally
{
this.Release(ref resetPointBeforeStatement);
}
}
private StatementSyntax ParsePossibleDeclarationOrBadAwaitStatement(ref ResetPoint resetPointBeforeStatement)
......
......@@ -458,5 +458,287 @@ public void TrashAfterDeclaration()
}
EOF();
}
[Fact]
[WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")]
public void GenericAsyncTask_01()
{
foreach (var options in new[] { TestOptions.Script, TestOptions.Regular })
{
UsingDeclaration("async Task<SomeNamespace.SomeType Method();", options: options,
// (1,1): error CS1073: Unexpected token '('
// async Task<SomeNamespace.SomeType Method();
Diagnostic(ErrorCode.ERR_UnexpectedToken, "async Task<SomeNamespace.SomeType Method").WithArguments("(").WithLocation(1, 1),
// (1,35): error CS1003: Syntax error, ',' expected
// async Task<SomeNamespace.SomeType Method();
Diagnostic(ErrorCode.ERR_SyntaxError, "Method").WithArguments(",", "").WithLocation(1, 35),
// (1,41): error CS1003: Syntax error, '>' expected
// async Task<SomeNamespace.SomeType Method();
Diagnostic(ErrorCode.ERR_SyntaxError, "(").WithArguments(">", "(").WithLocation(1, 41)
);
N(SyntaxKind.IncompleteMember);
{
N(SyntaxKind.AsyncKeyword);
N(SyntaxKind.GenericName);
{
N(SyntaxKind.IdentifierToken, "Task");
N(SyntaxKind.TypeArgumentList);
{
N(SyntaxKind.LessThanToken);
N(SyntaxKind.QualifiedName);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeNamespace");
}
N(SyntaxKind.DotToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeType");
}
}
M(SyntaxKind.CommaToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "Method");
}
M(SyntaxKind.GreaterThanToken);
}
}
}
EOF();
}
}
[Fact]
[WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")]
public void GenericPublicTask_01()
{
foreach (var options in new[] { TestOptions.Script, TestOptions.Regular })
{
UsingDeclaration("public Task<SomeNamespace.SomeType Method();", options: options,
// (1,1): error CS1073: Unexpected token '('
// public Task<SomeNamespace.SomeType Method();
Diagnostic(ErrorCode.ERR_UnexpectedToken, "public Task<SomeNamespace.SomeType Method").WithArguments("(").WithLocation(1, 1),
// (1,36): error CS1003: Syntax error, ',' expected
// public Task<SomeNamespace.SomeType Method();
Diagnostic(ErrorCode.ERR_SyntaxError, "Method").WithArguments(",", "").WithLocation(1, 36),
// (1,42): error CS1003: Syntax error, '>' expected
// public Task<SomeNamespace.SomeType Method();
Diagnostic(ErrorCode.ERR_SyntaxError, "(").WithArguments(">", "(").WithLocation(1, 42)
);
N(SyntaxKind.IncompleteMember);
{
N(SyntaxKind.PublicKeyword);
N(SyntaxKind.GenericName);
{
N(SyntaxKind.IdentifierToken, "Task");
N(SyntaxKind.TypeArgumentList);
{
N(SyntaxKind.LessThanToken);
N(SyntaxKind.QualifiedName);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeNamespace");
}
N(SyntaxKind.DotToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeType");
}
}
M(SyntaxKind.CommaToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "Method");
}
M(SyntaxKind.GreaterThanToken);
}
}
}
EOF();
}
}
[Fact]
[WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")]
public void GenericAsyncTask_02()
{
foreach (var options in new[] { TestOptions.Script, TestOptions.Regular })
{
UsingDeclaration("async Task<SomeNamespace. Method();", options: options,
// (1,1): error CS1073: Unexpected token '('
// async Task<SomeNamespace. Method();
Diagnostic(ErrorCode.ERR_UnexpectedToken, "async Task<SomeNamespace. Method").WithArguments("(").WithLocation(1, 1),
// (1,33): error CS1003: Syntax error, '>' expected
// async Task<SomeNamespace. Method();
Diagnostic(ErrorCode.ERR_SyntaxError, "(").WithArguments(">", "(").WithLocation(1, 33)
);
N(SyntaxKind.IncompleteMember);
{
N(SyntaxKind.AsyncKeyword);
N(SyntaxKind.GenericName);
{
N(SyntaxKind.IdentifierToken, "Task");
N(SyntaxKind.TypeArgumentList);
{
N(SyntaxKind.LessThanToken);
N(SyntaxKind.QualifiedName);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeNamespace");
N(SyntaxKind.DotToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "Method");
}
M(SyntaxKind.GreaterThanToken);
}
}
}
}
}
EOF();
}
}
[Fact]
[WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")]
public void GenericPublicTask_02()
{
foreach (var options in new[] { TestOptions.Script, TestOptions.Regular })
{
UsingDeclaration("public Task<SomeNamespace. Method();", options: options,
// (1,1): error CS1073: Unexpected token '('
// public Task<SomeNamespace. Method();
Diagnostic(ErrorCode.ERR_UnexpectedToken, "public Task<SomeNamespace. Method").WithArguments("(").WithLocation(1, 1),
// (1,34): error CS1003: Syntax error, '>' expected
// public Task<SomeNamespace. Method();
Diagnostic(ErrorCode.ERR_SyntaxError, "(").WithArguments(">", "(").WithLocation(1, 34)
);
N(SyntaxKind.IncompleteMember);
{
N(SyntaxKind.PublicKeyword);
N(SyntaxKind.GenericName);
{
N(SyntaxKind.IdentifierToken, "Task");
N(SyntaxKind.TypeArgumentList);
{
N(SyntaxKind.LessThanToken);
N(SyntaxKind.QualifiedName);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeNamespace");
}
N(SyntaxKind.DotToken);
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "Method");
}
}
M(SyntaxKind.GreaterThanToken);
}
}
}
EOF();
}
}
[Fact]
[WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")]
public void GenericAsyncTask_03()
{
foreach (var options in new[] { TestOptions.Script, TestOptions.Regular })
{
UsingDeclaration("async Task<SomeNamespace.> Method();", options: options,
// (1,26): error CS1001: Identifier expected
// async Task<SomeNamespace.> Method();
Diagnostic(ErrorCode.ERR_IdentifierExpected, ">").WithLocation(1, 26)
);
N(SyntaxKind.MethodDeclaration);
{
N(SyntaxKind.AsyncKeyword);
N(SyntaxKind.GenericName);
{
N(SyntaxKind.IdentifierToken, "Task");
N(SyntaxKind.TypeArgumentList);
{
N(SyntaxKind.LessThanToken);
N(SyntaxKind.QualifiedName);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeNamespace");
}
N(SyntaxKind.DotToken);
M(SyntaxKind.IdentifierName);
{
M(SyntaxKind.IdentifierToken);
}
}
N(SyntaxKind.GreaterThanToken);
}
}
N(SyntaxKind.IdentifierToken, "Method");
N(SyntaxKind.ParameterList);
{
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.CloseParenToken);
}
N(SyntaxKind.SemicolonToken);
}
EOF();
}
}
[Fact]
[WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")]
public void GenericPublicTask_03()
{
foreach (var options in new[] { TestOptions.Script, TestOptions.Regular })
{
UsingDeclaration("public Task<SomeNamespace.> Method();", options: options,
// (1,27): error CS1001: Identifier expected
// public Task<SomeNamespace.> Method();
Diagnostic(ErrorCode.ERR_IdentifierExpected, ">").WithLocation(1, 27)
);
N(SyntaxKind.MethodDeclaration);
{
N(SyntaxKind.PublicKeyword);
N(SyntaxKind.GenericName);
{
N(SyntaxKind.IdentifierToken, "Task");
N(SyntaxKind.TypeArgumentList);
{
N(SyntaxKind.LessThanToken);
N(SyntaxKind.QualifiedName);
{
N(SyntaxKind.IdentifierName);
{
N(SyntaxKind.IdentifierToken, "SomeNamespace");
}
N(SyntaxKind.DotToken);
M(SyntaxKind.IdentifierName);
{
M(SyntaxKind.IdentifierToken);
}
}
N(SyntaxKind.GreaterThanToken);
}
}
N(SyntaxKind.IdentifierToken, "Method");
N(SyntaxKind.ParameterList);
{
N(SyntaxKind.OpenParenToken);
N(SyntaxKind.CloseParenToken);
}
N(SyntaxKind.SemicolonToken);
}
EOF();
}
}
}
}
......@@ -4135,6 +4135,32 @@ class C
End Using
End Function
<WorkItem(11959, "https://github.com/dotnet/roslyn/issues/11959")>
<MemberData(NameOf(AllCompletionImplementations))>
<WpfTheory, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function TestGenericAsyncTaskDeclaration(completionImplementation As CompletionImplementation) As Task
Using state = TestStateFactory.CreateCSharpTestState(completionImplementation,
<Document>
namespace A.B
{
class TestClass { }
}
namespace A
{
class C
{
async Task&lt;A$$ Method()
{ }
}
}
</Document>)
state.SendTypeChars(".")
Await state.AssertSelectedCompletionItem(displayText:="B", isSoftSelected:=True)
End Using
End Function
Private Class MultipleChangeCompletionProvider
Inherits CompletionProvider
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册