From ccf98152596d7688946629ac152ae9a7a72b70b2 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 23 Jan 2024 00:43:42 +0800 Subject: [PATCH 1/4] libnixf: parse ExprAttrs (basic, without `inherit` support) --- .../include/nixf/Basic/DiagnosticKinds.inc | 1 + libnixf/include/nixf/Parse/Nodes.h | 114 ++++++++++++ libnixf/src/Parse/Parser.cpp | 173 +++++++++++++++++- libnixf/src/Parse/Token.h | 6 + libnixf/test/Parse/Parser.cpp | 83 +++++++++ 5 files changed, 376 insertions(+), 1 deletion(-) diff --git a/libnixf/include/nixf/Basic/DiagnosticKinds.inc b/libnixf/include/nixf/Basic/DiagnosticKinds.inc index efa839b20..b45614c89 100644 --- a/libnixf/include/nixf/Basic/DiagnosticKinds.inc +++ b/libnixf/include/nixf/Basic/DiagnosticKinds.inc @@ -9,6 +9,7 @@ DIAG("lex-float-no-exp", FloatNoExp, Fatal, DIAG("lex-float-leading-zero", FloatLeadingZero, Warning, "float begins with extra zeros `{}` is nixf extension") DIAG("parse-expected", Expected, Error, "expected {}") +DIAG("parse-attrpath-extra-dot", AttrPathExtraDot, Error, "extra `.` at the end of attrpath") DIAG("parse-unexpected-between", UnexpectedBetween, Error, "unexpected {} between {} and {}") DIAG("parse-unexpected", UnexpectedText, Error, "unexpected text {}") DIAG("parse-missing-sep-formals", MissingSepFormals, Error, "missing seperator `,` between two lambda formals") diff --git a/libnixf/include/nixf/Parse/Nodes.h b/libnixf/include/nixf/Parse/Nodes.h index 96464a833..f9eab5814 100644 --- a/libnixf/include/nixf/Parse/Nodes.h +++ b/libnixf/include/nixf/Parse/Nodes.h @@ -26,12 +26,19 @@ class Node { /// \see Misc NK_Misc, + NK_Identifer, + NK_AttrName, + NK_AttrPath, + NK_Binding, + NK_Binds, + NK_BeginExpr, NK_ExprInt, NK_ExprFloat, NK_ExprString, NK_ExprPath, NK_ExprParen, + NK_ExprAttrs, NK_EndExpr, }; @@ -171,4 +178,111 @@ class ExprParen : public Expr { [[nodiscard]] const std::shared_ptr &rparen() const { return RParen; } }; +/// \brief Identifier. Variable names, attribute names, etc. +class Identifier : public Node { + std::string Name; + +public: + Identifier(RangeTy Range, std::string Name) + : Node(NK_Identifer, Range), Name(std::move(Name)) {} + [[nodiscard]] const std::string &name() const { return Name; } +}; + +class AttrName : public Node { +public: + enum AttrNameKind { ANK_ID, ANK_String, ANK_Interpolation }; + +private: + AttrNameKind Kind; + std::shared_ptr ID; + std::shared_ptr String; + std::shared_ptr Interpolation; + +public: + [[nodiscard]] AttrNameKind kind() const { return Kind; } + + AttrName(std::shared_ptr ID, RangeTy Range) + : Node(NK_AttrName, Range), Kind(ANK_ID) { + this->ID = std::move(ID); + } + + AttrName(std::shared_ptr String, RangeTy Range) + : Node(NK_AttrName, Range), Kind(ANK_String) { + this->String = std::move(String); + } + + AttrName(std::shared_ptr Interpolation, RangeTy Range) + : Node(NK_AttrName, Range), Kind(ANK_Interpolation) { + this->Interpolation = std::move(Interpolation); + } + + [[nodiscard]] const std::shared_ptr &interpolation() const { + assert(Kind == ANK_Interpolation); + return Interpolation; + } + + [[nodiscard]] const std::shared_ptr &id() const { + assert(Kind == ANK_ID); + return ID; + } + + [[nodiscard]] const std::shared_ptr &string() const { + assert(Kind == ANK_String); + return String; + } +}; + +class AttrPath : public Node { + std::vector> Names; + +public: + AttrPath(RangeTy Range, std::vector> Names) + : Node(NK_AttrPath, Range), Names(std::move(Names)) {} + + [[nodiscard]] const std::vector> &names() const { + return Names; + } +}; + +class Binding : public Node { + std::shared_ptr Path; + std::shared_ptr Value; + +public: + Binding(RangeTy Range, std::shared_ptr Path, + std::shared_ptr Value) + : Node(NK_Binding, Range), Path(std::move(Path)), + Value(std::move(Value)) {} + + [[nodiscard]] const std::shared_ptr &path() const { return Path; } + [[nodiscard]] const std::shared_ptr &value() const { return Value; } +}; + +class Binds : public Node { + std::vector> Bindings; + +public: + Binds(RangeTy Range, std::vector> Bindings) + : Node(NK_Binds, Range), Bindings(std::move(Bindings)) {} + + [[nodiscard]] const std::vector> &bindings() const { + return Bindings; + } +}; + +class ExprAttrs : public Expr { + std::shared_ptr Body; + std::shared_ptr Rec; + +public: + ExprAttrs(RangeTy Range, std::shared_ptr Body, + std::shared_ptr Rec) + : Expr(NK_ExprAttrs, Range), Body(std::move(Body)), Rec(std::move(Rec)) {} + + [[nodiscard]] const std::shared_ptr &binds() const { return Body; } + [[nodiscard]] const std::shared_ptr &rec() const { return Rec; } + + [[nodiscard]] bool isRecursive() const { return Rec != nullptr; } +}; + } // namespace nixf diff --git a/libnixf/src/Parse/Parser.cpp b/libnixf/src/Parse/Parser.cpp index fa3b11610..6bfa7e416 100644 --- a/libnixf/src/Parse/Parser.cpp +++ b/libnixf/src/Parse/Parser.cpp @@ -227,7 +227,7 @@ class Parser { } } - std::shared_ptr parseString(bool IsIndented) { + std::shared_ptr parseString(bool IsIndented) { Token Quote = peek(); TokenKind QuoteKind = IsIndented ? tok_quote2 : tok_dquote; std::string QuoteSpel(tok::spelling(QuoteKind)); @@ -301,6 +301,174 @@ class Parser { std::move(Expr), std::move(LParen), /*RParen=*/nullptr); } + // attrname : ID + // | string + // | interpolation + std::shared_ptr parseAttrName() { + switch (Token Tok = peek(); Tok.kind()) { + case tok_kw_or: + Diags.emplace_back(Diagnostic::DK_OrIdentifier, Tok.range()); + [[fallthrough]]; + case tok_id: { + consume(); + auto ID = + std::make_shared(Tok.range(), std::string(Tok.view())); + return std::make_shared(std::move(ID), Tok.range()); + } + case tok_dquote: { + consume(); + std::shared_ptr String = parseString(/*IsIndented=*/false); + return std::make_shared(std::move(String), Tok.range()); + } + case tok_dollar_curly: { + std::shared_ptr Expr = parseInterpolation(); + return std::make_shared(std::move(Expr), Tok.range()); + } + default: + return nullptr; + } + } + + // attrpath : attrname ('.' attrname)* + std::shared_ptr parseAttrPath() { + auto First = parseAttrName(); + if (!First) + return nullptr; + assert(LastToken && "LastToken should be set after valid attrname"); + std::vector> AttrNames; + AttrNames.emplace_back(std::move(First)); + Point Begin = peek().begin(); + while (true) { + if (Token Tok = peek(); Tok.kind() == tok_dot) { + consume(); + auto Next = parseAttrName(); + if (!Next) { + // extra ".", consider remove it. + Diagnostic &D = + Diags.emplace_back(Diagnostic::DK_AttrPathExtraDot, Tok.range()); + D.fix("remove extra .").edit(TextEdit::mkRemoval(Tok.range())); + D.fix("insert dummy attrname") + .edit(TextEdit::mkInsertion(Tok.range().end(), R"("dummy")")); + } + AttrNames.emplace_back(std::move(Next)); + continue; + } + break; + } + return std::make_shared( + RangeTy{ + Begin, + LastToken->end(), + }, + std::move(AttrNames)); + } + + // binding : attrpath '=' expr ';' + std::shared_ptr parseBinding() { + auto Path = parseAttrPath(); + if (!Path) + return nullptr; + assert(LastToken && "LastToken should be set after valid attrpath"); + if (Token Tok = peek(); Tok.kind() == tok_eq) { + consume(); + } else { + // expected "=" for binding + Diagnostic &D = Diags.emplace_back(Diagnostic::DK_Expected, + RangeTy(LastToken->end())); + D << std::string(tok::spelling(tok_eq)); + D.fix("insert =").edit(TextEdit::mkInsertion(LastToken->end(), "=")); + } + auto Expr = parseExpr(); + if (!Expr) + diagNullExpr(Diags, LastToken->end(), "binding"); + if (Token Tok = peek(); Tok.kind() == tok_semi_colon) { + consume(); + } else { + // TODO: reset the cursor for error recovery. + // (https://github.com/nix-community/nixd/blob/2b0ca8cef0d13823132a52b6cd6f6d7372482664/libnixf/lib/Parse/Parser.cpp#L337) + // expected ";" for binding + Diagnostic &D = Diags.emplace_back(Diagnostic::DK_Expected, + RangeTy(LastToken->end())); + D << std::string(tok::spelling(tok_semi_colon)); + D.fix("insert ;").edit(TextEdit::mkInsertion(LastToken->end(), ";")); + } + return std::make_shared( + RangeTy{ + Path->begin(), + LastToken->end(), + }, + std::move(Path), std::move(Expr)); + } + + // binds : ( binding | inherit )* + std::shared_ptr parseBinds() { + // TODO: curently we don't support inherit + auto First = parseBinding(); + if (!First) + return nullptr; + assert(LastToken && "LastToken should be set after valid binding"); + std::vector> Bindings; + Bindings.emplace_back(std::move(First)); + Point Begin = peek().begin(); + while (true) { + if (auto Next = parseBinding()) { + Bindings.emplace_back(std::move(Next)); + continue; + } + break; + } + return std::make_shared( + RangeTy{ + Begin, + LastToken->end(), + }, + std::move(Bindings)); + } + + // attrset_expr : REC? '{' binds '}' + // + // Note: peek `tok_kw_rec` or `tok_l_curly` before calling this function. + std::shared_ptr parseExprAttrs() { + std::shared_ptr Rec; + + // "to match this ..." + // if "{" is missing, then use "rec", otherwise use "{" + Token Matcher = peek(); + Point Begin = peek().begin(); // rec or { + if (Token Tok = peek(); Tok.kind() == tok_kw_rec) { + consume(); + Rec = std::make_shared(Tok.range()); + } + if (Token Tok = peek(); Tok.kind() == tok_l_curly) { + // "{" is found, use it as matcher. + Matcher = Tok; + consume(); + } else { + // expected "{" for attrset + Point InsertPoint = LastToken ? LastToken->end() : peek().begin(); + Diagnostic &D = + Diags.emplace_back(Diagnostic::DK_Expected, RangeTy(InsertPoint)); + D << std::string(tok::spelling(tok_l_curly)); + D.fix("insert {").edit(TextEdit::mkInsertion(InsertPoint, "{")); + } + assert(LastToken && "LastToken should be set after valid { or rec"); + auto Binds = parseBinds(); + if (Token Tok = peek(); Tok.kind() == tok_r_curly) { + consume(); + } else { + // expected "}" for attrset + Point InsertPoint = LastToken ? LastToken->end() : peek().begin(); + Diagnostic &D = + Diags.emplace_back(Diagnostic::DK_Expected, RangeTy(InsertPoint)); + D << std::string(tok::spelling(tok_r_curly)); + D.note(Note::NK_ToMachThis, Matcher.range()) + << std::string(tok::spelling(Matcher.kind())); + D.fix("insert }").edit(TextEdit::mkInsertion(InsertPoint, "}")); + } + return std::make_shared(RangeTy{Begin, LastToken->end()}, + std::move(Binds), std::move(Rec)); + } + /// expr_simple : INT /// | FLOAT /// | string @@ -336,6 +504,9 @@ class Parser { return parseExprPath(); case tok_l_paren: return parseExprParen(); + case tok_kw_rec: + case tok_l_curly: + return parseExprAttrs(); default: return nullptr; } diff --git a/libnixf/src/Parse/Token.h b/libnixf/src/Parse/Token.h index c444921dd..55e87931a 100644 --- a/libnixf/src/Parse/Token.h +++ b/libnixf/src/Parse/Token.h @@ -28,12 +28,18 @@ constexpr std::string_view spelling(TokenKind Kind) { return "''"; case tok_dollar_curly: return "${"; + case tok_l_curly: + return "{"; case tok_r_curly: return "}"; case tok_l_paren: return "("; case tok_r_paren: return ")"; + case tok_eq: + return "="; + case tok_semi_colon: + return ";"; default: assert(false && "Not yet implemented!"); } diff --git a/libnixf/test/Parse/Parser.cpp b/libnixf/test/Parse/Parser.cpp index 8ae83eb89..8622b63f7 100644 --- a/libnixf/test/Parse/Parser.cpp +++ b/libnixf/test/Parse/Parser.cpp @@ -393,4 +393,87 @@ TEST(Parser, ParenNullExpr) { ASSERT_EQ(F.newText(), " expr"); } +TEST(Parser, AttrsOK) { + auto Src = R"({})"sv; + + std::vector Diags; + auto AST = nixf::parse(Src, Diags); + + ASSERT_TRUE(AST); + ASSERT_EQ(AST->kind(), Node::NK_ExprAttrs); + ASSERT_TRUE(AST->range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(AST->range().end().isAt(0, 2, 2)); + ASSERT_FALSE(static_cast(AST.get())->isRecursive()); + + ASSERT_EQ(Diags.size(), 0); +} + +TEST(Parser, RecAttrsOK) { + auto Src = R"(rec { })"sv; + + std::vector Diags; + auto AST = nixf::parse(Src, Diags); + + ASSERT_TRUE(AST); + ASSERT_EQ(AST->kind(), Node::NK_ExprAttrs); + ASSERT_TRUE(AST->range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(AST->range().end().isAt(0, 7, 7)); + ASSERT_TRUE(static_cast(AST.get())->isRecursive()); + + ASSERT_EQ(Diags.size(), 0); +} + +TEST(Parser, RecAttrsMissingLCurly) { + auto Src = R"(rec )"sv; + + std::vector Diags; + auto AST = nixf::parse(Src, Diags); + + ASSERT_TRUE(AST); + ASSERT_EQ(AST->kind(), Node::NK_ExprAttrs); + ASSERT_TRUE(AST->range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(AST->range().end().isAt(0, 3, 3)); + ASSERT_TRUE(static_cast(AST.get())->isRecursive()); + + ASSERT_EQ(Diags.size(), 2); + auto &D = Diags; + ASSERT_TRUE(D[0].range().begin().isAt(0, 3, 3)); + ASSERT_TRUE(D[0].range().end().isAt(0, 3, 3)); + ASSERT_EQ(D[0].kind(), Diagnostic::DK_Expected); + ASSERT_EQ(D[0].args().size(), 1); + ASSERT_EQ(D[0].args()[0], "{"); + ASSERT_TRUE(D[0].range().begin().isAt(0, 3, 3)); + ASSERT_TRUE(D[0].range().end().isAt(0, 3, 3)); + ASSERT_EQ(D[1].kind(), Diagnostic::DK_Expected); + ASSERT_EQ(D[1].args().size(), 1); + ASSERT_EQ(D[1].args()[0], "}"); + + // Check the note. + ASSERT_EQ(D[0].notes().size(), 0); + ASSERT_EQ(D[1].notes().size(), 1); + const auto &N = D[1].notes()[0]; + ASSERT_TRUE(N.range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(N.range().end().isAt(0, 3, 3)); + ASSERT_EQ(N.kind(), Note::NK_ToMachThis); + ASSERT_EQ(N.args().size(), 1); + ASSERT_EQ(N.args()[0], "rec"); + + // Check fix-it hints. + ASSERT_EQ(D[0].fixes().size(), 1); + ASSERT_EQ(D[0].fixes()[0].edits().size(), 1); + ASSERT_EQ(D[0].fixes()[0].message(), "insert {"); + const auto &F = D[0].fixes()[0].edits()[0]; + ASSERT_TRUE(F.oldRange().begin().isAt(0, 3, 3)); + ASSERT_TRUE(F.oldRange().end().isAt(0, 3, 3)); + ASSERT_EQ(F.newText(), "{"); + + ASSERT_EQ(D[1].fixes().size(), 1); + ASSERT_EQ(D[1].fixes()[0].edits().size(), 1); + ASSERT_EQ(D[1].fixes()[0].message(), "insert }"); + const auto &F2 = D[1].fixes()[0].edits()[0]; + ASSERT_TRUE(F2.oldRange().begin().isAt(0, 3, 3)); + ASSERT_TRUE(F2.oldRange().end().isAt(0, 3, 3)); + ASSERT_EQ(F2.newText(), "}"); +} + } // namespace From d54209f0120439dd3434780671b54dc213f45086 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 23 Jan 2024 23:25:43 +0800 Subject: [PATCH 2/4] lbinixf: add testing identifier `or` for parsing attrs --- libnixf/test/Parse/Parser.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libnixf/test/Parse/Parser.cpp b/libnixf/test/Parse/Parser.cpp index 8622b63f7..9a0498bd6 100644 --- a/libnixf/test/Parse/Parser.cpp +++ b/libnixf/test/Parse/Parser.cpp @@ -476,4 +476,23 @@ TEST(Parser, RecAttrsMissingLCurly) { ASSERT_EQ(F2.newText(), "}"); } +TEST(Parser, AttrsOrID) { + auto Src = R"({ or = 1; })"sv; + + std::vector Diags; + auto AST = nixf::parse(Src, Diags); + + ASSERT_TRUE(AST); + ASSERT_EQ(AST->kind(), Node::NK_ExprAttrs); + ASSERT_TRUE(AST->range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(AST->range().end().isAt(0, 11, 11)); + ASSERT_FALSE(static_cast(AST.get())->isRecursive()); + + ASSERT_EQ(Diags.size(), 1); + auto &D = Diags[0]; + ASSERT_TRUE(D.range().begin().isAt(0, 2, 2)); + ASSERT_TRUE(D.range().end().isAt(0, 4, 4)); + ASSERT_EQ(D.kind(), Diagnostic::DK_OrIdentifier); +} + } // namespace From 4d6abe9ac71c792b9c5468ee1f87ea5126697dd2 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 23 Jan 2024 23:27:26 +0800 Subject: [PATCH 3/4] libnixf: assert & use LastToken for attrs error recovery --- libnixf/src/Parse/Parser.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libnixf/src/Parse/Parser.cpp b/libnixf/src/Parse/Parser.cpp index 6bfa7e416..8c15cd805 100644 --- a/libnixf/src/Parse/Parser.cpp +++ b/libnixf/src/Parse/Parser.cpp @@ -445,11 +445,11 @@ class Parser { consume(); } else { // expected "{" for attrset - Point InsertPoint = LastToken ? LastToken->end() : peek().begin(); - Diagnostic &D = - Diags.emplace_back(Diagnostic::DK_Expected, RangeTy(InsertPoint)); + assert(LastToken && "LastToken should be set after valid rec"); + Diagnostic &D = Diags.emplace_back(Diagnostic::DK_Expected, + RangeTy(LastToken->range())); D << std::string(tok::spelling(tok_l_curly)); - D.fix("insert {").edit(TextEdit::mkInsertion(InsertPoint, "{")); + D.fix("insert {").edit(TextEdit::mkInsertion(LastToken->end(), "{")); } assert(LastToken && "LastToken should be set after valid { or rec"); auto Binds = parseBinds(); @@ -457,13 +457,12 @@ class Parser { consume(); } else { // expected "}" for attrset - Point InsertPoint = LastToken ? LastToken->end() : peek().begin(); - Diagnostic &D = - Diags.emplace_back(Diagnostic::DK_Expected, RangeTy(InsertPoint)); + Diagnostic &D = Diags.emplace_back(Diagnostic::DK_Expected, + RangeTy(LastToken->range())); D << std::string(tok::spelling(tok_r_curly)); D.note(Note::NK_ToMachThis, Matcher.range()) << std::string(tok::spelling(Matcher.kind())); - D.fix("insert }").edit(TextEdit::mkInsertion(InsertPoint, "}")); + D.fix("insert }").edit(TextEdit::mkInsertion(LastToken->end(), "}")); } return std::make_shared(RangeTy{Begin, LastToken->end()}, std::move(Binds), std::move(Rec)); From 345f48d27d8debbdb1d67d3086057d7f02a9d75f Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 23 Jan 2024 23:50:00 +0800 Subject: [PATCH 4/4] libnixf: fix string attr & add tests --- libnixf/src/Parse/Parser.cpp | 3 +-- libnixf/test/Parse/Parser.cpp | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/libnixf/src/Parse/Parser.cpp b/libnixf/src/Parse/Parser.cpp index 8c15cd805..b767b404b 100644 --- a/libnixf/src/Parse/Parser.cpp +++ b/libnixf/src/Parse/Parser.cpp @@ -316,7 +316,6 @@ class Parser { return std::make_shared(std::move(ID), Tok.range()); } case tok_dquote: { - consume(); std::shared_ptr String = parseString(/*IsIndented=*/false); return std::make_shared(std::move(String), Tok.range()); } @@ -447,7 +446,7 @@ class Parser { // expected "{" for attrset assert(LastToken && "LastToken should be set after valid rec"); Diagnostic &D = Diags.emplace_back(Diagnostic::DK_Expected, - RangeTy(LastToken->range())); + RangeTy(LastToken->end())); D << std::string(tok::spelling(tok_l_curly)); D.fix("insert {").edit(TextEdit::mkInsertion(LastToken->end(), "{")); } diff --git a/libnixf/test/Parse/Parser.cpp b/libnixf/test/Parse/Parser.cpp index 9a0498bd6..d0dbf7882 100644 --- a/libnixf/test/Parse/Parser.cpp +++ b/libnixf/test/Parse/Parser.cpp @@ -495,4 +495,55 @@ TEST(Parser, AttrsOrID) { ASSERT_EQ(D.kind(), Diagnostic::DK_OrIdentifier); } +TEST(Parser, AttrsOKSpecialAttr) { + auto Src = R"({ a.b."foo".${"bar"} = 1; })"sv; + + std::vector Diags; + auto AST = nixf::parse(Src, Diags); + + ASSERT_TRUE(AST); + ASSERT_EQ(AST->kind(), Node::NK_ExprAttrs); + ASSERT_TRUE(AST->range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(AST->range().end().isAt(0, 27, 27)); + + ASSERT_EQ(Diags.size(), 0); +} + +TEST(Parser, AttrsExtraDot) { + auto Src = R"({ a.b. = 1; })"sv; + + std::vector Diags; + auto AST = nixf::parse(Src, Diags); + + ASSERT_TRUE(AST); + ASSERT_EQ(AST->kind(), Node::NK_ExprAttrs); + ASSERT_TRUE(AST->range().begin().isAt(0, 0, 0)); + ASSERT_TRUE(AST->range().end().isAt(0, 13, 13)); + + ASSERT_EQ(Diags.size(), 1); + auto &D = Diags[0]; + ASSERT_TRUE(D.range().begin().isAt(0, 5, 5)); + ASSERT_TRUE(D.range().end().isAt(0, 6, 6)); + ASSERT_EQ(D.kind(), Diagnostic::DK_AttrPathExtraDot); + + // Check that the note is correct. + ASSERT_EQ(D.notes().size(), 0); + + // Check fix-it hints. + ASSERT_EQ(D.fixes().size(), 2); + ASSERT_EQ(D.fixes()[0].edits().size(), 1); + ASSERT_EQ(D.fixes()[0].message(), "remove extra ."); + const auto &F = D.fixes()[0].edits()[0]; + ASSERT_TRUE(F.oldRange().begin().isAt(0, 5, 5)); + ASSERT_TRUE(F.oldRange().end().isAt(0, 6, 6)); + ASSERT_EQ(F.newText(), ""); + + ASSERT_EQ(D.fixes()[1].edits().size(), 1); + ASSERT_EQ(D.fixes()[1].message(), "insert dummy attrname"); + const auto &F2 = D.fixes()[1].edits()[0]; + ASSERT_TRUE(F2.oldRange().begin().isAt(0, 6, 6)); + ASSERT_TRUE(F2.oldRange().end().isAt(0, 6, 6)); + ASSERT_EQ(F2.newText(), "\"dummy\""); +} + } // namespace