未验证 提交 74b2ea28 编写于 作者: J Joey Robichaud 提交者: GitHub

Classify constructor/destructor as containing type (#32912)

* Classify Constructors and Destructors as their containing type

* Updated classifier test

* Added tests for symbol display parts

* Added tests for static constructor and destructor
上级 19623b8d
...@@ -351,122 +351,142 @@ public override void VisitMethod(IMethodSymbol symbol) ...@@ -351,122 +351,142 @@ public override void VisitMethod(IMethodSymbol symbol)
case MethodKind.Ordinary: case MethodKind.Ordinary:
case MethodKind.DelegateInvoke: case MethodKind.DelegateInvoke:
case MethodKind.LocalFunction: case MethodKind.LocalFunction:
//containing type will be the delegate type, name will be Invoke {
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.Name)); //containing type will be the delegate type, name will be Invoke
break; builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.Name));
break;
}
case MethodKind.ReducedExtension: case MethodKind.ReducedExtension:
// Note: Extension methods invoked off of their static class will be tagged as methods. {
// This behavior matches the semantic classification done in NameSyntaxClassifier. // Note: Extension methods invoked off of their static class will be tagged as methods.
builder.Add(CreatePart(SymbolDisplayPartKind.ExtensionMethodName, symbol, symbol.Name)); // This behavior matches the semantic classification done in NameSyntaxClassifier.
break; builder.Add(CreatePart(SymbolDisplayPartKind.ExtensionMethodName, symbol, symbol.Name));
break;
}
case MethodKind.PropertyGet: case MethodKind.PropertyGet:
case MethodKind.PropertySet: case MethodKind.PropertySet:
isAccessor = true;
var associatedProperty = (IPropertySymbol)symbol.AssociatedSymbol;
if (associatedProperty == null)
{ {
goto case MethodKind.Ordinary; isAccessor = true;
var associatedProperty = (IPropertySymbol)symbol.AssociatedSymbol;
if (associatedProperty == null)
{
goto case MethodKind.Ordinary;
}
AddPropertyNameAndParameters(associatedProperty);
AddPunctuation(SyntaxKind.DotToken);
AddKeyword(symbol.MethodKind == MethodKind.PropertyGet ? SyntaxKind.GetKeyword : SyntaxKind.SetKeyword);
break;
} }
AddPropertyNameAndParameters(associatedProperty);
AddPunctuation(SyntaxKind.DotToken);
AddKeyword(symbol.MethodKind == MethodKind.PropertyGet ? SyntaxKind.GetKeyword : SyntaxKind.SetKeyword);
break;
case MethodKind.EventAdd: case MethodKind.EventAdd:
case MethodKind.EventRemove: case MethodKind.EventRemove:
isAccessor = true;
var associatedEvent = (IEventSymbol)symbol.AssociatedSymbol;
if (associatedEvent == null)
{ {
goto case MethodKind.Ordinary; isAccessor = true;
var associatedEvent = (IEventSymbol)symbol.AssociatedSymbol;
if (associatedEvent == null)
{
goto case MethodKind.Ordinary;
}
AddEventName(associatedEvent);
AddPunctuation(SyntaxKind.DotToken);
AddKeyword(symbol.MethodKind == MethodKind.EventAdd ? SyntaxKind.AddKeyword : SyntaxKind.RemoveKeyword);
break;
} }
AddEventName(associatedEvent);
AddPunctuation(SyntaxKind.DotToken);
AddKeyword(symbol.MethodKind == MethodKind.EventAdd ? SyntaxKind.AddKeyword : SyntaxKind.RemoveKeyword);
break;
case MethodKind.Constructor: case MethodKind.Constructor:
case MethodKind.StaticConstructor: case MethodKind.StaticConstructor:
// Note: we are using the metadata name also in the case that
// symbol.containingType is null (which should never be the case here) or is an
// anonymous type (which 'does not have a name').
var name = format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames) || symbol.ContainingType == null || symbol.ContainingType.IsAnonymousType
? symbol.Name
: symbol.ContainingType.Name;
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, name));
break;
case MethodKind.Destructor:
// Note: we are using the metadata name also in the case that symbol.containingType is null, which should never be the case here.
if (format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames) || symbol.ContainingType == null)
{ {
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.Name)); // Note: we are using the metadata name also in the case that
// symbol.containingType is null (which should never be the case here) or is an
// anonymous type (which 'does not have a name').
var name = format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames) || symbol.ContainingType == null || symbol.ContainingType.IsAnonymousType
? symbol.Name
: symbol.ContainingType.Name;
var partKind = GetPartKindForConstructorOrDestructor(symbol);
builder.Add(CreatePart(partKind, symbol, name));
break;
} }
else case MethodKind.Destructor:
{ {
AddPunctuation(SyntaxKind.TildeToken); var partKind = GetPartKindForConstructorOrDestructor(symbol);
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.ContainingType.Name));
// Note: we are using the metadata name also in the case that symbol.containingType is null, which should never be the case here.
if (format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames) || symbol.ContainingType == null)
{
builder.Add(CreatePart(partKind, symbol, symbol.Name));
}
else
{
AddPunctuation(SyntaxKind.TildeToken);
builder.Add(CreatePart(partKind, symbol, symbol.ContainingType.Name));
}
break;
} }
break;
case MethodKind.ExplicitInterfaceImplementation: case MethodKind.ExplicitInterfaceImplementation:
AddExplicitInterfaceIfRequired(symbol.ExplicitInterfaceImplementations);
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol,
ExplicitInterfaceHelpers.GetMemberNameWithoutInterfaceName(symbol.Name)));
break;
case MethodKind.UserDefinedOperator:
case MethodKind.BuiltinOperator:
if (format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames))
{ {
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.MetadataName)); AddExplicitInterfaceIfRequired(symbol.ExplicitInterfaceImplementations);
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol,
ExplicitInterfaceHelpers.GetMemberNameWithoutInterfaceName(symbol.Name)));
break;
} }
else case MethodKind.UserDefinedOperator:
case MethodKind.BuiltinOperator:
{ {
AddKeyword(SyntaxKind.OperatorKeyword); if (format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames))
AddSpace();
if (symbol.MetadataName == WellKnownMemberNames.TrueOperatorName)
{ {
AddKeyword(SyntaxKind.TrueKeyword); builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.MetadataName));
}
else if (symbol.MetadataName == WellKnownMemberNames.FalseOperatorName)
{
AddKeyword(SyntaxKind.FalseKeyword);
} }
else else
{ {
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, AddKeyword(SyntaxKind.OperatorKeyword);
SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(symbol.MetadataName)))); AddSpace();
if (symbol.MetadataName == WellKnownMemberNames.TrueOperatorName)
{
AddKeyword(SyntaxKind.TrueKeyword);
}
else if (symbol.MetadataName == WellKnownMemberNames.FalseOperatorName)
{
AddKeyword(SyntaxKind.FalseKeyword);
}
else
{
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol,
SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(symbol.MetadataName))));
}
} }
break;
} }
break;
case MethodKind.Conversion: case MethodKind.Conversion:
if (format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames))
{ {
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.MetadataName)); if (format.CompilerInternalOptions.IncludesOption(SymbolDisplayCompilerInternalOptions.UseMetadataMethodNames))
}
else
{
// "System.IntPtr.explicit operator System.IntPtr(int)"
if (symbol.MetadataName == WellKnownMemberNames.ExplicitConversionName)
{ {
AddKeyword(SyntaxKind.ExplicitKeyword); builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, symbol.MetadataName));
}
else if (symbol.MetadataName == WellKnownMemberNames.ImplicitConversionName)
{
AddKeyword(SyntaxKind.ImplicitKeyword);
} }
else else
{ {
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol, // "System.IntPtr.explicit operator System.IntPtr(int)"
SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(symbol.MetadataName))));
}
AddSpace(); if (symbol.MetadataName == WellKnownMemberNames.ExplicitConversionName)
AddKeyword(SyntaxKind.OperatorKeyword); {
AddSpace(); AddKeyword(SyntaxKind.ExplicitKeyword);
AddReturnType(symbol); }
} else if (symbol.MetadataName == WellKnownMemberNames.ImplicitConversionName)
break; {
AddKeyword(SyntaxKind.ImplicitKeyword);
}
else
{
builder.Add(CreatePart(SymbolDisplayPartKind.MethodName, symbol,
SyntaxFacts.GetText(SyntaxFacts.GetOperatorKind(symbol.MetadataName))));
}
AddSpace();
AddKeyword(SyntaxKind.OperatorKeyword);
AddSpace();
AddReturnType(symbol);
}
break;
}
default: default:
throw ExceptionUtilities.UnexpectedValue(symbol.MethodKind); throw ExceptionUtilities.UnexpectedValue(symbol.MethodKind);
} }
...@@ -479,6 +499,17 @@ public override void VisitMethod(IMethodSymbol symbol) ...@@ -479,6 +499,17 @@ public override void VisitMethod(IMethodSymbol symbol)
} }
} }
private static SymbolDisplayPartKind GetPartKindForConstructorOrDestructor(IMethodSymbol symbol)
{
// In the case that symbol.containingType is null (which should never be the case here) we will fallback to the MethodName symbol part
if (symbol.ContainingType is null)
{
return SymbolDisplayPartKind.MethodName;
}
return GetPartKind(symbol.ContainingType);
}
private void AddReturnType(IMethodSymbol symbol) private void AddReturnType(IMethodSymbol symbol)
{ {
var methodSymbol = symbol as MethodSymbol; var methodSymbol = symbol as MethodSymbol;
......
...@@ -1298,7 +1298,7 @@ class C { ...@@ -1298,7 +1298,7 @@ class C {
findSymbol, findSymbol,
format, format,
".ctor", ".ctor",
SymbolDisplayPartKind.MethodName); SymbolDisplayPartKind.ClassName);
} }
[Fact] [Fact]
...@@ -6930,5 +6930,161 @@ void M() ...@@ -6930,5 +6930,161 @@ void M()
SymbolDisplayPartKind.Space, SymbolDisplayPartKind.Space,
SymbolDisplayPartKind.Punctuation); // _ SymbolDisplayPartKind.Punctuation); // _
} }
[Fact]
public void ClassConstructorDeclaration()
{
TestSymbolDescription(
@"class C
{
C() { }
}",
global => global.GetTypeMember("C").Constructors[0],
new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType),
"C.C",
SymbolDisplayPartKind.ClassName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.ClassName);
}
[Fact]
public void ClassDestructorDeclaration()
{
TestSymbolDescription(
@"class C
{
~C() { }
}",
global => global.GetTypeMember("C").GetMember("Finalize"),
new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType),
"C.~C",
SymbolDisplayPartKind.ClassName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.ClassName);
}
[Fact]
public void ClassStaticConstructorDeclaration()
{
TestSymbolDescription(
@"class C
{
static C() { }
}",
global => global.GetTypeMember("C").Constructors[0],
new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType),
"C.C",
SymbolDisplayPartKind.ClassName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.ClassName);
}
[Fact]
public void ClassStaticDestructorDeclaration()
{
TestSymbolDescription(
@"class C
{
static ~C() { }
}",
global => global.GetTypeMember("C").GetMember("Finalize"),
new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType),
"C.~C",
SymbolDisplayPartKind.ClassName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.ClassName);
}
[Fact]
public void ClassConstructorInvocation()
{
var format = new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);
var source =
@"class C
{
C()
{
var c = new C();
}
}";
var compilation = CreateCompilation(source);
var tree = compilation.SyntaxTrees[0];
var model = compilation.GetSemanticModel(tree);
var constructor = tree.GetRoot().DescendantNodes().OfType<ObjectCreationExpressionSyntax>().Single();
var symbol = model.GetSymbolInfo(constructor).Symbol;
Verify(
symbol.ToMinimalDisplayParts(model, constructor.SpanStart, format),
"C.C",
SymbolDisplayPartKind.ClassName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.ClassName);
}
[Fact]
public void StructConstructorDeclaration()
{
TestSymbolDescription(
@"struct S
{
int i;
S(int i)
{
this.i = i;
}
}",
global => global.GetTypeMember("S").Constructors[0],
new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType),
"S.S",
SymbolDisplayPartKind.StructName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.StructName);
}
[Fact]
public void StructConstructorInvocation()
{
var format = new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);
var source =
@"struct S
{
int i;
public S(int i)
{
this.i = i;
}
}
class C
{
C()
{
var s = new S(1);
}
}";
var compilation = CreateCompilation(source);
var tree = compilation.SyntaxTrees[0];
var model = compilation.GetSemanticModel(tree);
var constructor = tree.GetRoot().DescendantNodes().OfType<ObjectCreationExpressionSyntax>().Single();
var symbol = model.GetSymbolInfo(constructor).Symbol;
Verify(
symbol.ToMinimalDisplayParts(model, constructor.SpanStart, format),
"S.S",
SymbolDisplayPartKind.StructName,
SymbolDisplayPartKind.Punctuation,
SymbolDisplayPartKind.StructName);
}
} }
} }
...@@ -581,10 +581,8 @@ class Derived : Base ...@@ -581,10 +581,8 @@ class Derived : Base
Namespace("System"), Namespace("System"),
Class("Base"), Class("Base"),
Class("My"), Class("My"),
Method("My"),
Class("Derived"), Class("Derived"),
Class("My"), Class("My"),
Method("My"),
Class("Attribute"), Class("Attribute"),
Class("Base")); Class("Base"));
} }
...@@ -1157,6 +1155,37 @@ class Nested { } ...@@ -1157,6 +1155,37 @@ class Nested { }
Class("String")); Class("String"));
} }
[Fact, Trait(Traits.Feature, Traits.Features.Classification)]
public async Task Constructors()
{
await TestAsync(
@"struct S
{
public int i;
public S(int i)
{
this.i = i;
}
}
class C
{
public C()
{
var s = new S(1);
var c = new C();
}
}",
Field("i"),
Parameter("i"),
Keyword("var"),
Struct("S"),
Keyword("var"),
Class("C"));
}
[Fact, Trait(Traits.Feature, Traits.Features.Classification)] [Fact, Trait(Traits.Feature, Traits.Features.Classification)]
public async Task TypesOfClassMembers() public async Task TypesOfClassMembers()
{ {
...@@ -2408,7 +2437,7 @@ class MyClass ...@@ -2408,7 +2437,7 @@ class MyClass
public MyClass(int x) public MyClass(int x)
{ {
} }
}", Method("MyClass")); }", Class("MyClass"));
} }
[WorkItem(633, "https://github.com/dotnet/roslyn/issues/633")] [WorkItem(633, "https://github.com/dotnet/roslyn/issues/633")]
...@@ -2426,7 +2455,7 @@ public MyClass(int x) ...@@ -2426,7 +2455,7 @@ public MyClass(int x)
} }
}", }",
Class("MyClass"), Class("MyClass"),
Method("MyClass")); Class("MyClass"));
} }
[WorkItem(13174, "https://github.com/dotnet/roslyn/issues/13174")] [WorkItem(13174, "https://github.com/dotnet/roslyn/issues/13174")]
......
...@@ -2104,7 +2104,7 @@ public async Task AttributeTargetSpecifiersOnCtor() ...@@ -2104,7 +2104,7 @@ public async Task AttributeTargetSpecifiersOnCtor()
Punctuation.Colon, Punctuation.Colon,
Identifier("A"), Identifier("A"),
Punctuation.CloseBracket, Punctuation.CloseBracket,
Method("C"), Class("C"),
Punctuation.OpenParen, Punctuation.OpenParen,
Punctuation.CloseParen, Punctuation.CloseParen,
Punctuation.OpenCurly, Punctuation.OpenCurly,
...@@ -2132,7 +2132,7 @@ public async Task AttributeTargetSpecifiersOnDtor() ...@@ -2132,7 +2132,7 @@ public async Task AttributeTargetSpecifiersOnDtor()
Identifier("A"), Identifier("A"),
Punctuation.CloseBracket, Punctuation.CloseBracket,
Operators.Tilde, Operators.Tilde,
Method("C"), Class("C"),
Punctuation.OpenParen, Punctuation.OpenParen,
Punctuation.CloseParen, Punctuation.CloseParen,
Punctuation.OpenCurly, Punctuation.OpenCurly,
...@@ -2679,7 +2679,7 @@ interface Bar ...@@ -2679,7 +2679,7 @@ interface Bar
Punctuation.OpenCurly, Punctuation.OpenCurly,
Punctuation.CloseCurly, Punctuation.CloseCurly,
Keyword("public"), Keyword("public"),
Method("Goo"), Class("Goo"),
Punctuation.OpenParen, Punctuation.OpenParen,
Keyword("int"), Keyword("int"),
Parameter("i"), Parameter("i"),
...@@ -2956,7 +2956,7 @@ interface Bar ...@@ -2956,7 +2956,7 @@ interface Bar
Field("field"), Field("field"),
Punctuation.Semicolon, Punctuation.Semicolon,
Keyword("public"), Keyword("public"),
Method("Baz"), Class("Baz"),
Punctuation.OpenParen, Punctuation.OpenParen,
Keyword("int"), Keyword("int"),
Parameter("i"), Parameter("i"),
......
...@@ -148,7 +148,7 @@ public async Task VarAsConstructorName() ...@@ -148,7 +148,7 @@ public async Task VarAsConstructorName()
Keyword("class"), Keyword("class"),
Class("var"), Class("var"),
Punctuation.OpenCurly, Punctuation.OpenCurly,
Method("var"), Class("var"),
Punctuation.OpenParen, Punctuation.OpenParen,
Punctuation.CloseParen, Punctuation.CloseParen,
Punctuation.OpenCurly, Punctuation.OpenCurly,
...@@ -741,7 +741,7 @@ public MyClass(int x) ...@@ -741,7 +741,7 @@ public MyClass(int x)
XmlDoc.AttributeQuotes("\""), XmlDoc.AttributeQuotes("\""),
Class("MyClass"), Class("MyClass"),
Operators.Dot, Operators.Dot,
Method("MyClass"), Class("MyClass"),
Punctuation.OpenParen, Punctuation.OpenParen,
Keyword("int"), Keyword("int"),
Punctuation.CloseParen, Punctuation.CloseParen,
...@@ -756,7 +756,7 @@ public MyClass(int x) ...@@ -756,7 +756,7 @@ public MyClass(int x)
Class("MyClass"), Class("MyClass"),
Punctuation.OpenCurly, Punctuation.OpenCurly,
Keyword("public"), Keyword("public"),
Method("MyClass"), Class("MyClass"),
Punctuation.OpenParen, Punctuation.OpenParen,
Keyword("int"), Keyword("int"),
Parameter("x"), Parameter("x"),
......
...@@ -707,7 +707,7 @@ public async Task DynamicAsConstructorDeclarationName() ...@@ -707,7 +707,7 @@ public async Task DynamicAsConstructorDeclarationName()
Keyword("class"), Keyword("class"),
Class("dynamic"), Class("dynamic"),
Punctuation.OpenCurly, Punctuation.OpenCurly,
Method("dynamic"), Class("dynamic"),
Punctuation.OpenParen, Punctuation.OpenParen,
Punctuation.CloseParen, Punctuation.CloseParen,
Punctuation.OpenCurly, Punctuation.OpenCurly,
......
...@@ -206,11 +206,15 @@ private static string GetClassificationForIdentifier(SyntaxToken token) ...@@ -206,11 +206,15 @@ private static string GetClassificationForIdentifier(SyntaxToken token)
} }
else if (token.Parent is ConstructorDeclarationSyntax constructorDeclaration && constructorDeclaration.Identifier == token) else if (token.Parent is ConstructorDeclarationSyntax constructorDeclaration && constructorDeclaration.Identifier == token)
{ {
return ClassificationTypeNames.MethodName; return constructorDeclaration.IsParentKind(SyntaxKind.ClassDeclaration)
? ClassificationTypeNames.ClassName
: ClassificationTypeNames.StructName;
} }
else if (token.Parent is DestructorDeclarationSyntax destructorDeclaration && destructorDeclaration.Identifier == token) else if (token.Parent is DestructorDeclarationSyntax destructorDeclaration && destructorDeclaration.Identifier == token)
{ {
return ClassificationTypeNames.MethodName; return destructorDeclaration.IsParentKind(SyntaxKind.ClassDeclaration)
? ClassificationTypeNames.ClassName
: ClassificationTypeNames.StructName;
} }
else if (token.Parent is LocalFunctionStatementSyntax localFunctionStatement && localFunctionStatement.Identifier == token) else if (token.Parent is LocalFunctionStatementSyntax localFunctionStatement && localFunctionStatement.Identifier == token)
{ {
......
...@@ -257,6 +257,14 @@ private static string GetClassificationForLocal(ILocalSymbol localSymbol) ...@@ -257,6 +257,14 @@ private static string GetClassificationForLocal(ILocalSymbol localSymbol)
private static string GetClassificationForMethod(IMethodSymbol methodSymbol) private static string GetClassificationForMethod(IMethodSymbol methodSymbol)
{ {
// Classify constructors by their containing type. We do not need to worry about
// destructors because their declaration is handled by syntactic classification
// and they cannot be invoked, so their is no usage to semantically classify.
if (methodSymbol.MethodKind == MethodKind.Constructor)
{
return methodSymbol.ContainingType?.GetClassification() ?? ClassificationTypeNames.MethodName;
}
// Note: We only classify an extension method if it is in reduced form. // Note: We only classify an extension method if it is in reduced form.
// If an extension method is called as a static method invocation (e.g. Enumerable.Select(...)), // If an extension method is called as a static method invocation (e.g. Enumerable.Select(...)),
// it is classified as an ordinary method. // it is classified as an ordinary method.
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册