clang-format: insert trailing commas into containers.

Summary:
This change adds an option to insert trailing commas into container
literals. For example, in JavaScript:

    const x = [
      a,
      b,
       ^~~~~ inserted if missing.
    ]

This is implemented as a seperate post-processing pass after formatting
(because formatting might change whether the container literal does or
does not wrap). This keeps the code relatively simple and orthogonal,
though it has the notable drawback that the newly inserted comma is not
taken into account for formatting decisions (e.g. it might exceed the 80
char limit). To avoid exceeding the ColumnLimit, a comma is only
inserted if it fits into the limit.

Trailing comma insertion conceptually conflicts with argument
bin-packing: inserting a comma disables bin-packing, so we cannot do
both. clang-format rejects FormatStyle configurations that do both with
this change.

Reviewers: krasimir, MyDeveloperDay

Subscribers: cfe-commits

Tags: #clang
This commit is contained in:
Martin Probst 2020-01-24 16:13:51 +01:00
parent 3cf80822a9
commit a324fcf1ae
4 changed files with 159 additions and 1 deletions

View File

@ -35,7 +35,12 @@ class DiagnosticConsumer;
namespace format {
enum class ParseError { Success = 0, Error, Unsuitable };
enum class ParseError {
Success = 0,
Error,
Unsuitable,
BinPackTrailingCommaConflict
};
class ParseErrorCategory final : public std::error_category {
public:
const char *name() const noexcept override;
@ -544,6 +549,20 @@ struct FormatStyle {
/// \endcode
bool BinPackArguments;
/// The style of inserting trailing commas into container literals.
enum TrailingCommaStyle {
/// Do not insert trailing commas.
TCS_None,
/// Insert trailing commas in container literals that were wrapped over
/// multiple lines. Note that this is conceptually incompatible with
/// bin-packing, because the trailing comma is used as an indicator
/// that a container should be formatted one-per-line (i.e. not bin-packed).
/// So inserting a trailing comma counteracts bin-packing.
TCS_Wrapped,
};
TrailingCommaStyle InsertTrailingCommas;
/// If ``false``, a function declaration's or function definition's
/// parameters will either all be on the same line or will have one line each.
/// \code

View File

@ -157,6 +157,13 @@ template <> struct ScalarEnumerationTraits<FormatStyle::BinPackStyle> {
}
};
template <> struct ScalarEnumerationTraits<FormatStyle::TrailingCommaStyle> {
static void enumeration(IO &IO, FormatStyle::TrailingCommaStyle &Value) {
IO.enumCase(Value, "None", FormatStyle::TCS_None);
IO.enumCase(Value, "Wrapped", FormatStyle::TCS_Wrapped);
}
};
template <> struct ScalarEnumerationTraits<FormatStyle::BinaryOperatorStyle> {
static void enumeration(IO &IO, FormatStyle::BinaryOperatorStyle &Value) {
IO.enumCase(Value, "All", FormatStyle::BOS_All);
@ -486,6 +493,7 @@ template <> struct MappingTraits<FormatStyle> {
IO.mapOptional("IndentWidth", Style.IndentWidth);
IO.mapOptional("IndentWrappedFunctionNames",
Style.IndentWrappedFunctionNames);
IO.mapOptional("InsertTrailingCommas", Style.InsertTrailingCommas);
IO.mapOptional("JavaImportGroups", Style.JavaImportGroups);
IO.mapOptional("JavaScriptQuotes", Style.JavaScriptQuotes);
IO.mapOptional("JavaScriptWrapImports", Style.JavaScriptWrapImports);
@ -644,6 +652,8 @@ std::string ParseErrorCategory::message(int EV) const {
return "Invalid argument";
case ParseError::Unsuitable:
return "Unsuitable";
case ParseError::BinPackTrailingCommaConflict:
return "trailing comma insertion cannot be used with bin packing";
}
llvm_unreachable("unexpected parse error");
}
@ -788,6 +798,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
LLVMStyle.IndentPPDirectives = FormatStyle::PPDIS_None;
LLVMStyle.IndentWrappedFunctionNames = false;
LLVMStyle.IndentWidth = 2;
LLVMStyle.InsertTrailingCommas = FormatStyle::TCS_None;
LLVMStyle.JavaScriptQuotes = FormatStyle::JSQS_Leave;
LLVMStyle.JavaScriptWrapImports = true;
LLVMStyle.TabWidth = 8;
@ -946,6 +957,9 @@ FormatStyle getGoogleStyle(FormatStyle::LanguageKind Language) {
// taze:, triple slash directives (`/// <...`), tslint:, and @see, which is
// commonly followed by overlong URLs.
GoogleStyle.CommentPragmas = "(taze:|^/[ \t]*<|tslint:|@see)";
// TODO: enable once decided, in particular re disabling bin packing.
// https://google.github.io/styleguide/jsguide.html#features-arrays-trailing-comma
// GoogleStyle.InsertTrailingCommas = FormatStyle::TCS_Wrapped;
GoogleStyle.MaxEmptyLinesToKeep = 3;
GoogleStyle.NamespaceIndentation = FormatStyle::NI_All;
GoogleStyle.SpacesInContainerLiterals = false;
@ -1211,6 +1225,11 @@ std::error_code parseConfiguration(StringRef Text, FormatStyle *Style) {
StyleSet.Add(std::move(DefaultStyle));
}
*Style = *StyleSet.Get(Language);
if (Style->InsertTrailingCommas != FormatStyle::TCS_None &&
Style->BinPackArguments) {
// See comment on FormatStyle::TSC_Wrapped.
return make_error_code(ParseError::BinPackTrailingCommaConflict);
}
return make_error_code(ParseError::Success);
}
@ -1466,6 +1485,75 @@ private:
FormattingAttemptStatus *Status;
};
/// TrailingCommaInserter inserts trailing commas into container literals.
/// E.g.:
/// const x = [
/// 1,
/// ];
/// TrailingCommaInserter runs after formatting. To avoid causing a required
/// reformatting (and thus reflow), it never inserts a comma that'd exceed the
/// ColumnLimit.
///
/// Because trailing commas disable binpacking of arrays, TrailingCommaInserter
/// is conceptually incompatible with bin packing.
class TrailingCommaInserter : public TokenAnalyzer {
public:
TrailingCommaInserter(const Environment &Env, const FormatStyle &Style)
: TokenAnalyzer(Env, Style) {}
std::pair<tooling::Replacements, unsigned>
analyze(TokenAnnotator &Annotator,
SmallVectorImpl<AnnotatedLine *> &AnnotatedLines,
FormatTokenLexer &Tokens) override {
AffectedRangeMgr.computeAffectedLines(AnnotatedLines);
tooling::Replacements Result;
insertTrailingCommas(AnnotatedLines, Result);
return {Result, 0};
}
private:
/// Inserts trailing commas in [] and {} initializers if they wrap over
/// multiple lines.
void insertTrailingCommas(SmallVectorImpl<AnnotatedLine *> &Lines,
tooling::Replacements &Result) {
for (AnnotatedLine *Line : Lines) {
insertTrailingCommas(Line->Children, Result);
if (!Line->Affected)
continue;
for (FormatToken *FormatTok = Line->First; FormatTok;
FormatTok = FormatTok->Next) {
if (FormatTok->NewlinesBefore == 0)
continue;
FormatToken *Matching = FormatTok->MatchingParen;
if (!Matching || !FormatTok->getPreviousNonComment())
continue;
if (!(FormatTok->is(tok::r_square) &&
Matching->is(TT_ArrayInitializerLSquare)) &&
!(FormatTok->is(tok::r_brace) && Matching->is(TT_DictLiteral)))
continue;
FormatToken *Prev = FormatTok->getPreviousNonComment();
if (Prev->is(tok::comma) || Prev->is(tok::semi))
continue;
// getEndLoc is not reliably set during re-lexing, use text length
// instead.
SourceLocation Start =
Prev->Tok.getLocation().getLocWithOffset(Prev->TokenText.size());
// If inserting a comma would push the code over the column limit, skip
// this location - it'd introduce an unstable formatting due to the
// required reflow.
unsigned ColumnNumber =
Env.getSourceManager().getSpellingColumnNumber(Start);
if (ColumnNumber > Style.ColumnLimit)
continue;
// Comma insertions cannot conflict with each other, and this pass has a
// clean set of Replacements, so the operation below cannot fail.
cantFail(Result.add(
tooling::Replacement(Env.getSourceManager(), Start, 0, ",")));
}
}
}
};
// This class clean up the erroneous/redundant code around the given ranges in
// file.
class Cleaner : public TokenAnalyzer {
@ -2435,6 +2523,12 @@ reformat(const FormatStyle &Style, StringRef Code,
return Formatter(Env, Expanded, Status).process();
});
if (Style.Language == FormatStyle::LK_JavaScript &&
Style.InsertTrailingCommas == FormatStyle::TCS_Wrapped)
Passes.emplace_back([&](const Environment &Env) {
return TrailingCommaInserter(Env, Expanded).process();
});
auto Env =
std::make_unique<Environment>(Code, FileName, Ranges, FirstStartColumn,
NextStartColumn, LastStartColumn);

View File

@ -13031,6 +13031,12 @@ TEST_F(FormatTest, ParsesConfigurationWithLanguages) {
"IndentWidth: 34",
&Style),
ParseError::Unsuitable);
FormatStyle BinPackedTCS = {};
BinPackedTCS.Language = FormatStyle::LK_JavaScript;
EXPECT_EQ(parseConfiguration("BinPackArguments: true\n"
"InsertTrailingCommas: Wrapped",
&BinPackedTCS),
ParseError::BinBackTrailingCommaConflict);
EXPECT_EQ(12u, Style.IndentWidth);
CHECK_PARSE("IndentWidth: 56", IndentWidth, 56u);
EXPECT_EQ(FormatStyle::LK_Cpp, Style.Language);

View File

@ -878,6 +878,45 @@ TEST_F(FormatTestJS, ColumnLayoutForArrayLiterals) {
"]);");
}
TEST_F(FormatTestJS, TrailingCommaInsertion) {
FormatStyle Style = getGoogleStyle(FormatStyle::LK_JavaScript);
Style.InsertTrailingCommas = FormatStyle::TCS_Wrapped;
// Insert comma in wrapped array.
verifyFormat("const x = [\n"
" 1, //\n"
" 2,\n"
"];",
"const x = [\n"
" 1, //\n"
" 2];",
Style);
// Insert comma in newly wrapped array.
Style.ColumnLimit = 30;
verifyFormat("const x = [\n"
" aaaaaaaaaaaaaaaaaaaaaaaaa,\n"
"];",
"const x = [aaaaaaaaaaaaaaaaaaaaaaaaa];", Style);
// Do not insert trailing commas if they'd exceed the colum limit
verifyFormat("const x = [\n"
" aaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"
"];",
"const x = [aaaaaaaaaaaaaaaaaaaaaaaaaaaa];", Style);
// Object literals.
verifyFormat("const x = {\n"
" a: aaaaaaaaaaaaaaaaa,\n"
"};",
"const x = {a: aaaaaaaaaaaaaaaaa};", Style);
verifyFormat("const x = {\n"
" a: aaaaaaaaaaaaaaaaaaaaaaaaa\n"
"};",
"const x = {a: aaaaaaaaaaaaaaaaaaaaaaaaa};", Style);
// Object literal types.
verifyFormat("let x: {\n"
" a: aaaaaaaaaaaaaaaaaaaaa,\n"
"};",
"let x: {a: aaaaaaaaaaaaaaaaaaaaa};", Style);
}
TEST_F(FormatTestJS, FunctionLiterals) {
FormatStyle Style = getGoogleStyle(FormatStyle::LK_JavaScript);
Style.AllowShortFunctionsOnASingleLine = FormatStyle::SFS_Inline;