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