aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSami Shalayel <sami.shalayel@qt.io>2023-12-15 11:48:13 +0100
committerSami Shalayel <sami.shalayel@qt.io>2023-12-29 09:48:18 +0100
commit2b008f242150017415bc427d87e84ac81b95e877 (patch)
tree01629d633db786027d77af779174e121fcf83900
parentff4c2c311f1957ebacb66f0d35406a51da2a544e (diff)
qmlls: adapt parser for attached/grouped properties completion
Add a recovery mode to the parser to accept bindings that are not completely written out, and create an empty statement for that. This mode is called enableIncompleteBindings and is only enabled for qmlls. Also adapt qqmldomastcreator to those empty statements, and qmllsutils. Pick-to: 6.7 Fixes: QTBUG-120169 Task-number: QTBUG-92876 Change-Id: Ic24cbb61e3be08368027371e377bf75ce87fafb1 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Ulf Hermann <ulf.hermann@qt.io>
-rw-r--r--src/qml/parser/qqmljs.g21
-rw-r--r--src/qmldom/qqmldomastcreator.cpp15
-rw-r--r--src/qmldom/qqmldomastcreator_p.h3
-rw-r--r--src/qmldom/qqmldomconstants_p.h2
-rw-r--r--src/qmldom/qqmldomexternalitems.cpp4
-rw-r--r--src/qmldom/qqmldomoutwriter.cpp3
-rw-r--r--src/qmlls/qqmllsutils.cpp17
-rw-r--r--tests/auto/qmlls/utils/tst_qmlls_utils.cpp6
-rw-r--r--tests/auto/quick/qquickloader/tst_qquickloader.cpp12
9 files changed, 71 insertions, 12 deletions
diff --git a/src/qml/parser/qqmljs.g b/src/qml/parser/qqmljs.g
index bc5955245a..bb27ccc96f 100644
--- a/src/qml/parser/qqmljs.g
+++ b/src/qml/parser/qqmljs.g
@@ -308,6 +308,12 @@ public:
inline void enableIdentifierInsertion()
{ m_enableIdentifierInsertion = true; }
+ inline bool incompleteBindings() const
+ { return m_enableIncompleteBindings; }
+
+ inline void enableIncompleteBindings()
+ { m_enableIncompleteBindings = true; }
+
protected:
bool parse(int startToken);
@@ -399,6 +405,7 @@ protected:
QList<DiagnosticMessage> diagnostic_messages;
bool m_enableIdentifierInsertion = false;
+ bool m_enableIncompleteBindings = false;
};
} // end of namespace QQmlJS
@@ -1142,6 +1149,20 @@ case $rule_number:
} break;
./
+UiObjectMember: UiQualifiedId Semicolon;
+/.
+ case $rule_number: {
+ if (!m_enableIncompleteBindings) {
+ diagnostic_messages.append(compileError(loc(1), QLatin1String("Incomplete binding, expected token `:` or `{`")));
+ return false;
+ }
+ AST::EmptyStatement *statement = new (pool) AST::EmptyStatement;
+ statement->semicolonToken = loc(2);
+ AST::UiScriptBinding *node = new (pool) AST::UiScriptBinding(sym(1).UiQualifiedId, statement);
+ sym(1).Node = node;
+ } break;
+./
+
UiPropertyType: T_VAR;
/. case $rule_number: Q_FALLTHROUGH(); ./
UiPropertyType: T_RESERVED_WORD;
diff --git a/src/qmldom/qqmldomastcreator.cpp b/src/qmldom/qqmldomastcreator.cpp
index a4f4a67789..a748038671 100644
--- a/src/qmldom/qqmldomastcreator.cpp
+++ b/src/qmldom/qqmldomastcreator.cpp
@@ -2658,6 +2658,21 @@ void QQmlDomAstCreator::endVisit(AST::PreIncrementExpression *statement)
pushScriptElement(current);
}
+bool QQmlDomAstCreator::visit(AST::EmptyStatement *)
+{
+ return m_enableScriptExpressions;
+}
+
+void QQmlDomAstCreator::endVisit(AST::EmptyStatement *statement)
+{
+ if (!m_enableScriptExpressions)
+ return;
+
+ auto current = makeGenericScriptElement(statement, DomType::ScriptEmptyStatement);
+ current->addLocation(FileLocationRegion::SemicolonTokenRegion, statement->semicolonToken);
+ pushScriptElement(current);
+}
+
bool QQmlDomAstCreator::visit(AST::PreDecrementExpression *)
{
return m_enableScriptExpressions;
diff --git a/src/qmldom/qqmldomastcreator_p.h b/src/qmldom/qqmldomastcreator_p.h
index 4ca4920514..b67687b5e7 100644
--- a/src/qmldom/qqmldomastcreator_p.h
+++ b/src/qmldom/qqmldomastcreator_p.h
@@ -480,6 +480,9 @@ public:
bool visit(AST::PreIncrementExpression *) override;
void endVisit(AST::PreIncrementExpression *) override;
+ bool visit(AST::EmptyStatement *) override;
+ void endVisit(AST::EmptyStatement *) override;
+
// lists of stuff whose children do not need a qqmljsscope: visitation order can be custom
bool visit(AST::ArgumentList *) override;
bool visit(AST::UiParameterList *) override;
diff --git a/src/qmldom/qqmldomconstants_p.h b/src/qmldom/qqmldomconstants_p.h
index 58aeba68e4..5797be9a63 100644
--- a/src/qmldom/qqmldomconstants_p.h
+++ b/src/qmldom/qqmldomconstants_p.h
@@ -225,6 +225,7 @@ enum class DomType {
ScriptBreakStatement,
ScriptContinueStatement,
ScriptConditionalExpression,
+ ScriptEmptyStatement,
ScriptElementStop, // marker to check if a DomType is a scriptelement or not
};
@@ -390,6 +391,7 @@ enum FileLocationRegion : int {
RightBracketRegion,
RightParenthesisRegion,
SecondSemicolonRegion,
+ SemicolonTokenRegion,
SignalKeywordRegion,
ThrowKeywordRegion,
TryKeywordRegion,
diff --git a/src/qmldom/qqmldomexternalitems.cpp b/src/qmldom/qqmldomexternalitems.cpp
index 2860be4c93..1e90c8408b 100644
--- a/src/qmldom/qqmldomexternalitems.cpp
+++ b/src/qmldom/qqmldomexternalitems.cpp
@@ -445,8 +445,10 @@ QmlFile::QmlFile(
QQmlJS::Lexer lexer(m_engine.get());
lexer.setCode(code, /*lineno = */ 1, /*qmlMode=*/true);
QQmlJS::Parser parser(m_engine.get());
- if (option == EnableParserRecovery)
+ if (option == EnableParserRecovery) {
parser.enableIdentifierInsertion();
+ parser.enableIncompleteBindings();
+ }
m_isValid = parser.parse();
const auto diagnostics = parser.diagnosticMessages();
for (const DiagnosticMessage &msg : diagnostics) {
diff --git a/src/qmldom/qqmldomoutwriter.cpp b/src/qmldom/qqmldomoutwriter.cpp
index 9b2977252c..3828545d3b 100644
--- a/src/qmldom/qqmldomoutwriter.cpp
+++ b/src/qmldom/qqmldomoutwriter.cpp
@@ -238,6 +238,9 @@ OutWriter &OutWriter::writeRegion(FileLocationRegion region)
case QuestionMarkTokenRegion:
codeForRegion = u"?"_s;
break;
+ case SemicolonTokenRegion:
+ codeForRegion = u";"_s;
+ break;
// not keywords:
case ImportUriRegion:
diff --git a/src/qmlls/qqmllsutils.cpp b/src/qmlls/qqmllsutils.cpp
index a1a7f13215..203f16cd7f 100644
--- a/src/qmlls/qqmllsutils.cpp
+++ b/src/qmlls/qqmllsutils.cpp
@@ -329,6 +329,12 @@ QList<QQmlLSUtilsItemLocation> QQmlLSUtils::itemsFromTextLocation(const DomItem
"not follow the DomItem Structure.";
continue;
}
+ // the parser inserts empty Script Expressions for bindings that are not completely
+ // written out yet. Ignore them here.
+ if (subItem.domItem.internalKind() == DomType::ScriptExpression
+ && subLoc->info().fullRegion.length == 0) {
+ continue;
+ }
subItem.fileLocation = subLoc;
toDo.append(subItem);
inParentButOutsideChildren = false;
@@ -2186,7 +2192,12 @@ QList<CompletionItem> QQmlLSUtils::suggestJSExpressionCompletion(const DomItem &
QList<CompletionItem> result;
QDuplicateTracker<QString> usedNames;
QQmlJSScope::ConstPtr nearestScope;
- const bool hasQualifier = isFieldMemberAccess(scriptIdentifier);
+
+ // note: there is an edge case, where the user asks for completion right after the dot
+ // of some qualified expression like `root.hello`. In this case, scriptIdentifier is actually
+ // the BinaryExpression instead of the left-hand-side that has not be written down yet.
+ const bool askForCompletionOnDot = isFieldMemberExpression(scriptIdentifier);
+ const bool hasQualifier = isFieldMemberAccess(scriptIdentifier) || askForCompletionOnDot;
if (!hasQualifier) {
result << idsCompletions(scriptIdentifier.component())
@@ -2201,7 +2212,9 @@ QList<CompletionItem> QQmlLSUtils::suggestJSExpressionCompletion(const DomItem &
result << enumerationCompletion(nearestScope, &usedNames);
} else {
- const DomItem owner = scriptIdentifier.directParent().field(Fields::left);
+ const DomItem owner =
+ (askForCompletionOnDot ? scriptIdentifier : scriptIdentifier.directParent())
+ .field(Fields::left);
auto expressionType = QQmlLSUtils::resolveExpressionType(
owner, ResolveActualTypeForFieldMemberExpression);
if (!expressionType || !expressionType->semanticScope)
diff --git a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp
index 234ea14677..a1d41c2fb7 100644
--- a/tests/auto/qmlls/utils/tst_qmlls_utils.cpp
+++ b/tests/auto/qmlls/utils/tst_qmlls_utils.cpp
@@ -3201,7 +3201,7 @@ void tst_qmlls_utils::completions_data()
u"bad"_s };
QTest::newRow("groupedPropertyAfterDotMissingRHS")
- << testFile("completions/attachedPropertyMissingRHS.qml") << 7 << 11
+ << testFile("completions/groupedPropertyMissingRHS.qml") << 7 << 11
<< ExpectedCompletions({
{ u"family"_s, CompletionItemKind::Property },
})
@@ -3231,10 +3231,6 @@ void tst_qmlls_utils::completions()
"Current parser cannot recover from this error yet!", Abort);
QEXPECT_FAIL("binaryExpressionMissingRHSWithStatement",
"Current parser cannot recover from this error yet!", Abort);
- QEXPECT_FAIL("attachedPropertyAfterDotMissingRHS",
- "Current parser cannot recover from this error yet!", Abort);
- QEXPECT_FAIL("groupedPropertyAfterDotMissingRHS",
- "Current parser cannot recover from this error yet!", Abort);
QCOMPARE(locations.size(), 1);
QString code;
diff --git a/tests/auto/quick/qquickloader/tst_qquickloader.cpp b/tests/auto/quick/qquickloader/tst_qquickloader.cpp
index 709deddc91..d1093a10e4 100644
--- a/tests/auto/quick/qquickloader/tst_qquickloader.cpp
+++ b/tests/auto/quick/qquickloader/tst_qquickloader.cpp
@@ -752,8 +752,10 @@ void tst_QQuickLoader::initialPropertyValuesError_data()
QTest::newRow("nonexistent source url") << testFileUrl("initialPropertyValues.error.2.qml")
<< (QStringList() << QString(testFileUrl("NonexistentSourceComponent.qml").toString() + ": No such file or directory"));
- QTest::newRow("invalid source url") << testFileUrl("initialPropertyValues.error.3.qml")
- << (QStringList() << QString(testFileUrl("InvalidSourceComponent.qml").toString() + ":5:1: Expected token `:'"));
+ QTest::newRow("invalid source url")
+ << testFileUrl("initialPropertyValues.error.3.qml")
+ << (QStringList() << QString(testFileUrl("InvalidSourceComponent.qml").toString()
+ + ":4:5: Incomplete binding, expected token `:` or `{`"));
QTest::newRow("invalid initial property values object with invalid property access") << testFileUrl("initialPropertyValues.error.4.qml")
<< (QStringList() << QString(testFileUrl("initialPropertyValues.error.4.qml").toString() + ":7:5: QML Loader: setSource: value is not an object")
@@ -898,8 +900,10 @@ void tst_QQuickLoader::asynchronous_data()
QTest::newRow("Non-existent component") << testFileUrl("IDoNotExist.qml")
<< (QStringList() << QString(testFileUrl("IDoNotExist.qml").toString() + ": No such file or directory"));
- QTest::newRow("Invalid component") << testFileUrl("InvalidSourceComponent.qml")
- << (QStringList() << QString(testFileUrl("InvalidSourceComponent.qml").toString() + ":5:1: Expected token `:'"));
+ QTest::newRow("Invalid component")
+ << testFileUrl("InvalidSourceComponent.qml")
+ << (QStringList() << QString(testFileUrl("InvalidSourceComponent.qml").toString()
+ + ":4:5: Incomplete binding, expected token `:` or `{`"));
}
void tst_QQuickLoader::asynchronous()