| 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 | { |
| 39 | m_function = function; |
| 40 | m_state = initialState(function); |
| 41 | decode(code: m_function->code.constData(), len: static_cast<uint>(m_function->code.size())); |
| 42 | |
| 43 | for (const auto &store : std::as_const(t&: m_resettableStores)) |
| 44 | checkResettable(accumulatorIn: store.accumulatorIn, instructionOffset: store.instructionOffset); |
| 45 | |
| 46 | // Re-check all base types. We may have made them var after detecting them. |
| 47 | for (const auto &base : std::as_const(t&: m_baseTypes)) { |
| 48 | if (checkBaseType(baseType: base) == Shadowable) |
| 49 | break; |
| 50 | } |
| 51 | |
| 52 | return { .basicBlocks: std::move(m_basicBlocks), .annotations: std::move(m_annotations) }; |
| 53 | } |
| 54 | |
| 55 | void QQmlJSShadowCheck::generate_LoadProperty(int nameIndex) |
| 56 | { |
| 57 | if (!m_state.readsRegister(registerIndex: Accumulator)) |
| 58 | return; // enum lookup cannot be shadowed. |
| 59 | |
| 60 | auto accumulatorIn = m_state.registers.find(key: Accumulator); |
| 61 | if (accumulatorIn != m_state.registers.end()) { |
| 62 | checkShadowing( |
| 63 | baseType: accumulatorIn.value().content, propertyName: m_jsUnitGenerator->stringForIndex(index: nameIndex), |
| 64 | baseRegister: Accumulator); |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | void QQmlJSShadowCheck::generate_GetLookup(int index) |
| 69 | { |
| 70 | if (!m_state.readsRegister(registerIndex: Accumulator)) |
| 71 | return; // enum lookup cannot be shadowed. |
| 72 | |
| 73 | auto accumulatorIn = m_state.registers.find(key: Accumulator); |
| 74 | if (accumulatorIn != m_state.registers.end()) { |
| 75 | checkShadowing( |
| 76 | baseType: accumulatorIn.value().content, propertyName: m_jsUnitGenerator->lookupName(index), baseRegister: Accumulator); |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | void QQmlJSShadowCheck::generate_GetOptionalLookup(int index, int offset) |
| 81 | { |
| 82 | Q_UNUSED(offset); |
| 83 | generate_GetLookup(index); |
| 84 | } |
| 85 | |
| 86 | void QQmlJSShadowCheck::handleStore(int base, const QString &memberName) |
| 87 | { |
| 88 | const int instructionOffset = currentInstructionOffset(); |
| 89 | QQmlJSRegisterContent readAccumulator |
| 90 | = m_annotations[instructionOffset].readRegisters[Accumulator].content; |
| 91 | const auto baseType = m_state.registers[base].content; |
| 92 | |
| 93 | // If the accumulator is already read as var, we don't have to do anything. |
| 94 | if (readAccumulator.contains(type: m_typeResolver->varType())) { |
| 95 | if (checkBaseType(baseType) == NotShadowable) |
| 96 | m_baseTypes.append(t: baseType); |
| 97 | return; |
| 98 | } |
| 99 | |
| 100 | if (checkShadowing(baseType, propertyName: memberName, baseRegister: base) == Shadowable) |
| 101 | return; |
| 102 | |
| 103 | // If the property isn't shadowable, we have to turn the read register into |
| 104 | // var if the accumulator can hold undefined. This has to be done in a second pass |
| 105 | // because the accumulator may still turn into var due to its own shadowing. |
| 106 | const QQmlJSRegisterContent member = m_typeResolver->memberType(type: baseType, name: memberName); |
| 107 | if (member.isProperty()) |
| 108 | m_resettableStores.append(t: {.accumulatorIn: m_state.accumulatorIn(), .instructionOffset: instructionOffset}); |
| 109 | } |
| 110 | |
| 111 | void QQmlJSShadowCheck::generate_StoreProperty(int nameIndex, int base) |
| 112 | { |
| 113 | handleStore(base, memberName: m_jsUnitGenerator->stringForIndex(index: nameIndex)); |
| 114 | } |
| 115 | |
| 116 | void QQmlJSShadowCheck::generate_SetLookup(int index, int base) |
| 117 | { |
| 118 | handleStore(base, memberName: m_jsUnitGenerator->lookupName(index)); |
| 119 | } |
| 120 | |
| 121 | void QQmlJSShadowCheck::generate_CallProperty(int nameIndex, int base, int argc, int argv) |
| 122 | { |
| 123 | Q_UNUSED(argc); |
| 124 | Q_UNUSED(argv); |
| 125 | checkShadowing(baseType: m_state.registers[base].content, propertyName: m_jsUnitGenerator->lookupName(index: nameIndex), baseRegister: base); |
| 126 | } |
| 127 | |
| 128 | void QQmlJSShadowCheck::generate_CallPropertyLookup(int nameIndex, int base, int argc, int argv) |
| 129 | { |
| 130 | Q_UNUSED(argc); |
| 131 | Q_UNUSED(argv); |
| 132 | checkShadowing(baseType: m_state.registers[base].content, propertyName: m_jsUnitGenerator->lookupName(index: nameIndex), baseRegister: base); |
| 133 | } |
| 134 | |
| 135 | QV4::Moth::ByteCodeHandler::Verdict QQmlJSShadowCheck::startInstruction(QV4::Moth::Instr::Type) |
| 136 | { |
| 137 | m_state = nextStateFromAnnotations(oldState: m_state, annotations: m_annotations); |
| 138 | return (m_state.hasInternalSideEffects() || m_state.changedRegisterIndex() != InvalidRegister) |
| 139 | ? ProcessInstruction |
| 140 | : SkipInstruction; |
| 141 | } |
| 142 | |
| 143 | void QQmlJSShadowCheck::endInstruction(QV4::Moth::Instr::Type) |
| 144 | { |
| 145 | } |
| 146 | |
| 147 | QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkShadowing( |
| 148 | QQmlJSRegisterContent baseType, const QString &memberName, int baseRegister) |
| 149 | { |
| 150 | if (checkBaseType(baseType) == Shadowable) |
| 151 | return Shadowable; |
| 152 | else |
| 153 | m_baseTypes.append(t: baseType); |
| 154 | |
| 155 | // JavaScript objects are not shadowable, as far as we can analyze them at all. |
| 156 | if (baseType.containedType()->isJavaScriptBuiltin()) |
| 157 | return NotShadowable; |
| 158 | |
| 159 | if (!baseType.containedType()->isReferenceType()) |
| 160 | return NotShadowable; |
| 161 | |
| 162 | switch (baseType.variant()) { |
| 163 | case QQmlJSRegisterContent::MethodCall: |
| 164 | case QQmlJSRegisterContent::Property: |
| 165 | case QQmlJSRegisterContent::TypeByName: |
| 166 | case QQmlJSRegisterContent::Cast: |
| 167 | case QQmlJSRegisterContent::Unknown: { |
| 168 | const QQmlJSRegisterContent member = m_typeResolver->memberType(type: baseType, name: memberName); |
| 169 | |
| 170 | // You can have something like parent.QtQuick.Screen.pixelDensity |
| 171 | // In that case "QtQuick" cannot be resolved as member type and we would later have to look |
| 172 | // for "QtQuick.Screen" instead. However, you can only do that with attached properties and |
| 173 | // those are not shadowable. |
| 174 | if (!member.isValid()) { |
| 175 | Q_ASSERT(m_typeResolver->isPrefix(memberName)); |
| 176 | return NotShadowable; |
| 177 | } |
| 178 | |
| 179 | if (member.isProperty()) { |
| 180 | if (member.property().isFinal()) |
| 181 | return NotShadowable; // final properties can't be shadowed |
| 182 | } else if (!member.isMethod()) { |
| 183 | return NotShadowable; // Only properties and methods can be shadowed |
| 184 | } |
| 185 | |
| 186 | m_logger->log( |
| 187 | message: u"Member %1 of %2 can be shadowed"_s .arg(args: memberName, args: baseType.descriptiveName()), |
| 188 | id: qmlCompiler, srcLocation: currentSourceLocation()); |
| 189 | |
| 190 | // Make it "var". We don't know what it is. |
| 191 | const QQmlJSScope::ConstPtr varType = m_typeResolver->varType(); |
| 192 | InstructionAnnotation ¤tAnnotation = m_annotations[currentInstructionOffset()]; |
| 193 | currentAnnotation.isShadowable = true; |
| 194 | |
| 195 | if (currentAnnotation.changedRegisterIndex != InvalidRegister) { |
| 196 | m_typeResolver->adjustOriginalType(tracked: currentAnnotation.changedRegister, conversion: varType); |
| 197 | m_adjustedTypes.insert(value: currentAnnotation.changedRegister); |
| 198 | } |
| 199 | |
| 200 | for (auto it = currentAnnotation.readRegisters.begin(), |
| 201 | end = currentAnnotation.readRegisters.end(); |
| 202 | it != end; ++it) { |
| 203 | if (it.key() != baseRegister) |
| 204 | it->second.content = m_typeResolver->convert(from: it->second.content, to: varType); |
| 205 | } |
| 206 | |
| 207 | return Shadowable; |
| 208 | } |
| 209 | default: |
| 210 | // In particular ObjectById is fine as that cannot change into something else |
| 211 | // Singleton should also be fine, unless the factory function creates an object |
| 212 | // with different property types than the declared class. |
| 213 | return NotShadowable; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | void QQmlJSShadowCheck::checkResettable( |
| 218 | QQmlJSRegisterContent accumulatorIn, int instructionOffset) |
| 219 | { |
| 220 | if (!m_typeResolver->canHoldUndefined(content: accumulatorIn)) |
| 221 | return; |
| 222 | |
| 223 | QQmlJSRegisterContent &readAccumulator |
| 224 | = m_annotations[instructionOffset].readRegisters[Accumulator].content; |
| 225 | readAccumulator = m_typeResolver->convert(from: readAccumulator, to: m_typeResolver->varType()); |
| 226 | } |
| 227 | |
| 228 | QQmlJSShadowCheck::Shadowability QQmlJSShadowCheck::checkBaseType( |
| 229 | QQmlJSRegisterContent baseType) |
| 230 | { |
| 231 | if (!m_adjustedTypes.contains(value: baseType)) |
| 232 | return NotShadowable; |
| 233 | addError(message: u"Cannot use shadowable base type for further lookups: %1"_s .arg(a: baseType.descriptiveName())); |
| 234 | return Shadowable; |
| 235 | } |
| 236 | |
| 237 | QT_END_NAMESPACE |
| 238 | |