1 | // Copyright (C) 2022 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 "quicklintplugin.h" |
5 | |
6 | QT_BEGIN_NAMESPACE |
7 | |
8 | using namespace Qt::StringLiterals; |
9 | |
10 | static constexpr QQmlSA::LoggerWarningId quickLayoutPositioning { "Quick.layout-positioning" }; |
11 | static constexpr QQmlSA::LoggerWarningId quickAttachedPropertyType { "Quick.attached-property-type" }; |
12 | static constexpr QQmlSA::LoggerWarningId quickControlsNativeCustomize { "Quick.controls-native-customize" }; |
13 | static constexpr QQmlSA::LoggerWarningId quickAnchorCombinations { "Quick.anchor-combinations" }; |
14 | static constexpr QQmlSA::LoggerWarningId quickUnexpectedVarType { "Quick.unexpected-var-type" }; |
15 | static constexpr QQmlSA::LoggerWarningId quickPropertyChangesParsed { "Quick.property-changes-parsed" }; |
16 | static constexpr QQmlSA::LoggerWarningId quickControlsAttachedPropertyReuse { "Quick.controls-attached-property-reuse" }; |
17 | static constexpr QQmlSA::LoggerWarningId quickAttachedPropertyReuse { "Quick.attached-property-reuse" }; |
18 | |
19 | ForbiddenChildrenPropertyValidatorPass::ForbiddenChildrenPropertyValidatorPass( |
20 | QQmlSA::PassManager *manager) |
21 | : QQmlSA::ElementPass(manager) |
22 | { |
23 | } |
24 | |
25 | void ForbiddenChildrenPropertyValidatorPass::addWarning(QAnyStringView moduleName, |
26 | QAnyStringView typeName, |
27 | QAnyStringView propertyName, |
28 | QAnyStringView warning) |
29 | { |
30 | auto element = resolveType(moduleName, typeName); |
31 | if (!element.isNull()) |
32 | m_types[element].append(t: { .propertyName: propertyName.toString(), .message: warning.toString() }); |
33 | } |
34 | |
35 | bool ForbiddenChildrenPropertyValidatorPass::shouldRun(const QQmlSA::Element &element) |
36 | { |
37 | if (!element.parentScope()) |
38 | return false; |
39 | |
40 | for (const auto pair : m_types.asKeyValueRange()) { |
41 | if (element.parentScope().inherits(pair.first)) |
42 | return true; |
43 | } |
44 | |
45 | return false; |
46 | } |
47 | |
48 | void ForbiddenChildrenPropertyValidatorPass::run(const QQmlSA::Element &element) |
49 | { |
50 | for (const auto elementPair : m_types.asKeyValueRange()) { |
51 | const QQmlSA::Element &type = elementPair.first; |
52 | if (!element.parentScope().inherits(type)) |
53 | continue; |
54 | |
55 | for (const auto &warning : elementPair.second) { |
56 | if (!element.hasOwnPropertyBindings(propertyName: warning.propertyName)) |
57 | continue; |
58 | |
59 | const auto bindings = element.ownPropertyBindings(propertyName: warning.propertyName); |
60 | const auto firstBinding = bindings.constBegin().value(); |
61 | emitWarning(diagnostic: warning.message, id: quickLayoutPositioning, srcLocation: firstBinding.sourceLocation()); |
62 | } |
63 | break; |
64 | } |
65 | } |
66 | |
67 | AttachedPropertyTypeValidatorPass::AttachedPropertyTypeValidatorPass(QQmlSA::PassManager *manager) |
68 | : QQmlSA::PropertyPass(manager) |
69 | { |
70 | } |
71 | |
72 | QString AttachedPropertyTypeValidatorPass::addWarning(TypeDescription attachType, |
73 | QList<TypeDescription> allowedTypes, |
74 | bool allowInDelegate, QAnyStringView warning) |
75 | { |
76 | QVarLengthArray<QQmlSA::Element, 4> elements; |
77 | |
78 | const QQmlSA::Element baseType = resolveType(moduleName: attachType.module, typeName: attachType.name); |
79 | const QQmlSA::Element attachedType = resolveAttached(moduleName: attachType.module, typeName: attachType.name); |
80 | |
81 | for (const TypeDescription &desc : allowedTypes) { |
82 | const QQmlSA::Element type = resolveType(moduleName: desc.module, typeName: desc.name); |
83 | if (type.isNull()) |
84 | continue; |
85 | elements.push_back(t: type); |
86 | } |
87 | |
88 | m_attachedTypes.insert( |
89 | hash: { std::make_pair<>(x: attachedType.internalId(), |
90 | y: Warning{ .allowedTypes: elements, .allowInDelegate: allowInDelegate, .message: warning.toString() }) }); |
91 | |
92 | return attachedType.internalId(); |
93 | } |
94 | |
95 | void AttachedPropertyTypeValidatorPass::checkWarnings(const QQmlSA::Element &element, |
96 | const QQmlSA::Element &scopeUsedIn, |
97 | const QQmlSA::SourceLocation &location) |
98 | { |
99 | auto warning = m_attachedTypes.constFind(key: element.internalId()); |
100 | if (warning == m_attachedTypes.cend()) |
101 | return; |
102 | for (const QQmlSA::Element &type : warning->allowedTypes) { |
103 | if (scopeUsedIn.inherits(type)) |
104 | return; |
105 | } |
106 | |
107 | if (warning->allowInDelegate) { |
108 | if (scopeUsedIn.isPropertyRequired(propertyName: u"index"_s ) |
109 | || scopeUsedIn.isPropertyRequired(propertyName: u"model"_s )) |
110 | return; |
111 | |
112 | // If the scope is at the root level, we cannot know whether it will be used |
113 | // as a delegate or not. |
114 | // ### TODO: add a method to check whether a scope is the global scope |
115 | // so that we do not need to use internalId |
116 | if (!scopeUsedIn.parentScope() || scopeUsedIn.parentScope().internalId() == u"global"_s ) |
117 | return; |
118 | |
119 | for (const QQmlSA::Binding &binding : |
120 | scopeUsedIn.parentScope().propertyBindings(propertyName: u"delegate"_s )) { |
121 | if (!binding.hasObject()) |
122 | continue; |
123 | if (binding.objectType() == scopeUsedIn) |
124 | return; |
125 | } |
126 | } |
127 | |
128 | emitWarning(diagnostic: warning->message, id: quickAttachedPropertyType, srcLocation: location); |
129 | } |
130 | |
131 | void AttachedPropertyTypeValidatorPass::onBinding(const QQmlSA::Element &element, |
132 | const QString &propertyName, |
133 | const QQmlSA::Binding &binding, |
134 | const QQmlSA::Element &bindingScope, |
135 | const QQmlSA::Element &value) |
136 | { |
137 | Q_UNUSED(element) |
138 | Q_UNUSED(propertyName) |
139 | Q_UNUSED(bindingScope) |
140 | Q_UNUSED(value) |
141 | |
142 | checkWarnings(element: bindingScope.baseType(), scopeUsedIn: element, location: binding.sourceLocation()); |
143 | } |
144 | |
145 | void AttachedPropertyTypeValidatorPass::onRead(const QQmlSA::Element &element, |
146 | const QString &propertyName, |
147 | const QQmlSA::Element &readScope, |
148 | QQmlSA::SourceLocation location) |
149 | { |
150 | // If the attachment does not have such a property or method then |
151 | // it's either a more general error or an enum. Enums are fine. |
152 | if (element.hasProperty(propertyName) || element.hasMethod(methodName: propertyName)) |
153 | checkWarnings(element, scopeUsedIn: readScope, location); |
154 | } |
155 | |
156 | void AttachedPropertyTypeValidatorPass::onWrite(const QQmlSA::Element &element, |
157 | const QString &propertyName, |
158 | const QQmlSA::Element &value, |
159 | const QQmlSA::Element &writeScope, |
160 | QQmlSA::SourceLocation location) |
161 | { |
162 | Q_UNUSED(propertyName) |
163 | Q_UNUSED(value) |
164 | |
165 | checkWarnings(element, scopeUsedIn: writeScope, location); |
166 | } |
167 | |
168 | ControlsNativeValidatorPass::ControlsNativeValidatorPass(QQmlSA::PassManager *manager) |
169 | : QQmlSA::ElementPass(manager) |
170 | { |
171 | m_elements = { |
172 | ControlElement { .name: "Control" , |
173 | .restrictedProperties: QStringList { "background" , "contentItem" , "leftPadding" , "rightPadding" , |
174 | "topPadding" , "bottomPadding" , "horizontalPadding" , |
175 | "verticalPadding" , "padding" }, |
176 | .isInModuleControls: false, .isControl: true }, |
177 | ControlElement { .name: "Button" , .restrictedProperties: QStringList { "indicator" } }, |
178 | ControlElement { |
179 | .name: "ApplicationWindow" , |
180 | .restrictedProperties: QStringList { "background" , "contentItem" , "header" , "footer" , "menuBar" } }, |
181 | ControlElement { .name: "ComboBox" , .restrictedProperties: QStringList { "indicator" } }, |
182 | ControlElement { .name: "Dial" , .restrictedProperties: QStringList { "handle" } }, |
183 | ControlElement { .name: "GroupBox" , .restrictedProperties: QStringList { "label" } }, |
184 | ControlElement { .name: "$internal$.QQuickIndicatorButton" , .restrictedProperties: QStringList { "indicator" }, .isInModuleControls: false }, |
185 | ControlElement { .name: "Label" , .restrictedProperties: QStringList { "background" } }, |
186 | ControlElement { .name: "MenuItem" , .restrictedProperties: QStringList { "arrow" } }, |
187 | ControlElement { .name: "Page" , .restrictedProperties: QStringList { "header" , "footer" } }, |
188 | ControlElement { .name: "Popup" , .restrictedProperties: QStringList { "background" , "contentItem" } }, |
189 | ControlElement { .name: "RangeSlider" , .restrictedProperties: QStringList { "handle" } }, |
190 | ControlElement { .name: "Slider" , .restrictedProperties: QStringList { "handle" } }, |
191 | ControlElement { .name: "$internal$.QQuickSwipe" , |
192 | .restrictedProperties: QStringList { "leftItem" , "behindItem" , "rightItem" }, .isInModuleControls: false }, |
193 | ControlElement { .name: "TextArea" , .restrictedProperties: QStringList { "background" } }, |
194 | ControlElement { .name: "TextField" , .restrictedProperties: QStringList { "background" } }, |
195 | }; |
196 | |
197 | for (const QString &module : { u"QtQuick.Controls.macOS"_s , u"QtQuick.Controls.Windows"_s }) { |
198 | if (!manager->hasImportedModule(name: module)) |
199 | continue; |
200 | |
201 | QQmlSA::Element control = resolveType(moduleName: module, typeName: "Control" ); |
202 | |
203 | for (ControlElement &element : m_elements) { |
204 | auto type = resolveType(moduleName: element.isInModuleControls ? module : "QtQuick.Templates" , |
205 | typeName: element.name); |
206 | |
207 | if (type.isNull()) |
208 | continue; |
209 | |
210 | element.inheritsControl = !element.isControl && type.inherits(control); |
211 | element.element = type; |
212 | } |
213 | |
214 | m_elements.removeIf(pred: [](const ControlElement &element) { return element.element.isNull(); }); |
215 | |
216 | break; |
217 | } |
218 | } |
219 | |
220 | bool ControlsNativeValidatorPass::shouldRun(const QQmlSA::Element &element) |
221 | { |
222 | for (const ControlElement &controlElement : m_elements) { |
223 | // If our element inherits control, we don't have to individually check for them here. |
224 | if (controlElement.inheritsControl) |
225 | continue; |
226 | if (element.inherits(controlElement.element)) |
227 | return true; |
228 | } |
229 | return false; |
230 | } |
231 | |
232 | void ControlsNativeValidatorPass::run(const QQmlSA::Element &element) |
233 | { |
234 | for (const ControlElement &controlElement : m_elements) { |
235 | if (element.inherits(controlElement.element)) { |
236 | for (const QString &propertyName : controlElement.restrictedProperties) { |
237 | if (element.hasOwnPropertyBindings(propertyName)) { |
238 | emitWarning(QStringLiteral("Not allowed to override \"%1\" because native " |
239 | "styles cannot be customized: See " |
240 | "https://doc-snapshots.qt.io/qt6-dev/" |
241 | "qtquickcontrols-customize.html#customization-" |
242 | "reference for more information." ) |
243 | .arg(a: propertyName), |
244 | id: quickControlsNativeCustomize, srcLocation: element.sourceLocation()); |
245 | } |
246 | } |
247 | // Since all the different types we have rules for don't inherit from each other (except |
248 | // for Control) we don't have to keep checking whether other types match once we've |
249 | // found one that has been inherited from. |
250 | if (!controlElement.isControl) |
251 | break; |
252 | } |
253 | } |
254 | } |
255 | |
256 | AnchorsValidatorPass::AnchorsValidatorPass(QQmlSA::PassManager *manager) |
257 | : QQmlSA::ElementPass(manager) |
258 | , m_item(resolveType(moduleName: "QtQuick" , typeName: "Item" )) |
259 | { |
260 | } |
261 | |
262 | bool AnchorsValidatorPass::shouldRun(const QQmlSA::Element &element) |
263 | { |
264 | return !m_item.isNull() && element.inherits(m_item) |
265 | && element.hasOwnPropertyBindings(propertyName: u"anchors"_s ); |
266 | } |
267 | |
268 | void AnchorsValidatorPass::run(const QQmlSA::Element &element) |
269 | { |
270 | enum BindingLocation { Exists = 1, Own = (1 << 1) }; |
271 | QHash<QString, qint8> bindings; |
272 | |
273 | const QStringList properties = { u"left"_s , u"right"_s , u"horizontalCenter"_s , |
274 | u"top"_s , u"bottom"_s , u"verticalCenter"_s , |
275 | u"baseline"_s }; |
276 | |
277 | QList<QQmlSA::Binding> anchorBindings = element.propertyBindings(propertyName: u"anchors"_s ); |
278 | |
279 | for (qsizetype i = anchorBindings.size() - 1; i >= 0; i--) { |
280 | auto groupType = anchorBindings[i].groupType(); |
281 | if (groupType.isNull()) |
282 | continue; |
283 | |
284 | for (const QString &name : properties) { |
285 | |
286 | const auto &propertyBindings = groupType.ownPropertyBindings(propertyName: name); |
287 | if (propertyBindings.begin() == propertyBindings.end()) |
288 | continue; |
289 | |
290 | bool isUndefined = false; |
291 | for (const auto &propertyBinding : propertyBindings) { |
292 | if (propertyBinding.hasUndefinedScriptValue()) { |
293 | isUndefined = true; |
294 | break; |
295 | } |
296 | } |
297 | |
298 | if (isUndefined) |
299 | bindings[name] = 0; |
300 | else |
301 | bindings[name] |= Exists | ((i == 0) ? Own : 0); |
302 | } |
303 | } |
304 | |
305 | auto ownSourceLocation = [&](QStringList properties) -> QQmlSA::SourceLocation { |
306 | QQmlSA::SourceLocation warnLoc; |
307 | |
308 | for (const QString &name : properties) { |
309 | if (bindings[name] & Own) { |
310 | QQmlSA::Element groupType = QQmlSA::Element{ anchorBindings[0].groupType() }; |
311 | auto bindings = groupType.ownPropertyBindings(propertyName: name); |
312 | Q_ASSERT(bindings.begin() != bindings.end()); |
313 | warnLoc = bindings.begin().value().sourceLocation(); |
314 | break; |
315 | } |
316 | } |
317 | return warnLoc; |
318 | }; |
319 | |
320 | if ((bindings[u"left"_s ] & bindings[u"right"_s ] & bindings[u"horizontalCenter"_s ]) & Exists) { |
321 | QQmlSA::SourceLocation warnLoc = |
322 | ownSourceLocation({ u"left"_s , u"right"_s , u"horizontalCenter"_s }); |
323 | |
324 | if (warnLoc.isValid()) { |
325 | emitWarning( |
326 | diagnostic: "Cannot specify left, right, and horizontalCenter anchors at the same time." , |
327 | id: quickAnchorCombinations, srcLocation: warnLoc); |
328 | } |
329 | } |
330 | |
331 | if ((bindings[u"top"_s ] & bindings[u"bottom"_s ] & bindings[u"verticalCenter"_s ]) & Exists) { |
332 | QQmlSA::SourceLocation warnLoc = |
333 | ownSourceLocation({ u"top"_s , u"bottom"_s , u"verticalCenter"_s }); |
334 | if (warnLoc.isValid()) { |
335 | emitWarning(diagnostic: "Cannot specify top, bottom, and verticalCenter anchors at the same time." , |
336 | id: quickAnchorCombinations, srcLocation: warnLoc); |
337 | } |
338 | } |
339 | |
340 | if ((bindings[u"baseline"_s ] & (bindings[u"bottom"_s ] | bindings[u"verticalCenter"_s ])) |
341 | & Exists) { |
342 | QQmlSA::SourceLocation warnLoc = |
343 | ownSourceLocation({ u"baseline"_s , u"bottom"_s , u"verticalCenter"_s }); |
344 | if (warnLoc.isValid()) { |
345 | emitWarning(diagnostic: "Baseline anchor cannot be used in conjunction with top, bottom, or " |
346 | "verticalCenter anchors." , |
347 | id: quickAnchorCombinations, srcLocation: warnLoc); |
348 | } |
349 | } |
350 | } |
351 | |
352 | ControlsSwipeDelegateValidatorPass::ControlsSwipeDelegateValidatorPass(QQmlSA::PassManager *manager) |
353 | : QQmlSA::ElementPass(manager) |
354 | , m_swipeDelegate(resolveType(moduleName: "QtQuick.Controls" , typeName: "SwipeDelegate" )) |
355 | { |
356 | } |
357 | |
358 | bool ControlsSwipeDelegateValidatorPass::shouldRun(const QQmlSA::Element &element) |
359 | { |
360 | return !m_swipeDelegate.isNull() && element.inherits(m_swipeDelegate); |
361 | } |
362 | |
363 | void ControlsSwipeDelegateValidatorPass::run(const QQmlSA::Element &element) |
364 | { |
365 | for (const auto &property : { u"background"_s , u"contentItem"_s }) { |
366 | for (const auto &binding : element.ownPropertyBindings(propertyName: property)) { |
367 | if (!binding.hasObject()) |
368 | continue; |
369 | const QQmlSA::Element element = QQmlSA::Element{ binding.objectType() }; |
370 | const auto &bindings = element.propertyBindings(propertyName: u"anchors"_s ); |
371 | if (bindings.isEmpty()) |
372 | continue; |
373 | |
374 | if (bindings.first().bindingType() != QQmlSA::BindingType::GroupProperty) |
375 | continue; |
376 | |
377 | auto anchors = bindings.first().groupType(); |
378 | for (const auto &disallowed : { u"fill"_s , u"centerIn"_s , u"left"_s , u"right"_s }) { |
379 | if (anchors.hasPropertyBindings(name: disallowed)) { |
380 | QQmlSA::SourceLocation location; |
381 | const auto &ownBindings = anchors.ownPropertyBindings(propertyName: disallowed); |
382 | if (ownBindings.begin() != ownBindings.end()) { |
383 | location = ownBindings.begin().value().sourceLocation(); |
384 | } |
385 | |
386 | emitWarning( |
387 | diagnostic: u"SwipeDelegate: Cannot use horizontal anchors with %1; unable to layout the item."_s |
388 | .arg(a: property), |
389 | id: quickAnchorCombinations, srcLocation: location); |
390 | break; |
391 | } |
392 | } |
393 | break; |
394 | } |
395 | } |
396 | |
397 | const auto &swipe = element.ownPropertyBindings(propertyName: u"swipe"_s ); |
398 | if (swipe.begin() == swipe.end()) |
399 | return; |
400 | |
401 | const auto firstSwipe = swipe.begin().value(); |
402 | if (firstSwipe.bindingType() != QQmlSA::BindingType::GroupProperty) |
403 | return; |
404 | |
405 | auto group = firstSwipe.groupType(); |
406 | |
407 | const std::array ownDirBindings = { group.ownPropertyBindings(propertyName: u"right"_s ), |
408 | group.ownPropertyBindings(propertyName: u"left"_s ), |
409 | group.ownPropertyBindings(propertyName: u"behind"_s ) }; |
410 | |
411 | auto ownBindingIterator = |
412 | std::find_if(first: ownDirBindings.begin(), last: ownDirBindings.end(), |
413 | pred: [](const auto &bindings) { return bindings.begin() != bindings.end(); }); |
414 | |
415 | if (ownBindingIterator == ownDirBindings.end()) |
416 | return; |
417 | |
418 | if (group.hasPropertyBindings(name: u"behind"_s ) |
419 | && (group.hasPropertyBindings(name: u"right"_s ) || group.hasPropertyBindings(name: u"left"_s ))) { |
420 | emitWarning(diagnostic: "SwipeDelegate: Cannot set both behind and left/right properties" , |
421 | id: quickAnchorCombinations, srcLocation: ownBindingIterator->begin().value().sourceLocation()); |
422 | } |
423 | } |
424 | |
425 | VarBindingTypeValidatorPass::VarBindingTypeValidatorPass( |
426 | QQmlSA::PassManager *manager, |
427 | const QMultiHash<QString, TypeDescription> &expectedPropertyTypes) |
428 | : QQmlSA::PropertyPass(manager) |
429 | { |
430 | QMultiHash<QString, QQmlSA::Element> propertyTypes; |
431 | |
432 | for (const auto pair : expectedPropertyTypes.asKeyValueRange()) { |
433 | const QQmlSA::Element propType = pair.second.module.isEmpty() |
434 | ? resolveBuiltinType(typeName: pair.second.name) |
435 | : resolveType(moduleName: pair.second.module, typeName: pair.second.name); |
436 | if (!propType.isNull()) |
437 | propertyTypes.insert(key: pair.first, value: propType); |
438 | } |
439 | |
440 | m_expectedPropertyTypes = propertyTypes; |
441 | } |
442 | |
443 | void VarBindingTypeValidatorPass::onBinding(const QQmlSA::Element &element, |
444 | const QString &propertyName, |
445 | const QQmlSA::Binding &binding, |
446 | const QQmlSA::Element &bindingScope, |
447 | const QQmlSA::Element &value) |
448 | { |
449 | Q_UNUSED(element); |
450 | Q_UNUSED(bindingScope); |
451 | |
452 | const auto range = m_expectedPropertyTypes.equal_range(key: propertyName); |
453 | |
454 | if (range.first == range.second) |
455 | return; |
456 | |
457 | QQmlSA::Element bindingType; |
458 | |
459 | if (!value.isNull()) { |
460 | bindingType = value; |
461 | } else { |
462 | if (QQmlSA::Binding::isLiteralBinding(binding.bindingType())) { |
463 | bindingType = resolveLiteralType(binding); |
464 | } else { |
465 | switch (binding.bindingType()) { |
466 | case QQmlSA::BindingType::Object: |
467 | bindingType = QQmlSA::Element{ binding.objectType() }; |
468 | break; |
469 | case QQmlSA::BindingType::Script: |
470 | break; |
471 | default: |
472 | return; |
473 | } |
474 | } |
475 | } |
476 | |
477 | if (std::find_if(first: range.first, last: range.second, |
478 | pred: [&](const QQmlSA::Element &scope) { return bindingType.inherits(scope); }) |
479 | == range.second) { |
480 | |
481 | const bool bindingTypeIsComposite = bindingType.isComposite(); |
482 | if (bindingTypeIsComposite && !bindingType.baseType()) { |
483 | /* broken module or missing import, there is nothing we |
484 | can really check here, as something is amiss. We |
485 | simply skip this binding, and assume that whatever |
486 | caused the breakage here will already cause another |
487 | warning somewhere else. |
488 | */ |
489 | return; |
490 | } |
491 | const QString bindingTypeName = |
492 | bindingTypeIsComposite ? bindingType.baseType().name() |
493 | : bindingType.name(); |
494 | QStringList expectedTypeNames; |
495 | |
496 | for (auto it = range.first; it != range.second; it++) |
497 | expectedTypeNames << it.value().name(); |
498 | |
499 | emitWarning(diagnostic: u"Unexpected type for property \"%1\" expected %2 got %3"_s .arg( |
500 | args: propertyName, args: expectedTypeNames.join(sep: u", "_s ), args: bindingTypeName), |
501 | id: quickUnexpectedVarType, srcLocation: binding.sourceLocation()); |
502 | } |
503 | } |
504 | |
505 | void AttachedPropertyReuse::onRead(const QQmlSA::Element &element, const QString &propertyName, |
506 | const QQmlSA::Element &readScope, |
507 | QQmlSA::SourceLocation location) |
508 | { |
509 | const auto range = usedAttachedTypes.equal_range(key: readScope); |
510 | const auto attachedTypeAndLocation = std::find_if( |
511 | first: range.first, last: range.second, pred: [&](const ElementAndLocation &elementAndLocation) { |
512 | return elementAndLocation.element == element; |
513 | }); |
514 | if (attachedTypeAndLocation != range.second) { |
515 | const QQmlSA::SourceLocation attachedLocation = attachedTypeAndLocation->location; |
516 | |
517 | // Ignore enum accesses, as these will not cause the attached object to be created. |
518 | // Also ignore anything we cannot determine. |
519 | if (!element.hasProperty(propertyName) && !element.hasMethod(methodName: propertyName)) |
520 | return; |
521 | |
522 | for (QQmlSA::Element scope = readScope.parentScope(); !scope.isNull(); |
523 | scope = scope.parentScope()) { |
524 | const auto range = usedAttachedTypes.equal_range(key: scope); |
525 | bool found = false; |
526 | for (auto it = range.first; it != range.second; ++it) { |
527 | if (it->element == element) { |
528 | found = true; |
529 | break; |
530 | } |
531 | } |
532 | if (!found) |
533 | continue; |
534 | |
535 | const QString id = resolveElementToId(element: scope, context: readScope); |
536 | const QQmlSA::SourceLocation idInsertLocation{ attachedLocation.offset(), 0, |
537 | attachedLocation.startLine(), |
538 | attachedLocation.startColumn() }; |
539 | QQmlSA::FixSuggestion suggestion{ "Reference it by id instead:"_L1 , idInsertLocation, |
540 | id.isEmpty() ? "<id>."_L1 : (id + '.'_L1) }; |
541 | |
542 | if (id.isEmpty()) |
543 | suggestion.setHint("You first have to give the element an id"_L1 ); |
544 | else |
545 | suggestion.setAutoApplicable(); |
546 | |
547 | emitWarning(diagnostic: "Using attached type %1 already initialized in a parent scope."_L1 .arg( |
548 | args: element.name()), |
549 | id: category, srcLocation: attachedLocation, fix: suggestion); |
550 | } |
551 | |
552 | return; |
553 | } |
554 | |
555 | if (element.hasProperty(propertyName)) |
556 | return; // an actual property |
557 | |
558 | QQmlSA::Element type = resolveTypeInFileScope(typeName: propertyName); |
559 | QQmlSA::Element attached = resolveAttachedInFileScope(typeName: propertyName); |
560 | if (!type || !attached) |
561 | return; |
562 | |
563 | if (category == quickControlsAttachedPropertyReuse) { |
564 | for (QQmlSA::Element parent = attached; parent; parent = parent.baseType()) { |
565 | // ### TODO: Make it possible to resolve QQuickAttachedPropertyPropagator |
566 | // so that we don't have to compare the internal id |
567 | if (parent.internalId() == "QQuickAttachedPropertyPropagator"_L1 ) { |
568 | usedAttachedTypes.insert(key: readScope, value: {.element: attached, .location: location}); |
569 | break; |
570 | } |
571 | } |
572 | |
573 | } else { |
574 | usedAttachedTypes.insert(key: readScope, value: {.element: attached, .location: location}); |
575 | } |
576 | } |
577 | |
578 | void AttachedPropertyReuse::onWrite(const QQmlSA::Element &element, const QString &propertyName, |
579 | const QQmlSA::Element &value, const QQmlSA::Element &writeScope, |
580 | QQmlSA::SourceLocation location) |
581 | { |
582 | Q_UNUSED(value); |
583 | onRead(element, propertyName, readScope: writeScope, location); |
584 | } |
585 | |
586 | void QmlLintQuickPlugin::registerPasses(QQmlSA::PassManager *manager, |
587 | const QQmlSA::Element &rootElement) |
588 | { |
589 | const QQmlSA::LoggerWarningId attachedReuseCategory = [manager]() { |
590 | if (manager->isCategoryEnabled(category: quickAttachedPropertyReuse)) |
591 | return quickAttachedPropertyReuse; |
592 | if (manager->isCategoryEnabled(category: qmlAttachedPropertyReuse)) |
593 | return qmlAttachedPropertyReuse; |
594 | return quickControlsAttachedPropertyReuse; |
595 | }(); |
596 | |
597 | const bool hasQuick = manager->hasImportedModule(name: "QtQuick" ); |
598 | const bool hasQuickLayouts = manager->hasImportedModule(name: "QtQuick.Layouts" ); |
599 | const bool hasQuickControls = manager->hasImportedModule(name: "QtQuick.Templates" ) |
600 | || manager->hasImportedModule(name: "QtQuick.Controls" ) |
601 | || manager->hasImportedModule(name: "QtQuick.Controls.Basic" ); |
602 | |
603 | Q_UNUSED(rootElement); |
604 | |
605 | if (hasQuick) { |
606 | manager->registerElementPass(pass: std::make_unique<AnchorsValidatorPass>(args&: manager)); |
607 | manager->registerElementPass(pass: std::make_unique<PropertyChangesValidatorPass>(args&: manager)); |
608 | |
609 | auto forbiddenChildProperty = |
610 | std::make_unique<ForbiddenChildrenPropertyValidatorPass>(args&: manager); |
611 | |
612 | for (const QString &element : { u"Grid"_s , u"Flow"_s }) { |
613 | for (const QString &property : { u"anchors"_s , u"x"_s , u"y"_s }) { |
614 | forbiddenChildProperty->addWarning( |
615 | moduleName: "QtQuick" , typeName: element, propertyName: property, |
616 | warning: u"Cannot specify %1 for items inside %2. %2 will not function."_s .arg( |
617 | args: property, args: element)); |
618 | } |
619 | } |
620 | |
621 | if (hasQuickLayouts) { |
622 | forbiddenChildProperty->addWarning( |
623 | moduleName: "QtQuick.Layouts" , typeName: "Layout" , propertyName: "anchors" , |
624 | warning: "Detected anchors on an item that is managed by a layout. This is undefined " |
625 | u"behavior; use Layout.alignment instead." ); |
626 | forbiddenChildProperty->addWarning( |
627 | moduleName: "QtQuick.Layouts" , typeName: "Layout" , propertyName: "x" , |
628 | warning: "Detected x on an item that is managed by a layout. This is undefined " |
629 | u"behavior; use Layout.leftMargin or Layout.rightMargin instead." ); |
630 | forbiddenChildProperty->addWarning( |
631 | moduleName: "QtQuick.Layouts" , typeName: "Layout" , propertyName: "y" , |
632 | warning: "Detected y on an item that is managed by a layout. This is undefined " |
633 | u"behavior; use Layout.topMargin or Layout.bottomMargin instead." ); |
634 | forbiddenChildProperty->addWarning( |
635 | moduleName: "QtQuick.Layouts" , typeName: "Layout" , propertyName: "width" , |
636 | warning: "Detected width on an item that is managed by a layout. This is undefined " |
637 | u"behavior; use implicitWidth or Layout.preferredWidth instead." ); |
638 | forbiddenChildProperty->addWarning( |
639 | moduleName: "QtQuick.Layouts" , typeName: "Layout" , propertyName: "height" , |
640 | warning: "Detected height on an item that is managed by a layout. This is undefined " |
641 | u"behavior; use implictHeight or Layout.preferredHeight instead." ); |
642 | } |
643 | |
644 | manager->registerElementPass(pass: std::move(forbiddenChildProperty)); |
645 | } |
646 | |
647 | auto attachedPropertyType = std::make_shared<AttachedPropertyTypeValidatorPass>(args&: manager); |
648 | |
649 | auto addAttachedWarning = [&](TypeDescription attachedType, QList<TypeDescription> allowedTypes, |
650 | QAnyStringView warning, bool allowInDelegate = false) { |
651 | QString attachedTypeName = attachedPropertyType->addWarning(attachType: attachedType, allowedTypes, |
652 | allowInDelegate, warning); |
653 | manager->registerPropertyPass(pass: attachedPropertyType, moduleName: attachedType.module, |
654 | typeName: u"$internal$."_s + attachedTypeName, propertyName: {}, allowInheritance: false); |
655 | }; |
656 | |
657 | auto addVarBindingWarning = |
658 | [&](QAnyStringView moduleName, QAnyStringView typeName, |
659 | const QMultiHash<QString, TypeDescription> &expectedPropertyTypes) { |
660 | auto varBindingType = std::make_shared<VarBindingTypeValidatorPass>( |
661 | args&: manager, args: expectedPropertyTypes); |
662 | for (const auto &propertyName : expectedPropertyTypes.uniqueKeys()) { |
663 | manager->registerPropertyPass(pass: varBindingType, moduleName, typeName, |
664 | propertyName); |
665 | } |
666 | }; |
667 | |
668 | if (hasQuick) { |
669 | addVarBindingWarning("QtQuick" , "TableView" , |
670 | { { "columnWidthProvider" , { .module: "" , .name: "function" } }, |
671 | { "rowHeightProvider" , { .module: "" , .name: "function" } } }); |
672 | addAttachedWarning({ .module: "QtQuick" , .name: "Accessible" }, { { .module: "QtQuick" , .name: "Item" } }, |
673 | "Accessible must be attached to an Item" ); |
674 | addAttachedWarning({ .module: "QtQuick" , .name: "LayoutMirroring" }, |
675 | { { .module: "QtQuick" , .name: "Item" }, { .module: "QtQuick" , .name: "Window" } }, |
676 | "LayoutDirection attached property only works with Items and Windows" ); |
677 | addAttachedWarning({ .module: "QtQuick" , .name: "EnterKey" }, { { .module: "QtQuick" , .name: "Item" } }, |
678 | "EnterKey attached property only works with Items" ); |
679 | } |
680 | if (hasQuickLayouts) { |
681 | addAttachedWarning({ .module: "QtQuick.Layouts" , .name: "Layout" }, { { .module: "QtQuick" , .name: "Item" } }, |
682 | "Layout must be attached to Item elements" ); |
683 | addAttachedWarning({ .module: "QtQuick.Layouts" , .name: "StackLayout" }, { { .module: "QtQuick" , .name: "Item" } }, |
684 | "StackLayout must be attached to an Item" ); |
685 | } |
686 | |
687 | |
688 | if (hasQuickControls) { |
689 | manager->registerElementPass(pass: std::make_unique<ControlsSwipeDelegateValidatorPass>(args&: manager)); |
690 | manager->registerPropertyPass(pass: std::make_unique<AttachedPropertyReuse>( |
691 | args&: manager, args: attachedReuseCategory), moduleName: "" , typeName: "" ); |
692 | |
693 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "ScrollBar" }, |
694 | { { .module: "QtQuick" , .name: "Flickable" }, { .module: "QtQuick.Templates" , .name: "ScrollView" } }, |
695 | "ScrollBar must be attached to a Flickable or ScrollView" ); |
696 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "ScrollIndicator" }, |
697 | { { .module: "QtQuick" , .name: "Flickable" } }, |
698 | "ScrollIndicator must be attached to a Flickable" ); |
699 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "TextArea" }, { { .module: "QtQuick" , .name: "Flickable" } }, |
700 | "TextArea must be attached to a Flickable" ); |
701 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "SplitView" }, { { .module: "QtQuick" , .name: "Item" } }, |
702 | "SplitView attached property only works with Items" ); |
703 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "StackView" }, { { .module: "QtQuick" , .name: "Item" } }, |
704 | "StackView attached property only works with Items" ); |
705 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "ToolTip" }, { { .module: "QtQuick" , .name: "Item" } }, |
706 | "ToolTip must be attached to an Item" ); |
707 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "SwipeDelegate" }, { { .module: "QtQuick" , .name: "Item" } }, |
708 | "Attached properties of SwipeDelegate must be accessed through an Item" ); |
709 | addAttachedWarning({ .module: "QtQuick.Templates" , .name: "SwipeView" }, { { .module: "QtQuick" , .name: "Item" } }, |
710 | "SwipeView must be attached to an Item" ); |
711 | addAttachedWarning( |
712 | { .module: "QtQuick.Templates" , .name: "Tumbler" }, { { .module: "QtQuick" , .name: "Tumbler" } }, |
713 | "Tumbler: attached properties of Tumbler must be accessed through a delegate item" , |
714 | true); |
715 | addVarBindingWarning("QtQuick.Templates" , "Tumbler" , |
716 | { { "contentItem" , { .module: "QtQuick" , .name: "PathView" } }, |
717 | { "contentItem" , { .module: "QtQuick" , .name: "ListView" } } }); |
718 | addVarBindingWarning("QtQuick.Templates" , "SpinBox" , |
719 | { { "textFromValue" , { .module: "" , .name: "function" } }, |
720 | { "valueFromText" , { .module: "" , .name: "function" } } }); |
721 | } else if (attachedReuseCategory != quickControlsAttachedPropertyReuse) { |
722 | manager->registerPropertyPass(pass: std::make_unique<AttachedPropertyReuse>( |
723 | args&: manager, args: attachedReuseCategory), moduleName: "" , typeName: "" ); |
724 | } |
725 | |
726 | if (manager->hasImportedModule(name: u"QtQuick.Controls.macOS"_s ) |
727 | || manager->hasImportedModule(name: u"QtQuick.Controls.Windows"_s )) |
728 | manager->registerElementPass(pass: std::make_unique<ControlsNativeValidatorPass>(args&: manager)); |
729 | } |
730 | |
731 | PropertyChangesValidatorPass::PropertyChangesValidatorPass(QQmlSA::PassManager *manager) |
732 | : QQmlSA::ElementPass(manager) |
733 | , m_propertyChanges(resolveType(moduleName: "QtQuick" , typeName: "PropertyChanges" )) |
734 | { |
735 | } |
736 | |
737 | bool PropertyChangesValidatorPass::shouldRun(const QQmlSA::Element &element) |
738 | { |
739 | return !m_propertyChanges.isNull() && element.inherits(m_propertyChanges); |
740 | } |
741 | |
742 | void PropertyChangesValidatorPass::run(const QQmlSA::Element &element) |
743 | { |
744 | const QQmlSA::Binding::Bindings bindings = element.ownPropertyBindings(); |
745 | |
746 | const auto target = |
747 | std::find_if(first: bindings.constBegin(), last: bindings.constEnd(), |
748 | pred: [](const auto binding) { return binding.propertyName() == u"target"_s ; }); |
749 | if (target == bindings.constEnd()) |
750 | return; |
751 | |
752 | QString targetId = u"<id>"_s ; |
753 | const QString targetBinding = sourceCode(location: target.value().sourceLocation()); |
754 | const QQmlSA::Element targetElement = resolveIdToElement(id: targetBinding, context: element); |
755 | if (!targetElement.isNull()) |
756 | targetId = targetBinding; |
757 | |
758 | for (auto it = bindings.constBegin(); it != bindings.constEnd(); ++it) { |
759 | const auto &propertyName = it.key(); |
760 | const auto &propertyBinding = it.value(); |
761 | if (element.hasProperty(propertyName)) |
762 | continue; |
763 | |
764 | const QQmlSA::SourceLocation bindingLocation = propertyBinding.sourceLocation(); |
765 | if (!targetElement.isNull() && !targetElement.hasProperty(propertyName)) { |
766 | emitWarning( |
767 | diagnostic: "Unknown property \"%1\" in PropertyChanges."_L1 .arg(args: propertyName), |
768 | id: quickPropertyChangesParsed, srcLocation: bindingLocation); |
769 | continue; |
770 | } |
771 | |
772 | QString binding = sourceCode(location: bindingLocation); |
773 | if (binding.length() > 16) |
774 | binding = binding.left(n: 13) + "..."_L1 ; |
775 | |
776 | emitWarning(diagnostic: "Property \"%1\" is custom-parsed in PropertyChanges. " |
777 | "You should phrase this binding as \"%2.%1: %3\""_L1 .arg(args: propertyName, args&: targetId, |
778 | args&: binding), |
779 | id: quickPropertyChangesParsed, srcLocation: bindingLocation); |
780 | } |
781 | } |
782 | |
783 | QT_END_NAMESPACE |
784 | |
785 | #include "moc_quicklintplugin.cpp" |
786 | |