aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOlivier De Cannière <[email protected]>2025-04-08 16:32:55 +0200
committerOlivier De Cannière <[email protected]>2025-04-10 13:35:00 +0200
commit3cb8e9b6056d66b409527617d87ae0c12135f6c5 (patch)
tree0e6be24f9154d552e7c45bb03e385f7ac34660e3
parent5ff76ab872de6bae9766d52f4f794256ae9eeebf (diff)
qmllint: Warn about redundant optional chaining
Using an optional lookup '?.' on a base type that cannot be null or undefined or when looking up an enum value is pointless. We generate a normal lookup for this in the compiler, but the bytecode keeps these redundant instructions in. Task-number: QTBUG-135649 Change-Id: I95d8b4ed22afa1bdc46a3f8b3d60a499d43931ac Reviewed-by: Fabian Kosmale <[email protected]>
-rw-r--r--src/qml/doc/src/qmllint/redundantOptionalChaining.qdoc60
-rw-r--r--src/qmlcompiler/qqmljslogger.cpp5
-rw-r--r--src/qmlcompiler/qqmljsloggingutils.h1
-rw-r--r--src/qmlcompiler/qqmljstypepropagator.cpp18
-rw-r--r--src/qmlcompiler/qqmljstypepropagator_p.h1
-rw-r--r--tests/auto/qml/qmllint/data/RedundantOptionalChainingEnums.qml7
-rw-r--r--tests/auto/qml/qmllint/tst_qmllint.cpp8
7 files changed, 99 insertions, 1 deletions
diff --git a/src/qml/doc/src/qmllint/redundantOptionalChaining.qdoc b/src/qml/doc/src/qmllint/redundantOptionalChaining.qdoc
new file mode 100644
index 0000000000..e3ca9154a0
--- /dev/null
+++ b/src/qml/doc/src/qmllint/redundantOptionalChaining.qdoc
@@ -0,0 +1,60 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+\page qmllint-warnings-and-errors-redundant-optional-chaining.html
+\ingroup qmllint-warnings-and-errors
+
+\title Redundant Optional Chaining
+\brief [redundant-optional-chaining] Some optional chaining lookups could be non-optional
+
+\qmllintwarningcategory redundant-optional-chaining
+
+\section1 Redundant Optional Chaining
+
+\section2 What happened?
+Some lookups use optional chaining when it isn't necessary. This can happen when looking up enum
+values or when performing a lookup on a base that cannot be \c{null} or \c{undefined}.
+
+\section2 Why is this bad?
+An optional lookup needs to perform a runtime check that a regular lookup doesn't. These extra
+instructions cannot always be determined to be redundant and optimized out by the tooling. They
+then add an extra runtime performance cost and bloat the program unnecessarily.
+
+Additionally, the warning may hint that the optional lookup was performed on the wrong base in the
+chain. See the next section for a more concrete example.
+
+\section2 Example
+\qml
+// Main.qml
+import QtQml
+
+QtObject {
+ // Main will either be resolved and always work, or throw an exception or fail to compile
+ enum E { A, B, C }
+ property int i: Main?.A
+
+ // A url cannot be null or undefined
+ property url u: ""
+ property string s: u?.toString()
+
+ // Did you mean to make the second lookup optional?
+ property int i: Safe?.Unsafe.i
+}
+\endqml
+To fix these warnings, replace the redundant optional lookups with non-optional ones:
+\qml
+// Main.qml
+import QtQml
+
+QtObject {
+ enum E { A, B, C }
+ property int i: Main.A
+
+ property url u: ""
+ property string s: u.toString()
+
+ property int i: Safe.Unsafe?.i
+}
+\endqml
+*/
diff --git a/src/qmlcompiler/qqmljslogger.cpp b/src/qmlcompiler/qqmljslogger.cpp
index de666e25f3..7b79f50789 100644
--- a/src/qmlcompiler/qqmljslogger.cpp
+++ b/src/qmlcompiler/qqmljslogger.cpp
@@ -106,7 +106,10 @@ using namespace Qt::StringLiterals;
"Warn about using missing enum values.", QtWarningMsg, false, false) \
X(qmlAssignmentInCondition, "assignment-in-condition", "AssignmentInCondition", \
"Warn about using assignment in conditions.", QtWarningMsg, false, false) \
- X(qmlEval, "eval", "Eval", "Warn about uses of eval()", QtWarningMsg, false, false)
+ X(qmlEval, "eval", "Eval", "Warn about uses of eval()", QtWarningMsg, false, false) \
+ X(qmlRedundantOptionalChaining, "redundant-optional-chaining", "RedundantOptionalChaining", \
+ "Warn about optional chaining on non-voidable and non-nullable base", QtWarningMsg, false, \
+ false)
#define X(category, name, setting, description, level, ignored, isDefault) \
const QQmlSA::LoggerWarningId category{ name };
diff --git a/src/qmlcompiler/qqmljsloggingutils.h b/src/qmlcompiler/qqmljsloggingutils.h
index 7dde44bf41..078b7d1a45 100644
--- a/src/qmlcompiler/qqmljsloggingutils.h
+++ b/src/qmlcompiler/qqmljsloggingutils.h
@@ -79,6 +79,7 @@ extern const Q_QMLCOMPILER_EXPORT QQmlSA::LoggerWarningId qmlUncreatableType;
extern const Q_QMLCOMPILER_EXPORT QQmlSA::LoggerWarningId qmlMissingEnumEntry;
extern const Q_QMLCOMPILER_EXPORT QQmlSA::LoggerWarningId qmlAssignmentInCondition;
extern const Q_QMLCOMPILER_EXPORT QQmlSA::LoggerWarningId qmlEval;
+extern const Q_QMLCOMPILER_EXPORT QQmlSA::LoggerWarningId qmlRedundantOptionalChaining;
QT_END_NAMESPACE
diff --git a/src/qmlcompiler/qqmljstypepropagator.cpp b/src/qmlcompiler/qqmljstypepropagator.cpp
index d880e66030..c6fec6643c 100644
--- a/src/qmlcompiler/qqmljstypepropagator.cpp
+++ b/src/qmlcompiler/qqmljstypepropagator.cpp
@@ -1008,6 +1008,24 @@ void QQmlJSTypePropagator::generate_GetOptionalLookup(int index, int offset)
Q_UNUSED(offset);
saveRegisterStateForJump(offset);
propagatePropertyLookup(m_jsUnitGenerator->lookupName(index), index);
+ if (m_passManager)
+ generate_GetOptionalLookup_SAcheck();
+}
+
+void QQmlJSTypePropagator::generate_GetOptionalLookup_SAcheck()
+{
+ auto suggMsg = "Consider using non-optional chaining instead: '?.' -> '.'"_L1;
+ auto suggestion = std::make_optional(QQmlJSFixSuggestion(suggMsg, currentSourceLocation()));
+ if (m_state.accumulatorOut().variant() == QQmlJSRegisterContent::Enum) {
+ m_logger->log("Redundant optional chaining for enum lookup"_L1, qmlRedundantOptionalChaining,
+ currentSourceLocation(), true, true, suggestion);
+ } else if (!m_state.accumulatorIn().containedType()->isReferenceType()
+ && !m_typeResolver->canHoldUndefined(m_state.accumulatorIn())) {
+ auto baseType = m_state.accumulatorIn().containedTypeName();
+ m_logger->log("Redundant optional chaining for lookup on non-voidable and non-nullable "_L1
+ "type %1"_L1.arg(baseType), qmlRedundantOptionalChaining,
+ currentSourceLocation(), true, true, suggestion);
+ }
}
void QQmlJSTypePropagator::generate_StoreProperty_SAcheck(const QString &propertyName,
diff --git a/src/qmlcompiler/qqmljstypepropagator_p.h b/src/qmlcompiler/qqmljstypepropagator_p.h
index 126b129cca..f54d856e8d 100644
--- a/src/qmlcompiler/qqmljstypepropagator_p.h
+++ b/src/qmlcompiler/qqmljstypepropagator_p.h
@@ -277,6 +277,7 @@ private:
const QQmlJSScope::ConstPtr &callBase);
void propagateCall_SAcheck(const QQmlJSMetaMethod &method,
const QQmlJSScope::ConstPtr &baseType);
+ void generate_GetOptionalLookup_SAcheck();
void addError(const QString &message)
{
diff --git a/tests/auto/qml/qmllint/data/RedundantOptionalChainingEnums.qml b/tests/auto/qml/qmllint/data/RedundantOptionalChainingEnums.qml
new file mode 100644
index 0000000000..d4ddb10cdf
--- /dev/null
+++ b/tests/auto/qml/qmllint/data/RedundantOptionalChainingEnums.qml
@@ -0,0 +1,7 @@
+import QtQml
+
+QtObject {
+ enum E { A, B, C }
+ property int e1: RedundantOptionalChainingEnums?.B
+ property int e2: Qt?.Horizontal
+}
diff --git a/tests/auto/qml/qmllint/tst_qmllint.cpp b/tests/auto/qml/qmllint/tst_qmllint.cpp
index 743e20f77f..c10f386faf 100644
--- a/tests/auto/qml/qmllint/tst_qmllint.cpp
+++ b/tests/auto/qml/qmllint/tst_qmllint.cpp
@@ -1286,6 +1286,10 @@ expression: \${expr} \${expr} \\\${expr} \\\${expr}`)",
<< QStringLiteral("settings/propertyAliasCycle/file.qml")
<< Result{ { { "\"cycle1\" is part of an alias cycle"_L1 },
{ "\"cycle1\" is part of an alias cycle"_L1 } } };
+ QTest::newRow("redundantOptionalChainingEnums")
+ << QStringLiteral("RedundantOptionalChainingEnums.qml")
+ << Result{ { { "Redundant optional chaining for enum lookup"_L1, 5, 54 },
+ { "Redundant optional chaining for enum lookup"_L1, 6, 26 } } };
}
void TestQmllint::dirtyQmlCode()
@@ -1458,6 +1462,10 @@ void TestQmllint::dirtyJsSnippet_data()
<< Result{ { { "Do not use 'eval'"_L1, 1, 13 } } };
QTest::newRow("indirectEval2") << u"let x = (1, eval)(\"1 + 1\");"_s
<< Result{ { { "Do not use 'eval'"_L1, 1, 13 } } };
+ QTest::newRow("redundantOptionalChainingNonVoidableBase")
+ << u"/a/?.flags"_s
+ << Result{ { { "Redundant optional chaining for lookup on non-voidable and "_L1
+ "non-nullable type QRegularExpression"_L1, 1, 6 } } };
}
void TestQmllint::dirtyJsSnippet()