[clang-format] Handle C# generic type constraints

Summary:
Treat each C# generic type constraint, `where T: ...`, as a line.

Add C# keyword: where

Add Token Types: CSharpGenericTypeConstraint, CSharpGenericTypeConstraintColon, CSharpGenericTypeConstraintComma.

This patch does not wrap generic type constraints well, that will be addressed in a follow up patch.

Reviewers: krasimir

Reviewed By: krasimir

Subscribers: cfe-commits, MyDeveloperDay

Tags: #clang-format, #clang

Differential Revision: https://reviews.llvm.org/D76367
This commit is contained in:
Jonathan Coe 2020-03-19 12:49:15 +00:00
parent 0ea4fb5bb7
commit dcbcec4822
6 changed files with 84 additions and 1 deletions

View File

@ -108,6 +108,9 @@ namespace format {
TYPE(CSharpNullCoalescing) \
TYPE(CSharpNullConditional) \
TYPE(CSharpNullConditionalLSquare) \
TYPE(CSharpGenericTypeConstraint) \
TYPE(CSharpGenericTypeConstraintColon) \
TYPE(CSharpGenericTypeConstraintComma) \
TYPE(Unknown)
enum TokenType {
@ -779,6 +782,7 @@ struct AdditionalKeywords {
kw_unsafe = &IdentTable.get("unsafe");
kw_ushort = &IdentTable.get("ushort");
kw_when = &IdentTable.get("when");
kw_where = &IdentTable.get("where");
// Keep this at the end of the constructor to make sure everything here
// is
@ -796,6 +800,7 @@ struct AdditionalKeywords {
kw_is, kw_lock, kw_null, kw_object, kw_out, kw_override, kw_params,
kw_readonly, kw_ref, kw_string, kw_stackalloc, kw_sbyte, kw_sealed,
kw_uint, kw_ulong, kw_unchecked, kw_unsafe, kw_ushort, kw_when,
kw_where,
// Keywords from the JavaScript section.
kw_as, kw_async, kw_await, kw_declare, kw_finally, kw_from,
kw_function, kw_get, kw_import, kw_is, kw_let, kw_module, kw_readonly,
@ -900,6 +905,7 @@ struct AdditionalKeywords {
IdentifierInfo *kw_unsafe;
IdentifierInfo *kw_ushort;
IdentifierInfo *kw_when;
IdentifierInfo *kw_where;
/// Returns \c true if \p Tok is a true JavaScript identifier, returns
/// \c false if it is a keyword or a pseudo keyword.

View File

@ -1047,6 +1047,11 @@ private:
Keywords.kw___has_include_next)) {
parseHasInclude();
}
if (Tok->is(Keywords.kw_where) && Tok->Next &&
Tok->Next->isNot(tok::l_paren)) {
Tok->Type = TT_CSharpGenericTypeConstraint;
parseCSharpGenericTypeConstraint();
}
break;
default:
break;
@ -1054,6 +1059,30 @@ private:
return true;
}
void parseCSharpGenericTypeConstraint() {
while (CurrentToken) {
if (CurrentToken->is(tok::less)) {
// parseAngle is too greedy and will consume the whole line.
CurrentToken->Type = TT_TemplateOpener;
next();
} else if (CurrentToken->is(tok::greater)) {
CurrentToken->Type = TT_TemplateCloser;
next();
} else if (CurrentToken->is(tok::comma)) {
CurrentToken->Type = TT_CSharpGenericTypeConstraintComma;
next();
} else if (CurrentToken->is(Keywords.kw_where)) {
CurrentToken->Type = TT_CSharpGenericTypeConstraint;
next();
} else if (CurrentToken->is(tok::colon)) {
CurrentToken->Type = TT_CSharpGenericTypeConstraintColon;
next();
} else {
next();
}
}
}
void parseIncludeDirective() {
if (CurrentToken && CurrentToken->is(tok::less)) {
next();
@ -3299,6 +3328,8 @@ bool TokenAnnotator::mustBreakBefore(const AnnotatedLine &Line,
if (Right.is(TT_CSharpNamedArgumentColon) ||
Left.is(TT_CSharpNamedArgumentColon))
return false;
if (Right.is(TT_CSharpGenericTypeConstraint))
return true;
} else if (Style.Language == FormatStyle::LK_JavaScript) {
// FIXME: This might apply to other languages and token kinds.
if (Right.is(tok::string_literal) && Left.is(tok::plus) && Left.Previous &&

View File

@ -64,6 +64,8 @@ public:
}
if (static_cast<int>(Indent) + Offset >= 0)
Indent += Offset;
if (Line.First->is(TT_CSharpGenericTypeConstraint))
Indent = Line.Level * Style.IndentWidth + Style.ContinuationIndentWidth;
}
/// Update the indent state given that \p Line indent should be

View File

@ -323,6 +323,24 @@ void UnwrappedLineParser::parseFile() {
addUnwrappedLine();
}
void UnwrappedLineParser::parseCSharpGenericTypeConstraint() {
do {
switch (FormatTok->Tok.getKind()) {
case tok::l_brace:
return;
default:
if (FormatTok->is(Keywords.kw_where)) {
addUnwrappedLine();
nextToken();
parseCSharpGenericTypeConstraint();
break;
}
nextToken();
break;
}
} while (!eof());
}
void UnwrappedLineParser::parseCSharpAttribute() {
int UnpairedSquareBrackets = 1;
do {
@ -1344,6 +1362,12 @@ void UnwrappedLineParser::parseStructuralElement() {
parseTryCatch();
return;
case tok::identifier: {
if (Style.isCSharp() && FormatTok->is(Keywords.kw_where) &&
Line->MustBeDeclaration) {
addUnwrappedLine();
parseCSharpGenericTypeConstraint();
break;
}
if (FormatTok->is(TT_MacroBlockEnd)) {
addUnwrappedLine();
return;

View File

@ -126,6 +126,10 @@ private:
void parseJavaScriptEs6ImportExport();
void parseStatementMacro();
void parseCSharpAttribute();
// Parse a C# generic type constraint: `where T : IComparable<T>`.
// See:
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
void parseCSharpGenericTypeConstraint();
bool tryToParseLambda();
bool tryToParseLambdaIntroducer();
void tryToParseJSFunction();

View File

@ -628,7 +628,6 @@ TEST_F(FormatTestCSharp, CSharpSpaces) {
verifyFormat(R"(catch (TestException) when (innerFinallyExecuted))", Style);
verifyFormat(R"(private float[,] Values;)", Style);
verifyFormat(R"(Result this[Index x] => Foo(x);)", Style);
verifyFormat(R"(class ItemFactory<T> where T : new() {})", Style);
Style.SpacesInSquareBrackets = true;
verifyFormat(R"(private float[ , ] Values;)", Style);
@ -673,5 +672,22 @@ if (someThings[i][j][k].Contains(myThing)) {
Style);
}
TEST_F(FormatTestCSharp, CSharpGenericTypeConstraints) {
FormatStyle Style = getGoogleStyle(FormatStyle::LK_CSharp);
verifyFormat(R"(//
class ItemFactory<T>
where T : new() {})", Style);
verifyFormat(R"(//
class Dictionary<TKey, TVal>
where TKey : IComparable<TKey>
where TVal : IMyInterface {
public void MyMethod<T>(T t)
where T : IMyInterface { doThing(); }
})",
Style);
}
} // namespace format
} // end namespace clang