diff options
Diffstat (limited to 'src/qmlcompiler/qqmljsshadowcheck.cpp')
-rw-r--r-- | src/qmlcompiler/qqmljsshadowcheck.cpp | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/src/qmlcompiler/qqmljsshadowcheck.cpp b/src/qmlcompiler/qqmljsshadowcheck.cpp new file mode 100644 index 0000000000..d542767dce --- /dev/null +++ b/src/qmlcompiler/qqmljsshadowcheck.cpp @@ -0,0 +1,246 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qqmljsshadowcheck_p.h" + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +/*! + * \internal + * \class QQmlJSShadowCheck + * + * This pass looks for possible shadowing when accessing members of QML-exposed + * types. A member can be shadowed if a non-final property is re-declared in a + * derived class. As the QML engine will always pick up the most derived variant + * of that property, we cannot rely on any property of a type to be actually + * accessible, unless one of a few special cases holds: + * + * 1. We are dealing with a direct scope lookup, without an intermediate object. + * Such lookups are protected from shadowing. For example "property int a: b" + * always works. + * 2. The object we are retrieving the property from is identified by an ID, or + * an attached property or a singleton. Such objects cannot be replaced. + * Therefore we can be sure to see all the type information at compile time. + * 3. The property is declared final. + * 4. The object we are retrieving the property from is a value type. Value + * types cannot be used polymorphically. + * + * If the property is potentially shadowed, we can still retrieve it, but we + * don't know its type. We should assume "var" then. + * + * All of the above also holds for methods. There we have to transform the + * arguments and return types into "var". + */ + +QQmlJSCompilePass::BlocksAndAnnotations QQmlJSShadowCheck::run(const Function *function, + QQmlJS::DiagnosticMessage *error) +{ + m_function = function; + m_error = error; + m_state = initialState(function); + decode(m_function->code.constData(), static_cast<uint>(m_function->code.size())); + + for (const auto &store : m_resettableStores) + checkResettable(store.accumulatorIn, store.instructionOffset); + + // Re-check all base types. We may have made them var after detecting them. + for (const auto &base : m_baseTypes) { + if (checkBaseType(base) == Shadowable) + break; + } + + return { std::move(m_basicBlocks), std::move(m_annotations) }; +} + +void QQmlJSShadowCheck::generate_LoadProperty(int nameIndex) +{ + if (!m_state.readsRegister(Accumulator)) + return; // enum lookup cannot be shadowed. + + auto accumulatorIn = m_state.registers.find(Accumulator); + if (accumulatorIn != m_state.registers.end()) { + checkShadowing( + accumulatorIn.value().content, m_jsUnitGenerator->stringForIndex(nameIndex), + Accumulator); + } +} + +void QQmlJSShadowCheck::generate_GetLookup(int index) +{ + if (!m_state.readsRegister(Accumulator)) + return; // enum lookup cannot be shadowed. + + auto accumulatorIn = m_state.registers.find(Accumulator); + if (accumulatorIn != m_state.registers.end()) { + checkShadowing( + accumulatorIn.value().content, m_jsUnitGenerator->lookupName(index), Accumulator); + } +} + +void QQmlJSShadowCheck::generate_GetOptionalLookup(int index, int offset) +{ + Q_UNUSED(offset); + generate_GetLookup(index); +} + +void QQmlJSShadowCheck::handleStore(int base, const QString &memberName) +{ + const int instructionOffset = currentInstructionOffset(); + const QQmlJSRegisterContent &readAccumulator + = m_annotations[instructionOffset].readRegisters[Accumulator].content; + const auto baseType = m_state.registers[base].content; + + // If the accumulator is already read as var, we don't have to do anything. + if (m_typeResolver->registerContains(readAccumulator, m_typeResolver->varType())) { + if (checkBaseType(baseType) == NotShadowable) + m_baseTypes.append(baseType); + return; + } + + if (checkShadowing(baseType, memberName, base) == Shadowable) + return; + + // If the property isn't shadowable, we have to turn the read register into + // var if the accumulator can hold undefined. This has to be done in a second pass + // because the accumulator may still turn into var due to its own shadowing. + const QQmlJSRegisterContent member = m_typeResolver->memberType(baseType, memberName); + if (member.isProperty()) + m_resettableStores.append({m_state.accumulatorIn(), instructionOffset}); +} + +void QQmlJSShadowCheck::generate_StoreProperty(int nameIndex, int base) +{ + handleStore(base, m_jsUnitGenerator->stringForIndex(nameIndex)); +} + +void QQmlJSShadowCheck::generate_SetLookup(int index, int base) +{ + handleStore(base, m_jsUnitGenerator->lookupName(index)); +} + +void QQmlJSShadowCheck::generate_CallProperty(int nameIndex, int base, int argc, int argv) +{ + Q_UNUSED(argc); + Q_UNUSED(argv); + checkShadowing(m_state.registers[base].content, m_jsUnitGenerator->lookupName(nameIndex), base); +} + +void QQmlJSShadowCheck::generate_CallPropertyLookup(int nameIndex, int base, int argc, int argv) +{ + Q_UNUSED(argc); + Q_UNUSED(argv); + checkShadowing(m_state.registers[base].content, m_jsUnitGenerator->lookupName(nameIndex), base); +} + +QV4::Moth::ByteCodeHandler::Verdict QQmlJSShadowCheck::startInstruction(QV4::Moth::Instr::Type) +{ + m_state = nextStateFromAnnotations(m_state, m_annotations); + return (m_state.hasSideEffects() || m_state.changedRegisterIndex() != InvalidRegister) + ? ProcessInstruction + : SkipInstruction; +} + +void QQmlJSShadowCheck::endInstruction(QV4::Moth::Instr::Type) +{ +} + +QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkShadowing( + const QQmlJSRegisterContent &baseType, const QString &memberName, int baseRegister) +{ + if (checkBaseType(baseType) == Shadowable) + return Shadowable; + else + m_baseTypes.append(baseType); + + if (baseType.storedType()->accessSemantics() != QQmlJSScope::AccessSemantics::Reference) + return NotShadowable; + + switch (baseType.variant()) { + case QQmlJSRegisterContent::ExtensionObjectProperty: + case QQmlJSRegisterContent::ExtensionScopeProperty: + case QQmlJSRegisterContent::MethodReturnValue: + case QQmlJSRegisterContent::ObjectProperty: + case QQmlJSRegisterContent::ScopeProperty: + case QQmlJSRegisterContent::Unknown: { + const QQmlJSRegisterContent member = m_typeResolver->memberType(baseType, memberName); + + // You can have something like parent.QtQuick.Screen.pixelDensity + // In that case "QtQuick" cannot be resolved as member type and we would later have to look + // for "QtQuick.Screen" instead. However, you can only do that with attached properties and + // those are not shadowable. + if (!member.isValid()) { + Q_ASSERT(m_typeResolver->isPrefix(memberName)); + return NotShadowable; + } + + if (member.isProperty()) { + if (member.property().isFinal()) + return NotShadowable; // final properties can't be shadowed + } else if (!member.isMethod()) { + return NotShadowable; // Only properties and methods can be shadowed + } + + m_logger->log( + u"Member %1 of %2 can be shadowed"_s.arg(memberName, baseType.descriptiveName()), + qmlCompiler, currentSourceLocation()); + + // Make it "var". We don't know what it is. + const QQmlJSScope::ConstPtr varType = m_typeResolver->varType(); + const QQmlJSRegisterContent varContent = m_typeResolver->globalType(varType); + InstructionAnnotation ¤tAnnotation = m_annotations[currentInstructionOffset()]; + + if (currentAnnotation.changedRegisterIndex != InvalidRegister) { + m_typeResolver->adjustOriginalType( + currentAnnotation.changedRegister.storedType(), varType); + m_typeResolver->adjustOriginalType( + m_typeResolver->containedType(currentAnnotation.changedRegister), varType); + m_adjustedTypes.insert(currentAnnotation.changedRegister); + } + + for (auto it = currentAnnotation.readRegisters.begin(), + end = currentAnnotation.readRegisters.end(); + it != end; ++it) { + if (it.key() != baseRegister) + it->second.content = m_typeResolver->convert(it->second.content, varContent); + } + return Shadowable; + } + default: + // In particular ObjectById is fine as that cannot change into something else + // Singleton should also be fine, unless the factory function creates an object + // with different property types than the declared class. + return NotShadowable; + } +} + +void QQmlJSShadowCheck::checkResettable( + const QQmlJSRegisterContent &accumulatorIn, int instructionOffset) +{ + const QQmlJSScope::ConstPtr varType = m_typeResolver->varType(); + + // The stored type is not necessarily updated by the shadow check, but it + // will be in the basic blocks pass. For the purpose of adjusting newly + // shadowable types we can ignore it. We only want to know if any of the + // contents can hold undefined. + if (!m_typeResolver->canHoldUndefined(accumulatorIn.storedIn(varType))) + return; + + const QQmlJSRegisterContent varContent = m_typeResolver->globalType(varType); + + QQmlJSRegisterContent &readAccumulator + = m_annotations[instructionOffset].readRegisters[Accumulator].content; + readAccumulator = m_typeResolver->convert(readAccumulator, varContent); +} + +QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkBaseType( + const QQmlJSRegisterContent &baseType) +{ + if (!m_adjustedTypes.contains(baseType)) + return NotShadowable; + setError(u"Cannot use shadowable base type for further lookups: %1"_s.arg(baseType.descriptiveName())); + return Shadowable; +} + +QT_END_NAMESPACE |