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