Index: include/clang/Format/Format.h =================================================================== --- include/clang/Format/Format.h +++ include/clang/Format/Format.h @@ -591,6 +591,20 @@ /// \brief The way to use tab characters in the resulting file. UseTabStyle UseTab; + /// \brief Quotation styles for JavaScript strings. Does not affect template + /// strings. + enum JavaScriptQuoteStyle { + /// Leave string quotes as they are. + JSQS_Leave, + /// Always use single quotes. + JSQS_Single, + /// Always use double quotes. + JSQS_Double + }; + + /// \brief The JavaScriptQuoteStyle to use for JavaScript strings. + JavaScriptQuoteStyle JavaScriptQuotes; + bool operator==(const FormatStyle &R) const { return AccessModifierOffset == R.AccessModifierOffset && AlignAfterOpenBracket == R.AlignAfterOpenBracket && @@ -667,7 +681,8 @@ SpacesInParentheses == R.SpacesInParentheses && SpacesInSquareBrackets == R.SpacesInSquareBrackets && Standard == R.Standard && TabWidth == R.TabWidth && - UseTab == R.UseTab; + UseTab == R.UseTab && + JavaScriptQuotes == R.JavaScriptQuotes; } }; Index: lib/Format/Format.cpp =================================================================== --- lib/Format/Format.cpp +++ lib/Format/Format.cpp @@ -71,6 +71,14 @@ } }; +template <> struct ScalarEnumerationTraits { + static void enumeration(IO &IO, FormatStyle::JavaScriptQuoteStyle &Value) { + IO.enumCase(Value, "Leave", FormatStyle::JSQS_Leave); + IO.enumCase(Value, "Single", FormatStyle::JSQS_Single); + IO.enumCase(Value, "Double", FormatStyle::JSQS_Double); + } +}; + template <> struct ScalarEnumerationTraits { static void enumeration(IO &IO, FormatStyle::ShortFunctionStyle &Value) { IO.enumCase(Value, "None", FormatStyle::SFS_None); @@ -335,6 +343,7 @@ IO.mapOptional("Standard", Style.Standard); IO.mapOptional("TabWidth", Style.TabWidth); IO.mapOptional("UseTab", Style.UseTab); + IO.mapOptional("JavaScriptQuotes", Style.JavaScriptQuotes); } }; @@ -522,6 +531,7 @@ LLVMStyle.SpacesBeforeTrailingComments = 1; LLVMStyle.Standard = FormatStyle::LS_Cpp11; LLVMStyle.UseTab = FormatStyle::UT_Never; + LLVMStyle.JavaScriptQuotes = FormatStyle::JSQS_Leave; LLVMStyle.ReflowComments = true; LLVMStyle.SpacesInParentheses = false; LLVMStyle.SpacesInSquareBrackets = false; @@ -590,6 +600,7 @@ GoogleStyle.CommentPragmas = "@(export|see|visibility) "; GoogleStyle.MaxEmptyLinesToKeep = 3; GoogleStyle.SpacesInContainerLiterals = false; + GoogleStyle.JavaScriptQuotes = FormatStyle::JSQS_Single; } else if (Language == FormatStyle::LK_Proto) { GoogleStyle.AllowShortFunctionsOnASingleLine = FormatStyle::SFS_None; GoogleStyle.SpacesInContainerLiterals = false; @@ -766,13 +777,13 @@ class FormatTokenLexer { public: FormatTokenLexer(SourceManager &SourceMgr, FileID ID, FormatStyle &Style, - encoding::Encoding Encoding) + encoding::Encoding Encoding, tooling::Replacements &Replaces) : FormatTok(nullptr), IsFirstToken(true), GreaterStashed(false), LessStashed(false), Column(0), TrailingWhitespace(0), SourceMgr(SourceMgr), ID(ID), Style(Style), IdentTable(getFormattingLangOpts(Style)), Keywords(IdentTable), - Encoding(Encoding), FirstInLineIndex(0), FormattingDisabled(false), - MacroBlockBeginRegex(Style.MacroBlockBegin), + Encoding(Encoding), Replaces(Replaces), FirstInLineIndex(0), + FormattingDisabled(false), MacroBlockBeginRegex(Style.MacroBlockBegin), MacroBlockEndRegex(Style.MacroBlockEnd) { Lex.reset(new Lexer(ID, SourceMgr.getBuffer(ID), SourceMgr, getFormattingLangOpts(Style))); @@ -791,6 +802,8 @@ if (Style.Language == FormatStyle::LK_JavaScript) tryParseJSRegexLiteral(); tryMergePreviousTokens(); + if (Style.Language == FormatStyle::LK_JavaScript) + tryRequoteJSStringLiteral(); if (Tokens.back()->NewlinesBefore > 0 || Tokens.back()->IsMultiline) FirstInLineIndex = Tokens.size() - 1; } while (Tokens.back()->Tok.isNot(tok::eof)); @@ -1061,6 +1074,67 @@ return false; } + // If the last token is a double/single-quoted string literal, generates a + // replacement with a single/double quoted string literal, re-escaping the + // contents in the process. + void tryRequoteJSStringLiteral() { + if (Style.JavaScriptQuotes == FormatStyle::JSQS_Leave) + return; + FormatToken *FormatTok = Tokens.back(); + + StringRef Input = FormatTok->TokenText; + if (!FormatTok->isStringLiteral() || + // NB: testing for not starting with a double quote to avoid breaking + // `template strings`. + (Style.JavaScriptQuotes == FormatStyle::JSQS_Single && + !Input.startswith("\"")) || + (Style.JavaScriptQuotes == FormatStyle::JSQS_Double && + !Input.startswith("\'"))) + return; + + char Quote = Style.JavaScriptQuotes == FormatStyle::JSQS_Single ? '\'' : '\"'; + + size_t ColumnWidth = FormatTok->TokenText.size(); + std::string Res(1, Quote); + llvm::raw_string_ostream Out(Res); + bool Escaped = false; + for (size_t i = 1; i < Input.size() - 1; i++) { + switch (Input[i]) { + case '\\': + if (!Escaped && i + 1 < Input.size() && + ((Quote == '\'' && Input[i + 1] == '"') || + (Quote == '"' && Input[i + 1] == '\''))) { + // Skip this \, it's escaping a " or ' that no longer needs escaping. + ColumnWidth--; + continue; + } + Escaped = !Escaped; + break; + case '\"': + case '\'': + if (Input[i] == Quote && !Escaped) { + Out << '\\'; + ColumnWidth++; + } + Escaped = false; + break; + default: + Escaped = false; + break; + } + Out << Input[i]; + } + Out << Quote; + + // For formatting, count the number of non-escaped single quotes in them + // and adjust ColumnWidth to take the added escapes into account. + FormatTok->ColumnWidth = ColumnWidth; + + SourceRange Range(FormatTok->Tok.getLocation(), FormatTok->Tok.getEndLoc()); + Replaces.insert(tooling::Replacement( + SourceMgr, CharSourceRange::getCharRange(Range), Out.str())); + } + bool tryMerge_TMacro() { if (Tokens.size() < 4) return false; @@ -1359,6 +1433,7 @@ IdentifierTable IdentTable; AdditionalKeywords Keywords; encoding::Encoding Encoding; + tooling::Replacements &Replaces; llvm::SpecificBumpPtrAllocator Allocator; // Index (in 'Tokens') of the last token that starts a new line. unsigned FirstInLineIndex; @@ -1382,10 +1457,15 @@ Tok.IsUnterminatedLiteral = true; } else if (Style.Language == FormatStyle::LK_JavaScript && Tok.TokenText == "''") { - Tok.Tok.setKind(tok::char_constant); + Tok.Tok.setKind(tok::string_literal); } } + if (Style.Language == FormatStyle::LK_JavaScript && + Tok.is(tok::char_constant)) { + Tok.Tok.setKind(tok::string_literal); + } + if (Tok.is(tok::comment) && (Tok.TokenText == "// clang-format on" || Tok.TokenText == "/* clang-format on */")) { FormattingDisabled = false; @@ -1443,7 +1523,7 @@ tooling::Replacements format(bool *IncompleteFormat) { tooling::Replacements Result; - FormatTokenLexer Tokens(SourceMgr, ID, Style, Encoding); + FormatTokenLexer Tokens(SourceMgr, ID, Style, Encoding, Result); UnwrappedLineParser Parser(Style, Tokens.getKeywords(), Tokens.lex(), *this); Index: lib/Format/TokenAnnotator.cpp =================================================================== --- lib/Format/TokenAnnotator.cpp +++ lib/Format/TokenAnnotator.cpp @@ -2168,8 +2168,8 @@ if (Style.Language == FormatStyle::LK_JavaScript) { // FIXME: This might apply to other languages and token kinds. - if (Right.is(tok::char_constant) && Left.is(tok::plus) && Left.Previous && - Left.Previous->is(tok::char_constant)) + if (Right.is(tok::string_literal) && Left.is(tok::plus) && Left.Previous && + Left.Previous->is(tok::string_literal)) return true; if (Left.is(TT_DictLiteral) && Left.is(tok::l_brace) && Line.Level == 0 && Left.Previous && Left.Previous->is(tok::equal) && Index: unittests/Format/FormatTest.cpp =================================================================== --- unittests/Format/FormatTest.cpp +++ unittests/Format/FormatTest.cpp @@ -6642,7 +6642,7 @@ " bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb};"); verifyNoCrash("a<,"); - + // No braced initializer here. verifyFormat("void f() {\n" " struct Dummy {};\n" Index: unittests/Format/FormatTestJS.cpp =================================================================== --- unittests/Format/FormatTestJS.cpp +++ unittests/Format/FormatTestJS.cpp @@ -250,7 +250,7 @@ verifyFormat("f({'a': [{}]});"); } -TEST_F(FormatTestJS, SingleQuoteStrings) { +TEST_F(FormatTestJS, SingleQuotedStrings) { verifyFormat("this.function('', true);"); } @@ -874,7 +874,7 @@ verifyFormat("import {\n" " X,\n" " Y,\n" - "} from 'some/long/module.js';", + "} from\n 'some/long/module.js';", getGoogleJSStyleWithColumns(20)); verifyFormat("import {X as myLocalX, Y as myLocalY} from 'some/module.js';"); verifyFormat("import * as lib from 'some/module.js';"); @@ -1085,5 +1085,36 @@ getGoogleJSStyleWithColumns(20))); } +TEST_F(FormatTestJS, RequoteStringsSingle) { + EXPECT_EQ("var x = 'foo';", format("var x = \"foo\";")); + EXPECT_EQ("var x = 'fo\\'o\\'';", format("var x = \"fo'o'\";")); + EXPECT_EQ("var x = 'fo\\'o\\'';", format("var x = \"fo\\'o'\";")); + EXPECT_EQ("var x =\n" + " 'foo\\'';", + // Code below is 15 chars wide, doesn't fit into the line with the + // \ escape added. + format("var x = \"foo'\";", getGoogleJSStyleWithColumns(15))); + // Removes no-longer needed \ escape from ". + EXPECT_EQ("var x = 'fo\"o';", format("var x = \"fo\\\"o\";")); + // Code below fits into 15 chars *after* removing the \ escape. + EXPECT_EQ("var x = 'fo\"o';", + format("var x = \"fo\\\"o\";", getGoogleJSStyleWithColumns(15))); +} + +TEST_F(FormatTestJS, RequoteStringsDouble) { + FormatStyle DoubleQuotes = getGoogleStyle(FormatStyle::LK_JavaScript); + DoubleQuotes.JavaScriptQuotes = FormatStyle::JSQS_Double; + verifyFormat("var x = \"foo\";", DoubleQuotes); + EXPECT_EQ("var x = \"foo\";", format("var x = 'foo';", DoubleQuotes)); + EXPECT_EQ("var x = \"fo'o\";", format("var x = 'fo\\'o';", DoubleQuotes)); +} + +TEST_F(FormatTestJS, RequoteStringsLeave) { + FormatStyle LeaveQuotes = getGoogleStyle(FormatStyle::LK_JavaScript); + LeaveQuotes.JavaScriptQuotes = FormatStyle::JSQS_Leave; + verifyFormat("var x = \"foo\";", LeaveQuotes); + verifyFormat("var x = 'foo';", LeaveQuotes); +} + } // end namespace tooling } // end namespace clang