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 | |