diff options
author | Olivier De Cannière <[email protected]> | 2025-04-08 16:32:55 +0200 |
---|---|---|
committer | Olivier De Cannière <[email protected]> | 2025-04-10 13:35:00 +0200 |
commit | 3cb8e9b6056d66b409527617d87ae0c12135f6c5 (patch) | |
tree | 0e6be24f9154d552e7c45bb03e385f7ac34660e3 | |
parent | 5ff76ab872de6bae9766d52f4f794256ae9eeebf (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.qdoc | 60 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljslogger.cpp | 5 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljsloggingutils.h | 1 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljstypepropagator.cpp | 18 | ||||
-rw-r--r-- | src/qmlcompiler/qqmljstypepropagator_p.h | 1 | ||||
-rw-r--r-- | tests/auto/qml/qmllint/data/RedundantOptionalChainingEnums.qml | 7 | ||||
-rw-r--r-- | tests/auto/qml/qmllint/tst_qmllint.cpp | 8 |
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() |