未验证 提交 98198ea0 编写于 作者: F Fred Silberberg 提交者: GitHub

Wire GetSpeculativeTypeInfo with nullability information (#36833)

Wire GetSpeculativeTypeInfo with nullability information
......@@ -885,7 +885,7 @@ private ImmutableArray<ParameterSymbol> BindCrefParameters(BaseCrefParameterList
}
/// <remarks>
/// Keep in sync with CSharpSemanticModel.GetSpeculativelyBoundExpression.
/// Keep in sync with CSharpSemanticModel.GetSpeculativelyBoundExpressionWithoutNullability.
/// </remarks>
private TypeSymbol BindCrefParameterOrReturnType(TypeSyntax typeSyntax, MemberCrefSyntax memberCrefSyntax, DiagnosticBag diagnostics)
{
......
......@@ -151,6 +151,18 @@ internal static bool CanGetSemanticInfo(CSharpSyntaxNode node, bool allowNamedAr
/// <param name="cancellationToken">The cancellation token.</param>
internal abstract CSharpTypeInfo GetTypeInfoWorker(CSharpSyntaxNode node, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// Binds the provided expression in the given context.
/// </summary>
/// <param name="position">The position to bind at.</param>
/// <param name="expression">The expression to bind</param>
/// <param name="bindingOption">How to speculatively bind the given expression. If this is <see cref="SpeculativeBindingOption.BindAsTypeOrNamespace"/>
/// then the provided expression should be a <see cref="TypeSyntax"/>.</param>
/// <param name="binder">The binder that was used to bind the given syntax.</param>
/// <param name="crefSymbols">The symbols used in a cref. If this is not default, then the return is null.</param>
/// <returns>The expression that was bound. If <paramref name="crefSymbols"/> is not default, this is null.</returns>
internal abstract BoundExpression GetSpeculativelyBoundExpression(int position, ExpressionSyntax expression, SpeculativeBindingOption bindingOption, out Binder binder, out ImmutableArray<Symbol> crefSymbols);
/// <summary>
/// Gets a list of method or indexed property symbols for a syntax node. This is overridden by various specializations of SemanticModel.
/// It can assume that CheckSyntaxNode and CanGetSemanticInfo have already been called, as well as that named
......@@ -261,7 +273,7 @@ private static BoundExpression GetSpeculativelyBoundExpressionHelper(Binder bind
/// <remarks>
/// Keep in sync with Binder.BindCrefParameterOrReturnType.
/// </remarks>
private BoundExpression GetSpeculativelyBoundExpression(int position, ExpressionSyntax expression, SpeculativeBindingOption bindingOption, out Binder binder, out ImmutableArray<Symbol> crefSymbols)
protected BoundExpression GetSpeculativelyBoundExpressionWithoutNullability(int position, ExpressionSyntax expression, SpeculativeBindingOption bindingOption, out Binder binder, out ImmutableArray<Symbol> crefSymbols)
{
if (expression == null)
{
......
......@@ -27,6 +27,10 @@ internal abstract partial class MemberSemanticModel : CSharpSemanticModel
private readonly Dictionary<SyntaxNode, ImmutableArray<BoundNode>> _guardedNodeMap = new Dictionary<SyntaxNode, ImmutableArray<BoundNode>>();
private Dictionary<SyntaxNode, BoundStatement> _lazyGuardedSynthesizedStatementsMap;
private NullableWalker.SnapshotManager _lazySnapshotManager;
/// <summary>
/// Only used when this is a speculative semantic model.
/// </summary>
private readonly NullableWalker.SnapshotManager _parentSnapshotManagerOpt;
internal readonly Binder RootBinder;
......@@ -63,7 +67,7 @@ internal abstract partial class MemberSemanticModel : CSharpSemanticModel
this.RootBinder = rootBinder.WithAdditionalFlags(GetSemanticModelBinderFlags());
_containingSemanticModelOpt = containingSemanticModelOpt;
_parentSemanticModelOpt = parentSemanticModelOpt;
_lazySnapshotManager = snapshotManagerOpt;
_parentSnapshotManagerOpt = snapshotManagerOpt;
_speculatedPosition = speculatedPosition;
_operationFactory = new Lazy<CSharpOperationFactory>(() => new CSharpOperationFactory(this));
......@@ -135,14 +139,15 @@ internal override MemberSemanticModel GetMemberModel(SyntaxNode node)
return IsInTree(node) ? this : null;
}
protected NullableWalker.SnapshotManager SnapshotManager
{
get
/// <remarks>
/// This will cause the bound node cache to be populated if nullable semantic analysis is enabled.
/// </remarks>
protected NullableWalker.SnapshotManager GetSnapshotManager()
{
EnsureRootBoundForNullabilityIfNecessary();
Debug.Assert(_lazySnapshotManager is object || !Compilation.NullableSemanticAnalysisEnabled);
return _lazySnapshotManager;
}
}
internal sealed override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticModel parentModel, int position, TypeSyntax type, SpeculativeBindingOption bindingOption, out SemanticModel speculativeModel)
{
......@@ -166,6 +171,27 @@ internal sealed override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSeman
return false;
}
internal override BoundExpression GetSpeculativelyBoundExpression(int position, ExpressionSyntax expression, SpeculativeBindingOption bindingOption, out Binder binder, out ImmutableArray<Symbol> crefSymbols)
{
if (expression == null)
{
throw new ArgumentNullException(nameof(expression));
}
if (!Compilation.NullableSemanticAnalysisEnabled || bindingOption != SpeculativeBindingOption.BindAsExpression)
{
return GetSpeculativelyBoundExpressionWithoutNullability(position, expression, bindingOption, out binder, out crefSymbols);
}
crefSymbols = default;
position = CheckAndAdjustPosition(position);
expression = SyntaxFactory.GetStandaloneExpression(expression);
var bindableNewExpression = GetBindableSyntaxNode(expression);
binder = GetEnclosingBinder(position);
var boundRoot = Bind(binder, bindableNewExpression, _ignoredDiagnostics);
return (BoundExpression)NullableWalker.AnalyzeAndRewriteSpeculation(position, boundRoot, binder, GetSnapshotManager(), takeNewSnapshots: false, out _);
}
private Binder GetEnclosingBinderInternalWithinRoot(SyntaxNode node, int position)
{
AssertPositionAdjusted(position);
......@@ -1431,21 +1457,9 @@ protected void GuardedAddBoundTreeForStandaloneSyntax(SyntaxNode syntax, BoundNo
NodeMapBuilder.AddToMap(bound, _guardedNodeMap, syntax);
}
// If we're a speculative model, then we should never be passed a new manager, as we should not be recording
// new snapshot info, only using what was given when the model was created. If we're not a speculative model,
// then there are 3 possibilities:
// 1. Nullable analysis wasn't enabled, and we should never be passed a manager.
// 2. Nullable analysis is enabled, but we're not adding info for a root node and we shouldn't be passed
// new snapshots. This can occur when we're asked to bind a standalone node that wasn't in the original
// tree, such as aliases. These nodes do not affect analysis, so we should not be attempting to save
// snapshot information for use in speculative models created off this one.
// 3. Nullable analysis is enabled, and we were passed a new manager. If this is the case, we must be adding
// cached nodes for the root syntax node, and the existing snapshot manager must be null.
Debug.Assert((IsSpeculativeSemanticModel && manager is null) ||
(!IsSpeculativeSemanticModel &&
(manager is null && (!Compilation.NullableSemanticAnalysisEnabled || syntax != Root)) ||
(manager is object && syntax == Root && Compilation.NullableSemanticAnalysisEnabled && _lazySnapshotManager is null)));
if (!IsSpeculativeSemanticModel && manager is object)
Debug.Assert((manager is null && (!Compilation.NullableSemanticAnalysisEnabled || syntax != Root)) ||
(manager is object && syntax == Root && Compilation.NullableSemanticAnalysisEnabled && _lazySnapshotManager is null));
if (manager is object)
{
_lazySnapshotManager = manager;
}
......@@ -1855,10 +1869,10 @@ protected void EnsureRootBoundForNullabilityIfNecessary()
#endif
}
// If this isn't a speculative model and we have a snapshot manager,
// then we've already done all the work necessary and we should avoid
// taking an unnecessary read lock.
if (!IsSpeculativeSemanticModel && _lazySnapshotManager is object)
// If we have a snapshot manager, then we've already done
// all the work necessary and we should avoid taking an
// unnecessary read lock.
if (_lazySnapshotManager is object)
{
return;
}
......@@ -1890,7 +1904,7 @@ protected void EnsureRootBoundForNullabilityIfNecessary()
}
else
{
bindAndRewrite(takeSnapshots: true);
bindAndRewrite();
}
void ensureSpeculativeNodeBound()
......@@ -1898,20 +1912,19 @@ void ensureSpeculativeNodeBound()
// Not all speculative models are created with existing snapshots. Attributes,
// TypeSyntaxes, and MethodBodies do not depend on existing state in a member,
// and so the SnapshotManager can be null in these cases.
if (_lazySnapshotManager is null)
if (_parentSnapshotManagerOpt is null)
{
bindAndRewrite(takeSnapshots: false);
bindAndRewrite();
return;
}
var analyzedNullabilitiesBuilder = ImmutableDictionary.CreateBuilder<BoundExpression, (NullabilityInfo, TypeSymbol)>();
boundRoot = NullableWalker.AnalyzeAndRewriteSpeculation(_speculatedPosition, boundRoot, binder, _lazySnapshotManager);
GuardedAddBoundTreeForStandaloneSyntax(bindableRoot, boundRoot);
boundRoot = NullableWalker.AnalyzeAndRewriteSpeculation(_speculatedPosition, boundRoot, binder, _parentSnapshotManagerOpt, takeNewSnapshots: true, out var newSnapshots);
GuardedAddBoundTreeForStandaloneSyntax(bindableRoot, boundRoot, newSnapshots);
}
void bindAndRewrite(bool takeSnapshots)
void bindAndRewrite()
{
boundRoot = RewriteNullableBoundNodesWithSnapshots(boundRoot, binder, diagnostics, takeSnapshots, out var snapshotManager);
boundRoot = RewriteNullableBoundNodesWithSnapshots(boundRoot, binder, diagnostics, takeSnapshots: true, out var snapshotManager);
#if DEBUG
// Don't actually cache the results if the nullable analysis is not enabled in debug mode.
if (!Compilation.NullableSemanticAnalysisEnabled) return;
......
......@@ -162,7 +162,7 @@ internal override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSemanticMode
var methodSymbol = (MethodSymbol)this.MemberSymbol;
binder = new ExecutableCodeBinder(statement, methodSymbol, binder);
speculativeModel = CreateSpeculative(parentModel, methodSymbol, statement, binder, SnapshotManager, position);
speculativeModel = CreateSpeculative(parentModel, methodSymbol, statement, binder, GetSnapshotManager(), position);
return true;
}
......
......@@ -718,6 +718,28 @@ internal sealed override bool TryGetSpeculativeSemanticModelCore(SyntaxTreeSeman
return false;
}
internal override BoundExpression GetSpeculativelyBoundExpression(int position, ExpressionSyntax expression, SpeculativeBindingOption bindingOption, out Binder binder, out ImmutableArray<Symbol> crefSymbols)
{
if (expression == null)
{
throw new ArgumentNullException(nameof(expression));
}
// If the given position is in a member that we can get a semantic model for, we want to defer to that implementation
// of GetSpeculativelyBoundExpression so it can take nullability into account.
if (bindingOption == SpeculativeBindingOption.BindAsExpression)
{
position = CheckAndAdjustPosition(position);
var model = GetMemberModel(position);
if (model is object)
{
return model.GetSpeculativelyBoundExpression(position, expression, bindingOption, out binder, out crefSymbols);
}
}
return GetSpeculativelyBoundExpressionWithoutNullability(position, expression, bindingOption, out binder, out crefSymbols);
}
private MemberSemanticModel GetMemberModel(int position)
{
AssertPositionAdjusted(position);
......
......@@ -17,7 +17,8 @@ internal sealed class SnapshotManager
/// <summary>
/// The int key corresponds to <see cref="Snapshot.SharedStateIndex"/>.
/// </summary>
private readonly ImmutableArray<SharedWalkerState> _walkerGlobalStates;
private readonly ImmutableArray<SharedWalkerState> _walkerSharedStates;
/// <summary>
/// The snapshot array should be sorted in ascending order by the position tuple element in order for the binary search algorithm to
/// function correctly.
......@@ -26,9 +27,9 @@ internal sealed class SnapshotManager
private static readonly Func<(int position, Snapshot snapshot), int, int> BinarySearchComparer = (current, target) => current.position.CompareTo(target);
private SnapshotManager(ImmutableArray<SharedWalkerState> walkerGlobalStates, ImmutableArray<(int position, Snapshot snapshot)> incrementalSnapshots)
private SnapshotManager(ImmutableArray<SharedWalkerState> walkerSharedStates, ImmutableArray<(int position, Snapshot snapshot)> incrementalSnapshots)
{
_walkerGlobalStates = walkerGlobalStates;
_walkerSharedStates = walkerSharedStates;
_incrementalSnapshots = incrementalSnapshots;
#if DEBUG
......@@ -43,11 +44,12 @@ private SnapshotManager(ImmutableArray<SharedWalkerState> walkerGlobalStates, Im
#endif
}
internal (NullableWalker, VariableState) RestoreWalkerToAnalyzeNewNode(
internal (NullableWalker, VariableState, Symbol) RestoreWalkerToAnalyzeNewNode(
int position,
BoundNode nodeToAnalyze,
Binder binder,
ImmutableDictionary<BoundExpression, (NullabilityInfo, TypeSymbol)>.Builder analyzedNullabilityMap)
ImmutableDictionary<BoundExpression, (NullabilityInfo, TypeSymbol)>.Builder analyzedNullabilityMap,
SnapshotManager.Builder newManagerOpt)
{
var snapshotPosition = _incrementalSnapshots.BinarySearch(position, BinarySearchComparer);
......@@ -61,11 +63,11 @@ private SnapshotManager(ImmutableArray<SharedWalkerState> walkerGlobalStates, Im
}
(_, Snapshot incrementalSnapshot) = _incrementalSnapshots[snapshotPosition];
var globalState = _walkerGlobalStates[incrementalSnapshot.SharedStateIndex];
var variableState = new VariableState(globalState.VariableSlot, globalState.VariableBySlot, globalState.VariableTypes, incrementalSnapshot.VariableState.Clone());
var method = globalState.Symbol as MethodSymbol;
var sharedState = _walkerSharedStates[incrementalSnapshot.SharedStateIndex];
var variableState = new VariableState(sharedState.VariableSlot, sharedState.VariableBySlot, sharedState.VariableTypes, incrementalSnapshot.VariableState.Clone());
var method = sharedState.Symbol as MethodSymbol;
return (new NullableWalker(binder.Compilation,
globalState.Symbol,
sharedState.Symbol,
useMethodSignatureParameterTypes: !(method is null),
method,
nodeToAnalyze,
......@@ -74,9 +76,10 @@ private SnapshotManager(ImmutableArray<SharedWalkerState> walkerGlobalStates, Im
variableState,
returnTypesOpt: null,
analyzedNullabilityMap,
snapshotBuilderOpt: null,
snapshotBuilderOpt: newManagerOpt,
isSpeculative: true),
variableState);
variableState,
sharedState.Symbol);
}
#if DEBUG
......@@ -94,7 +97,7 @@ internal void VerifyNode(BoundNode node)
{
Debug.Fail($"Did not find a snapshot for {node} `{node.Syntax}.`");
}
Debug.Assert(_walkerGlobalStates.Length > _incrementalSnapshots[position].snapshot.SharedStateIndex, $"Did not find global state for {node} `{node.Syntax}`.");
Debug.Assert(_walkerSharedStates.Length > _incrementalSnapshots[position].snapshot.SharedStateIndex, $"Did not find shared state for {node} `{node.Syntax}`.");
}
#endif
......@@ -108,7 +111,7 @@ internal sealed class Builder
private readonly SortedDictionary<int, Snapshot> _incrementalSnapshots = new SortedDictionary<int, Snapshot>();
/// <summary>
/// Every walker is walking a specific symbol, and can potentially walk each symbol multiple times
/// to get to a stable state. Each of these symbols gets a single global state slot, which this
/// to get to a stable state. Each of these symbols gets a single shared state slot, which this
/// dictionary keeps track of. These slots correspond to indexes into <see cref="_walkerStates"/>.
/// </summary>
private readonly PooledDictionary<Symbol, int> _symbolToSlot = PooledDictionary<Symbol, int>.GetInstance();
......@@ -198,10 +201,10 @@ internal struct SharedWalkerState
internal readonly LocalState VariableState;
internal readonly int SharedStateIndex;
internal Snapshot(LocalState variableState, int globalStateIndex)
internal Snapshot(LocalState variableState, int sharedStateIndex)
{
VariableState = variableState;
SharedStateIndex = globalStateIndex;
SharedStateIndex = sharedStateIndex;
}
}
}
......
......@@ -487,18 +487,22 @@ protected override ImmutableArray<PendingBranch> Scan(ref bool badRegion)
int position,
BoundNode node,
Binder binder,
SnapshotManager originalSnapshots)
SnapshotManager originalSnapshots,
bool takeNewSnapshots,
out SnapshotManager newSnapshots)
{
var analyzedNullabilities = ImmutableDictionary.CreateBuilder<BoundExpression, (NullabilityInfo, TypeSymbol)>();
var (walker, initialState) = originalSnapshots.RestoreWalkerToAnalyzeNewNode(position, node, binder, analyzedNullabilities);
Analyze(walker, symbol: null, diagnostics: null, initialState, snapshotBuilderOpt: null);
var newSnapshotBuilder = takeNewSnapshots ? new SnapshotManager.Builder() : null;
var (walker, initialState, symbol) = originalSnapshots.RestoreWalkerToAnalyzeNewNode(position, node, binder, analyzedNullabilities, newSnapshotBuilder);
Analyze(walker, symbol, diagnostics: null, initialState, snapshotBuilderOpt: newSnapshotBuilder);
var analyzedNullabilitiesMap = analyzedNullabilities.ToImmutable();
newSnapshots = newSnapshotBuilder?.ToManagerAndFree();
#if DEBUG
if (binder.Compilation.NullableSemanticAnalysisEnabled)
{
DebugVerifier.Verify(analyzedNullabilitiesMap, snapshotManagerOpt: null, node);
DebugVerifier.Verify(analyzedNullabilitiesMap, newSnapshots, node);
}
#endif
......
......@@ -913,12 +913,16 @@ void M(string? s1)
var conditionalAccessExpression = root.DescendantNodes().OfType<ConditionalAccessExpressionSyntax>().Single();
var ternary = root.DescendantNodes().OfType<ConditionalExpressionSyntax>().Single();
var newSource = ((ExpressionStatementSyntax)SyntaxFactory.ParseStatement(@"_ = s1 == """" ? s1 : s1;"));
var newTernary = (ConditionalExpressionSyntax)((AssignmentExpressionSyntax)newSource.Expression).Right;
var newSource = (BlockSyntax)SyntaxFactory.ParseStatement(@"{ string? s3 = null; _ = s1 == """" ? s1 : s1; }");
var newExprStatement = (ExpressionStatementSyntax)newSource.Statements[1];
var newTernary = (ConditionalExpressionSyntax)((AssignmentExpressionSyntax)newExprStatement.Expression).Right;
var inCondition = ((BinaryExpressionSyntax)newTernary.Condition).Left;
var whenTrue = newTernary.WhenTrue;
var whenFalse = newTernary.WhenFalse;
var newReference = (IdentifierNameSyntax)SyntaxFactory.ParseExpression(@"s1");
var newCoalesce = (AssignmentExpressionSyntax)SyntaxFactory.ParseExpression(@"s3 ??= s1", options: TestOptions.Regular8);
// Before the if statement
verifySpeculativeModel(ifStatement.SpanStart, PublicNullableFlowState.MaybeNull);
......@@ -940,12 +944,25 @@ void M(string? s1)
void verifySpeculativeModel(int spanStart, PublicNullableFlowState conditionFlowState)
{
Assert.True(model.TryGetSpeculativeSemanticModel(spanStart, newSource, out var speculativeModel));
var speculativeTypeInfo = speculativeModel.GetTypeInfo(inCondition);
Assert.Equal(conditionFlowState, speculativeTypeInfo.Nullability.FlowState);
speculativeTypeInfo = speculativeModel.GetTypeInfo(whenTrue);
Assert.Equal(PublicNullableFlowState.NotNull, speculativeTypeInfo.Nullability.FlowState);
var referenceTypeInfo = speculativeModel.GetSpeculativeTypeInfo(whenTrue.SpanStart, newReference, SpeculativeBindingOption.BindAsExpression);
Assert.Equal(PublicNullableFlowState.NotNull, referenceTypeInfo.Nullability.FlowState);
var coalesceTypeInfo = speculativeModel.GetSpeculativeTypeInfo(whenTrue.SpanStart, newCoalesce, SpeculativeBindingOption.BindAsExpression);
Assert.Equal(PublicNullableFlowState.NotNull, coalesceTypeInfo.Nullability.FlowState);
speculativeTypeInfo = speculativeModel.GetTypeInfo(whenFalse);
Assert.Equal(conditionFlowState, speculativeTypeInfo.Nullability.FlowState);
referenceTypeInfo = speculativeModel.GetSpeculativeTypeInfo(whenFalse.SpanStart, newReference, SpeculativeBindingOption.BindAsExpression);
Assert.Equal(conditionFlowState, referenceTypeInfo.Nullability.FlowState);
coalesceTypeInfo = speculativeModel.GetSpeculativeTypeInfo(whenFalse.SpanStart, newCoalesce, SpeculativeBindingOption.BindAsExpression);
Assert.Equal(conditionFlowState, coalesceTypeInfo.Nullability.FlowState);
}
}
......@@ -1011,5 +1028,63 @@ void M(C? x, C x2)
Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "(default(object), default(int))").WithArguments("(object?, int)", "(object a, int b)").WithLocation(11, 32)
);
}
[Fact]
public void SpeculativeGetTypeInfo_Basic()
{
var source = @"
class C
{
static object? staticField = null;
object field = staticField is null ? new object() : staticField;
string M(string? s1)
{
if (s1 != null)
{
s1.ToString();
}
s1?.ToString();
s1 = """";
var s2 = s1 == null ? """" : s1;
return null!;
}
}";
var comp = CreateCompilation(source, options: WithNonNullTypesTrue(), parseOptions: TestOptions.Regular8WithNullableAnalysis);
comp.VerifyDiagnostics();
var syntaxTree = comp.SyntaxTrees[0];
var root = syntaxTree.GetRoot();
var model = comp.GetSemanticModel(syntaxTree);
var ifStatement = root.DescendantNodes().OfType<IfStatementSyntax>().Single();
var conditionalAccessExpression = root.DescendantNodes().OfType<ConditionalAccessExpressionSyntax>().Single();
var ternary = root.DescendantNodes().OfType<ConditionalExpressionSyntax>().Skip(1).Single();
var newReference = (IdentifierNameSyntax)SyntaxFactory.ParseExpression(@"s1");
var newCoalesce = (AssignmentExpressionSyntax)SyntaxFactory.ParseExpression(@"s1 ??= """"");
verifySpeculativeTypeInfo(ifStatement.SpanStart, PublicNullableFlowState.MaybeNull);
verifySpeculativeTypeInfo(ifStatement.Statement.SpanStart, PublicNullableFlowState.NotNull);
verifySpeculativeTypeInfo(conditionalAccessExpression.SpanStart, PublicNullableFlowState.MaybeNull);
verifySpeculativeTypeInfo(conditionalAccessExpression.WhenNotNull.SpanStart, PublicNullableFlowState.NotNull);
verifySpeculativeTypeInfo(ternary.WhenTrue.SpanStart, PublicNullableFlowState.MaybeNull);
verifySpeculativeTypeInfo(ternary.WhenFalse.SpanStart, PublicNullableFlowState.NotNull);
void verifySpeculativeTypeInfo(int position, PublicNullableFlowState expectedFlowState)
{
var specTypeInfo = model.GetSpeculativeTypeInfo(position, newReference, SpeculativeBindingOption.BindAsExpression);
Assert.Equal(expectedFlowState, specTypeInfo.Nullability.FlowState);
specTypeInfo = model.GetSpeculativeTypeInfo(position, newCoalesce, SpeculativeBindingOption.BindAsExpression);
Assert.Equal(PublicNullableFlowState.NotNull, specTypeInfo.Nullability.FlowState);
}
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册