diff --git a/clang/lib/Format/FormatToken.h b/clang/lib/Format/FormatToken.h --- a/clang/lib/Format/FormatToken.h +++ b/clang/lib/Format/FormatToken.h @@ -114,6 +114,7 @@ TYPE(CSharpNamedArgumentColon) \ TYPE(CSharpNullable) \ TYPE(CSharpNullCoalescing) \ + TYPE(CSharpNullCoalescingAssignment) \ TYPE(CSharpNullConditional) \ TYPE(CSharpNullConditionalLSquare) \ TYPE(CSharpGenericTypeConstraint) \ diff --git a/clang/lib/Format/FormatTokenLexer.h b/clang/lib/Format/FormatTokenLexer.h --- a/clang/lib/Format/FormatTokenLexer.h +++ b/clang/lib/Format/FormatTokenLexer.h @@ -54,7 +54,8 @@ bool tryMergeJSPrivateIdentifier(); bool tryMergeCSharpStringLiteral(); bool tryMergeCSharpKeywordVariables(); - bool tryMergeCSharpDoubleQuestion(); + bool tryMergeCSharpNullCoalescing(); + bool tryMergeCSharpNullCoalescingAssignment(); bool tryMergeCSharpNullConditional(); bool tryTransformCSharpForEach(); bool tryMergeForEach(); diff --git a/clang/lib/Format/FormatTokenLexer.cpp b/clang/lib/Format/FormatTokenLexer.cpp --- a/clang/lib/Format/FormatTokenLexer.cpp +++ b/clang/lib/Format/FormatTokenLexer.cpp @@ -97,7 +97,9 @@ return; if (tryMergeCSharpStringLiteral()) return; - if (tryMergeCSharpDoubleQuestion()) + if (tryMergeCSharpNullCoalescing()) + return; + if (tryMergeCSharpNullCoalescingAssignment()) return; if (tryMergeCSharpNullConditional()) return; @@ -310,7 +312,7 @@ "param", "property", "return", "type", }; -bool FormatTokenLexer::tryMergeCSharpDoubleQuestion() { +bool FormatTokenLexer::tryMergeCSharpNullCoalescing() { if (Tokens.size() < 2) return false; auto &FirstQuestion = *(Tokens.end() - 2); @@ -327,6 +329,24 @@ return true; } +bool FormatTokenLexer::tryMergeCSharpNullCoalescingAssignment() { + if (Tokens.size() < 2) + return false; + auto &NullCoalescing = *(Tokens.end() - 2); + auto &Equal = *(Tokens.end() - 1); + if (NullCoalescing->getType() != TT_CSharpNullCoalescing || + !Equal->is(tok::equal)) + return false; + NullCoalescing->Tok.setKind(tok::equal); // no '??=' in clang tokens. + NullCoalescing->TokenText = + StringRef(NullCoalescing->TokenText.begin(), + Equal->TokenText.end() - NullCoalescing->TokenText.begin()); + NullCoalescing->ColumnWidth += Equal->ColumnWidth; + NullCoalescing->setType(TT_CSharpNullCoalescingAssignment); + Tokens.erase(Tokens.end() - 1); + return true; +} + // Merge '?[' and '?.' pairs into single tokens. bool FormatTokenLexer::tryMergeCSharpNullConditional() { if (Tokens.size() < 2) diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp --- a/clang/lib/Format/TokenAnnotator.cpp +++ b/clang/lib/Format/TokenAnnotator.cpp @@ -1579,24 +1579,29 @@ } } - if (Style.Language == FormatStyle::LK_JavaScript) { - if (Current.is(tok::exclaim)) { - if (Current.Previous && - (Keywords.IsJavaScriptIdentifier( - *Current.Previous, /* AcceptIdentifierName= */ true) || - Current.Previous->isOneOf( - tok::kw_namespace, tok::r_paren, tok::r_square, tok::r_brace, - Keywords.kw_type, Keywords.kw_get, Keywords.kw_set) || - Current.Previous->Tok.isLiteral())) { - Current.setType(TT_JsNonNullAssertion); - return; - } - if (Current.Next && - Current.Next->isOneOf(TT_BinaryOperator, Keywords.kw_as)) { + if ((Style.Language == FormatStyle::LK_JavaScript || Style.isCSharp()) && + Current.is(tok::exclaim)) { + if (Current.Previous) { + bool isIdentifier = + Style.Language == FormatStyle::LK_JavaScript + ? Keywords.IsJavaScriptIdentifier( + *Current.Previous, /* AcceptIdentifierName= */ true) + : Current.Previous->is(tok::identifier); + if (isIdentifier || + Current.Previous->isOneOf( + tok::kw_namespace, tok::r_paren, tok::r_square, tok::r_brace, + tok::kw_false, tok::kw_true, Keywords.kw_type, Keywords.kw_get, + Keywords.kw_set) || + Current.Previous->Tok.isLiteral()) { Current.setType(TT_JsNonNullAssertion); return; } } + if (Current.Next && + Current.Next->isOneOf(TT_BinaryOperator, Keywords.kw_as)) { + Current.setType(TT_JsNonNullAssertion); + return; + } } // Line.MightBeFunctionDecl can only be true after the parentheses of a @@ -3186,6 +3191,10 @@ if (Left.is(TT_CSharpNullCoalescing) || Right.is(TT_CSharpNullCoalescing)) return true; + // No space before null forgiving '!'. + if (Right.is(TT_JsNonNullAssertion)) + return false; + // No space before '?['. if (Right.is(TT_CSharpNullConditionalLSquare)) return false; diff --git a/clang/unittests/Format/FormatTestCSharp.cpp b/clang/unittests/Format/FormatTestCSharp.cpp --- a/clang/unittests/Format/FormatTestCSharp.cpp +++ b/clang/unittests/Format/FormatTestCSharp.cpp @@ -358,6 +358,29 @@ verifyFormat("return _name ?? \"DEF\";"); } +TEST_F(FormatTestCSharp, CSharpNullCoalescingAssignment) { + FormatStyle Style = getGoogleStyle(FormatStyle::LK_CSharp); + Style.SpaceBeforeAssignmentOperators = true; + + verifyFormat("test \?\?= ABC;", Style); + verifyFormat("test \?\?= true;", Style); + + Style.SpaceBeforeAssignmentOperators = false; + + verifyFormat("test\?\?= ABC;", Style); + verifyFormat("test\?\?= true;", Style); +} + +TEST_F(FormatTestCSharp, CSharpNullForgiving) { + verifyFormat("var test = null!;"); + verifyFormat("string test = someFunctionCall()! + \"ABC\"!"); + verifyFormat("int test = (1! + 2 + bar! + foo())!"); + verifyFormat("test \?\?= !foo!;"); + verifyFormat("test = !bar! \?\? !foo!;"); + verifyFormat("bool test = !(!true || !null! || !false && !false! && !bar()! " + "+ (!foo()))!"); +} + TEST_F(FormatTestCSharp, AttributesIndentation) { FormatStyle Style = getMicrosoftStyle(FormatStyle::LK_CSharp); Style.AlwaysBreakAfterReturnType = FormatStyle::RTBS_None;