| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
| 3 | |
| 4 | #include "qqmljsshadowcheck_p.h" |
| 5 | |
| 6 | QT_BEGIN_NAMESPACE |
| 7 | |
| 8 | using namespace Qt::StringLiterals; |
| 9 | |
| 10 | /*! |
| 11 | * \internal |
| 12 | * \class QQmlJSShadowCheck |
| 13 | * |
| 14 | * This pass looks for possible shadowing when accessing members of QML-exposed |
| 15 | * types. A member can be shadowed if a non-final property is re-declared in a |
| 16 | * derived class. As the QML engine will always pick up the most derived variant |
| 17 | * of that property, we cannot rely on any property of a type to be actually |
| 18 | * accessible, unless one of a few special cases holds: |
| 19 | * |
| 20 | * 1. We are dealing with a direct scope lookup, without an intermediate object. |
| 21 | * Such lookups are protected from shadowing. For example "property int a: b" |
| 22 | * always works. |
| 23 | * 2. The object we are retrieving the property from is identified by an ID, or |
| 24 | * an attached property or a singleton. Such objects cannot be replaced. |
| 25 | * Therefore we can be sure to see all the type information at compile time. |
| 26 | * 3. The property is declared final. |
| 27 | * 4. The object we are retrieving the property from is a value type. Value |
| 28 | * types cannot be used polymorphically. |
| 29 | * |
| 30 | * If the property is potentially shadowed, we can still retrieve it, but we |
| 31 | * don't know its type. We should assume "var" then. |
| 32 | * |
| 33 | * All of the above also holds for methods. There we have to transform the |
| 34 | * arguments and return types into "var". |
| 35 | */ |
| 36 | |
| 37 | QQmlJSCompilePass::BlocksAndAnnotations QQmlJSShadowCheck::run(const Function *function, |
| 38 | QQmlJS::DiagnosticMessage *error) |
| 39 | { |
| 40 | m_function = function; |
| 41 | m_error = error; |
| 42 | m_state = initialState(function); |
| 43 | decode(code: m_function->code.constData(), len: static_cast<uint>(m_function->code.size())); |
| 44 | |
| 45 | for (const auto &store : m_resettableStores) |
| 46 | checkResettable(accumulatorIn: store.accumulatorIn, instructionOffset: store.instructionOffset); |
| 47 | |
| 48 | // Re-check all base types. We may have made them var after detecting them. |
| 49 | for (const auto &base : m_baseTypes) { |
| 50 | if (checkBaseType(baseType: base) == Shadowable) |
| 51 | break; |
| 52 | } |
| 53 | |
| 54 | return { .basicBlocks: std::move(m_basicBlocks), .annotations: std::move(m_annotations) }; |
| 55 | } |
| 56 | |
| 57 | void QQmlJSShadowCheck::generate_LoadProperty(int nameIndex) |
| 58 | { |
| 59 | if (!m_state.readsRegister(registerIndex: Accumulator)) |
| 60 | return; // enum lookup cannot be shadowed. |
| 61 | |
| 62 | auto accumulatorIn = m_state.registers.find(key: Accumulator); |
| 63 | if (accumulatorIn != m_state.registers.end()) { |
| 64 | checkShadowing( |
| 65 | baseType: accumulatorIn.value().content, propertyName: m_jsUnitGenerator->stringForIndex(index: nameIndex), |
| 66 | baseRegister: Accumulator); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | void QQmlJSShadowCheck::generate_GetLookup(int index) |
| 71 | { |
| 72 | if (!m_state.readsRegister(registerIndex: Accumulator)) |
| 73 | return; // enum lookup cannot be shadowed. |
| 74 | |
| 75 | auto accumulatorIn = m_state.registers.find(key: Accumulator); |
| 76 | if (accumulatorIn != m_state.registers.end()) { |
| 77 | checkShadowing( |
| 78 | baseType: accumulatorIn.value().content, propertyName: m_jsUnitGenerator->lookupName(index), baseRegister: Accumulator); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | void QQmlJSShadowCheck::generate_GetOptionalLookup(int index, int offset) |
| 83 | { |
| 84 | Q_UNUSED(offset); |
| 85 | generate_GetLookup(index); |
| 86 | } |
| 87 | |
| 88 | void QQmlJSShadowCheck::handleStore(int base, const QString &memberName) |
| 89 | { |
| 90 | const int instructionOffset = currentInstructionOffset(); |
| 91 | const QQmlJSRegisterContent &readAccumulator |
| 92 | = m_annotations[instructionOffset].readRegisters[Accumulator].content; |
| 93 | const auto baseType = m_state.registers[base].content; |
| 94 | |
| 95 | // If the accumulator is already read as var, we don't have to do anything. |
| 96 | if (m_typeResolver->registerContains(reg: readAccumulator, type: m_typeResolver->varType())) { |
| 97 | if (checkBaseType(baseType) == NotShadowable) |
| 98 | m_baseTypes.append(t: baseType); |
| 99 | return; |
| 100 | } |
| 101 | |
| 102 | if (checkShadowing(baseType, propertyName: memberName, baseRegister: base) == Shadowable) |
| 103 | return; |
| 104 | |
| 105 | // If the property isn't shadowable, we have to turn the read register into |
| 106 | // var if the accumulator can hold undefined. This has to be done in a second pass |
| 107 | // because the accumulator may still turn into var due to its own shadowing. |
| 108 | const QQmlJSRegisterContent member = m_typeResolver->memberType(type: baseType, name: memberName); |
| 109 | if (member.isProperty()) |
| 110 | m_resettableStores.append(t: {.accumulatorIn: m_state.accumulatorIn(), .instructionOffset: instructionOffset}); |
| 111 | } |
| 112 | |
| 113 | void QQmlJSShadowCheck::generate_StoreProperty(int nameIndex, int base) |
| 114 | { |
| 115 | handleStore(base, memberName: m_jsUnitGenerator->stringForIndex(index: nameIndex)); |
| 116 | } |
| 117 | |
| 118 | void QQmlJSShadowCheck::generate_SetLookup(int index, int base) |
| 119 | { |
| 120 | handleStore(base, memberName: m_jsUnitGenerator->lookupName(index)); |
| 121 | } |
| 122 | |
| 123 | void QQmlJSShadowCheck::generate_CallProperty(int nameIndex, int base, int argc, int argv) |
| 124 | { |
| 125 | Q_UNUSED(argc); |
| 126 | Q_UNUSED(argv); |
| 127 | checkShadowing(baseType: m_state.registers[base].content, propertyName: m_jsUnitGenerator->lookupName(index: nameIndex), baseRegister: base); |
| 128 | } |
| 129 | |
| 130 | void QQmlJSShadowCheck::generate_CallPropertyLookup(int nameIndex, int base, int argc, int argv) |
| 131 | { |
| 132 | Q_UNUSED(argc); |
| 133 | Q_UNUSED(argv); |
| 134 | checkShadowing(baseType: m_state.registers[base].content, propertyName: m_jsUnitGenerator->lookupName(index: nameIndex), baseRegister: base); |
| 135 | } |
| 136 | |
| 137 | QV4::Moth::ByteCodeHandler::Verdict QQmlJSShadowCheck::startInstruction(QV4::Moth::Instr::Type) |
| 138 | { |
| 139 | m_state = nextStateFromAnnotations(oldState: m_state, annotations: m_annotations); |
| 140 | return (m_state.hasSideEffects() || m_state.changedRegisterIndex() != InvalidRegister) |
| 141 | ? ProcessInstruction |
| 142 | : SkipInstruction; |
| 143 | } |
| 144 | |
| 145 | void QQmlJSShadowCheck::endInstruction(QV4::Moth::Instr::Type) |
| 146 | { |
| 147 | } |
| 148 | |
| 149 | QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkShadowing( |
| 150 | const QQmlJSRegisterContent &baseType, const QString &memberName, int baseRegister) |
| 151 | { |
| 152 | if (checkBaseType(baseType) == Shadowable) |
| 153 | return Shadowable; |
| 154 | else |
| 155 | m_baseTypes.append(t: baseType); |
| 156 | |
| 157 | if (baseType.storedType()->accessSemantics() != QQmlJSScope::AccessSemantics::Reference) |
| 158 | return NotShadowable; |
| 159 | |
| 160 | switch (baseType.variant()) { |
| 161 | case QQmlJSRegisterContent::ExtensionObjectProperty: |
| 162 | case QQmlJSRegisterContent::ExtensionScopeProperty: |
| 163 | case QQmlJSRegisterContent::MethodReturnValue: |
| 164 | case QQmlJSRegisterContent::ObjectProperty: |
| 165 | case QQmlJSRegisterContent::ScopeProperty: |
| 166 | case QQmlJSRegisterContent::Unknown: { |
| 167 | const QQmlJSRegisterContent member = m_typeResolver->memberType(type: baseType, name: memberName); |
| 168 | |
| 169 | // You can have something like parent.QtQuick.Screen.pixelDensity |
| 170 | // In that case "QtQuick" cannot be resolved as member type and we would later have to look |
| 171 | // for "QtQuick.Screen" instead. However, you can only do that with attached properties and |
| 172 | // those are not shadowable. |
| 173 | if (!member.isValid()) { |
| 174 | Q_ASSERT(m_typeResolver->isPrefix(memberName)); |
| 175 | return NotShadowable; |
| 176 | } |
| 177 | |
| 178 | if (member.isProperty()) { |
| 179 | if (member.property().isFinal()) |
| 180 | return NotShadowable; // final properties can't be shadowed |
| 181 | } else if (!member.isMethod()) { |
| 182 | return NotShadowable; // Only properties and methods can be shadowed |
| 183 | } |
| 184 | |
| 185 | m_logger->log( |
| 186 | message: u"Member %1 of %2 can be shadowed"_s .arg(args: memberName, args: baseType.descriptiveName()), |
| 187 | id: qmlCompiler, srcLocation: currentSourceLocation()); |
| 188 | |
| 189 | // Make it "var". We don't know what it is. |
| 190 | const QQmlJSScope::ConstPtr varType = m_typeResolver->varType(); |
| 191 | const QQmlJSRegisterContent varContent = m_typeResolver->globalType(type: varType); |
| 192 | InstructionAnnotation ¤tAnnotation = m_annotations[currentInstructionOffset()]; |
| 193 | |
| 194 | if (currentAnnotation.changedRegisterIndex != InvalidRegister) { |
| 195 | m_typeResolver->adjustOriginalType( |
| 196 | tracked: currentAnnotation.changedRegister.storedType(), conversion: varType); |
| 197 | m_typeResolver->adjustOriginalType( |
| 198 | tracked: m_typeResolver->containedType(container: currentAnnotation.changedRegister), conversion: varType); |
| 199 | m_adjustedTypes.insert(value: currentAnnotation.changedRegister); |
| 200 | } |
| 201 | |
| 202 | for (auto it = currentAnnotation.readRegisters.begin(), |
| 203 | end = currentAnnotation.readRegisters.end(); |
| 204 | it != end; ++it) { |
| 205 | if (it.key() != baseRegister) |
| 206 | it->second.content = m_typeResolver->convert(from: it->second.content, to: varContent); |
| 207 | } |
| 208 | return Shadowable; |
| 209 | } |
| 210 | default: |
| 211 | // In particular ObjectById is fine as that cannot change into something else |
| 212 | // Singleton should also be fine, unless the factory function creates an object |
| 213 | // with different property types than the declared class. |
| 214 | return NotShadowable; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | void QQmlJSShadowCheck::checkResettable( |
| 219 | const QQmlJSRegisterContent &accumulatorIn, int instructionOffset) |
| 220 | { |
| 221 | const QQmlJSScope::ConstPtr varType = m_typeResolver->varType(); |
| 222 | |
| 223 | // The stored type is not necessarily updated by the shadow check, but it |
| 224 | // will be in the basic blocks pass. For the purpose of adjusting newly |
| 225 | // shadowable types we can ignore it. We only want to know if any of the |
| 226 | // contents can hold undefined. |
| 227 | if (!m_typeResolver->canHoldUndefined(content: accumulatorIn.storedIn(newStoredType: varType))) |
| 228 | return; |
| 229 | |
| 230 | const QQmlJSRegisterContent varContent = m_typeResolver->globalType(type: varType); |
| 231 | |
| 232 | QQmlJSRegisterContent &readAccumulator |
| 233 | = m_annotations[instructionOffset].readRegisters[Accumulator].content; |
| 234 | readAccumulator = m_typeResolver->convert(from: readAccumulator, to: varContent); |
| 235 | } |
| 236 | |
| 237 | QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkBaseType( |
| 238 | const QQmlJSRegisterContent &baseType) |
| 239 | { |
| 240 | if (!m_adjustedTypes.contains(value: baseType)) |
| 241 | return NotShadowable; |
| 242 | setError(u"Cannot use shadowable base type for further lookups: %1"_s .arg(a: baseType.descriptiveName())); |
| 243 | return Shadowable; |
| 244 | } |
| 245 | |
| 246 | QT_END_NAMESPACE |
| 247 | |