Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qdslintplugin.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qlist.h>
8#include <QtCore/qvarlengtharray.h>
9#include <QtCore/qhash.h>
10#include <QtCore/qset.h>
11#include <QtCore/qspan.h>
12
13#include <QtQmlCompiler/private/qqmljsscope_p.h>
14
16
17using namespace Qt::StringLiterals;
18using namespace QQmlSA;
19
20// note: is a warning, but is prefixed Err to share the name with its QtC codemodel counterpart.
22 "QtDesignStudio.FunctionsNotSupportedInQmlUi"
23};
25 "QtDesignStudio.ReferenceToParentItemNotSupportedByVisualDesigner"
26};
28 "QtDesignStudio.ImperativeCodeNotEditableInVisualDesigner"
29};
30constexpr LoggerWarningId ErrUnsupportedTypeInQmlUi{ "QtDesignStudio.UnsupportedTypeInQmlUi" };
32 "QtDesignStudio.InvalidIdeInVisualDesigner"
33};
35 "QtDesignStudio.UnsupportedRootTypeInQmlUi"
36};
38 "QtDesignStudio.TranslationFunctionMismatch"
39};
40
42{
43public:
44 FunctionCallValidator(PassManager *manager)
46 , m_connectionsType(resolveType("QtQuick", "Connections")) {}
47
48 void onCall(const Element &element, const QString &propertyName, const Element &readScope,
49 SourceLocation location) override;
50private:
51 Element m_connectionsType;
52};
53
55{
56public:
57 QdsBindingValidator(PassManager *manager, const Element &)
58 : PropertyPass(manager), m_statesType(resolveType("QtQuick", "State"))
59 {
60 }
61
62 void onRead(const QQmlSA::Element &element, const QString &propertyName,
63 const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override;
64
65 void onWrite(const QQmlSA::Element &element, const QString &propertyName,
66 const QQmlSA::Element &value, const QQmlSA::Element &writeScope,
67 QQmlSA::SourceLocation location) override;
68
69private:
70 Element m_statesType;
71};
72
74{
75public:
76 QdsElementValidator(PassManager *passManager);
77 void run(const Element &element) override;
78
79private:
80 void complainAboutFunctions(const Element &element);
81 static constexpr std::array s_unsupportedElementNames = {
82 std::make_pair("QtQuick.Controls"_L1, "ApplicationWindow"_L1),
83 std::make_pair("QtQuick.Controls"_L1, "Drawer"_L1),
84 std::make_pair("QtQml.Models"_L1, "Package"_L1),
85 std::make_pair("QtQuick"_L1, "ShaderEffect"_L1),
86 };
88 std::array<Element, s_unsupportedElementNames.size()> m_unsupportedElements;
89
90 static constexpr std::array s_unsupportedRootNames = {
91 std::make_pair("QtQml.Models"_L1, "ListModel"_L1),
92 std::make_pair("QtQml.Models"_L1, "Package"_L1),
93 std::make_pair("QtQml"_L1, "Timer"_L1),
94 };
95 std::array<Element, s_unsupportedRootNames.size()> m_unsupportedRootElements;
96 std::array<Element, 2> m_supportFunctions;
97 Element m_qtObject;
98};
99
101{
102public:
104
105 void onCall(const QQmlSA::Element &element, const QString &propertyName,
106 const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override;
107
108private:
109 enum TranslationType : quint8 { None, Normal, IdBased };
110 TranslationType m_lastTranslationFunction = None;
111};
112
113void QdsBindingValidator::onRead(const QQmlSA::Element &element, const QString &propertyName,
114 const QQmlSA::Element &readScope, QQmlSA::SourceLocation location)
115{
116 Q_UNUSED(readScope);
117
118 if (element.isFileRootComponent() && propertyName == u"parent") {
119 emitWarning("Referencing the parent of the root item is not supported in a UI file (.ui.qml)",
120 WarnReferenceToParentItemNotSupportedByVisualDesigner, location);
121 }
122}
123
124void QdsBindingValidator::onWrite(const QQmlSA::Element &, const QString &propertyName,
125 const QQmlSA::Element &, const QQmlSA::Element &,
126 QQmlSA::SourceLocation location)
127{
128 static constexpr std::array forbiddenAssignments = { "baseline"_L1,
129 "baselineOffset"_L1,
130 "bottomMargin"_L1,
131 "centerIn"_L1,
132 "color"_L1,
133 "fill"_L1,
134 "height"_L1,
135 "horizontalCenter"_L1,
136 "horizontalCenterOffset"_L1,
137 "left"_L1,
138 "leftMargin"_L1,
139 "margins"_L1,
140 "mirrored"_L1,
141 "opacity"_L1,
142 "right"_L1,
143 "rightMargin"_L1,
144 "rotation"_L1,
145 "scale"_L1,
146 "topMargin"_L1,
147 "verticalCenter"_L1,
148 "verticalCenterOffset"_L1,
149 "width"_L1,
150 "x"_L1,
151 "y"_L1,
152 "z"_L1 };
153 Q_ASSERT(std::is_sorted(forbiddenAssignments.cbegin(), forbiddenAssignments.cend()));
154 if (std::find(forbiddenAssignments.cbegin(), forbiddenAssignments.cend(), propertyName)
155 != forbiddenAssignments.cend()) {
156 emitWarning("Imperative JavaScript assignments can break the visual tooling in Qt Design "
157 "Studio.",
158 WarnImperativeCodeNotEditableInVisualDesigner, location);
159 }
160}
161
162void QQmlJSTranslationFunctionMismatchCheck::onCall(const QQmlSA::Element &element,
163 const QString &propertyName,
164 const QQmlSA::Element &readScope,
165 QQmlSA::SourceLocation location)
166{
167 Q_UNUSED(readScope);
168
169 const QQmlSA::Element globalJSObject = resolveBuiltinType(u"GlobalObject");
170 if (element != globalJSObject)
171 return;
172
173 constexpr std::array translationFunctions = {
174 "qsTranslate"_L1,
175 "QT_TRANSLATE_NOOP"_L1,
176 "qsTr"_L1,
177 "QT_TR_NOOP"_L1,
178 };
179
180 constexpr std::array idTranslationFunctions = {
181 "qsTrId"_L1,
182 "QT_TRID_NOOP"_L1,
183 };
184
185 const bool isTranslation =
186 std::find(translationFunctions.cbegin(), translationFunctions.cend(), propertyName)
187 != translationFunctions.cend();
188 const bool isIdTranslation =
189 std::find(idTranslationFunctions.cbegin(), idTranslationFunctions.cend(), propertyName)
190 != idTranslationFunctions.cend();
191
192 if (!isTranslation && !isIdTranslation)
193 return;
194
195 const TranslationType current = isTranslation ? Normal : IdBased;
196
197 if (m_lastTranslationFunction == None) {
198 m_lastTranslationFunction = current;
199 return;
200 }
201
202 if (m_lastTranslationFunction != current) {
203 emitWarning("Do not mix translation functions", qmlTranslationFunctionMismatch, location);
204 }
205}
206
207void QmlLintQdsPlugin::registerPasses(PassManager *manager, const Element &rootElement)
208{
209 if (!rootElement.filePath().endsWith(u".ui.qml"))
210 return;
211
212 manager->registerPropertyPass(std::make_shared<FunctionCallValidator>(manager),
213 QAnyStringView(), QAnyStringView());
214 manager->registerPropertyPass(std::make_shared<QdsBindingValidator>(manager, rootElement),
215 QAnyStringView(), QAnyStringView());
216 manager->registerPropertyPass(std::make_unique<QQmlJSTranslationFunctionMismatchCheck>(manager),
217 QString(), QString(), QString());
218 manager->registerElementPass(std::make_unique<QdsElementValidator>(manager));
219}
220
221void FunctionCallValidator::onCall(const Element &element, const QString &propertyName,
222 const Element &readScope, SourceLocation location)
223{
224 auto currentQmlScope = QQmlJSScope::findCurrentQMLScope(QQmlJSScope::scope(readScope));
225 // TODO: we would benefit from some public additional public QQmlSA API here.
226 // This should be considered in the context of QTBUG-138360
227 if (currentQmlScope && currentQmlScope->inherits(QQmlJSScope::scope(m_connectionsType)))
228 return;
229
230 // all math functions are allowed
231 const Element globalJSObject = resolveBuiltinType(u"GlobalObject");
232 const Element mathObjectType = globalJSObject.property(u"Math"_s).type();
233 if (element.inherits(mathObjectType))
234 return;
235
236 const Element qjsValue = resolveBuiltinType(u"QJSValue");
237 if (element.inherits(qjsValue)) {
238 // Workaround because the Date method has methods and those are only represented in
239 // QQmlJSTypePropagator as QJSValue.
240 // This is an overapproximation and might flag unrelated methods with the same name as ok
241 // even if they are not, but this is better than bogus warnings about the valid Date methods.
242 const std::array<QStringView, 4> dateMethodmethods{ u"now", u"parse", u"prototype",
243 u"UTC" };
244 if (auto it = std::find(dateMethodmethods.cbegin(), dateMethodmethods.cend(), propertyName);
245 it != dateMethodmethods.cend())
246 return;
247 }
248
249 static const std::vector<std::pair<Element, std::unordered_set<QString>>>
250 whiteListedFunctions = {
251 { Element(),
252 {
253 // used on JS objects and many other types
254 u"valueOf"_s,
255 u"toString"_s,
256 u"toLocaleString"_s,
257 } },
258 { globalJSObject,
259 {
260 u"isNaN"_s, u"isFinite"_s,
261 u"qsTr"_s, u"qsTrId"_s, u"qsTranslate"_s,
262 u"QT_TRANSLATE_NOOP"_s, u"QT_TRID_NOOP"_s, u"QT_TR_NOOP"_s,
263 }
264 },
265 { resolveBuiltinType(u"ArrayPrototype"_s), { u"indexOf"_s, u"lastIndexOf"_s } },
266 { resolveBuiltinType(u"NumberPrototype"_s),
267 {
268 u"isNaN"_s,
269 u"isFinite"_s,
270 u"toFixed"_s,
271 u"toExponential"_s,
272 u"toPrecision"_s,
273 u"isInteger"_s,
274 } },
275 { resolveBuiltinType(u"StringPrototype"_s),
276 {
277 u"arg"_s,
278 u"toLowerCase"_s,
279 u"toLocaleLowerCase"_s,
280 u"toUpperCase"_s,
281 u"toLocaleUpperCase"_s,
282 u"substring"_s,
283 u"charAt"_s,
284 u"charCodeAt"_s,
285 u"concat"_s,
286 u"includes"_s,
287 u"endsWith"_s,
288 u"indexOf"_s,
289 u"lastIndexOf"_s,
290 } },
291 { resolveType(u"QtQml"_s, u"Qt"_s),
292 { u"lighter"_s, u"darker"_s, u"rgba"_s, u"tint"_s, u"hsla"_s, u"hsva"_s,
293 u"point"_s, u"rect"_s, u"size"_s, u"vector2d"_s, u"vector3d"_s, u"vector4d"_s,
294 u"quaternion"_s, u"matrix4x4"_s, u"formatDate"_s, u"formatDateTime"_s,
295 u"formatTime"_s, u"resolvedUrl"_s } },
296 };
297
298 for (const auto &[currentElement, methods] : whiteListedFunctions) {
299 if ((!currentElement || element.inherits(currentElement)) && methods.count(propertyName)) {
300 return;
301 }
302 }
303
304 // all other functions are forbidden
305 emitWarning(u"Arbitrary functions and function calls outside of a Connections object are not "
306 u"supported in a UI file (.ui.qml)",
307 ErrFunctionsNotSupportedInQmlUi, location);
308}
309
311{
312 auto loadTypes = [&manager, this](QSpan<const UnsupportedName> names, QSpan<Element> output) {
313 for (qsizetype i = 0; i < qsizetype(names.size()); ++i) {
314 if (!manager->hasImportedModule(names[i].first))
315 continue;
316 output[i] = resolveType(names[i].first, names[i].second);
317 }
318 };
319 loadTypes(s_unsupportedElementNames, m_unsupportedElements);
320 loadTypes(s_unsupportedRootNames, m_unsupportedRootElements);
321 m_qtObject = resolveType("QtQml"_L1, "QtObject"_L1);
322 m_supportFunctions = { resolveType("QtQml"_L1, "Connections"_L1),
323 resolveType("QtQuick"_L1, "ScriptAction"_L1) };
324}
325
326void QdsElementValidator::complainAboutFunctions(const Element &element)
327{
328 for (const auto &method : element.ownMethods()) {
329 if (method.methodType() != QQmlSA::MethodType::Method)
330 continue;
331
332 emitWarning(
333 u"Arbitrary functions and function calls outside of a Connections object are not "
334 u"supported in a UI file (.ui.qml)",
335 ErrFunctionsNotSupportedInQmlUi, method.sourceLocation());
336 }
337
338 for (const auto &binding : element.ownPropertyBindings()) {
339 if (!binding.hasFunctionScriptValue())
340 continue;
341 emitWarning(u"Arbitrary functions and function calls outside of a Connections object "
342 u"are not "
343 u"supported in a UI file (.ui.qml)",
344 ErrFunctionsNotSupportedInQmlUi, binding.sourceLocation());
345 }
346}
347
348void QdsElementValidator::run(const Element &element)
349{
350 enum WarningType { ForElements, ForRootElements };
351 auto warnIfElementIsUnsupported = [this, &element](WarningType warningType) {
352 QSpan<const Element> unsupportedComponents = warningType == ForElements
353 ? QSpan<const Element>(m_unsupportedElements)
354 : QSpan<const Element>(m_unsupportedRootElements);
355 const QStringView message = warningType == ForElements
356 ? u"This type (%1) is not supported in a UI file (.ui.qml)."_sv
357 : u"This type (%1) is not supported as a root element of a UI file (.ui.qml)."_sv;
358 const LoggerWarningId &id = warningType == ForElements ? ErrUnsupportedTypeInQmlUi
359 : ErrUnsupportedRootTypeInQmlUi;
360
361 for (const auto &unsupportedElement : unsupportedComponents) {
362 if (!unsupportedElement || !element.inherits(unsupportedElement))
363 continue;
364
365 emitWarning(message.arg(element.baseTypeName()), id, element.sourceLocation());
366 break;
367 }
368
369 // special case: we don't want to warn on types indirectly inheriting from QtObject, for
370 // example Item.
371 if (warningType == ForRootElements && element.baseType() == m_qtObject)
372 emitWarning(message.arg(element.baseTypeName()), id, element.sourceLocation());
373 };
374
375 if (element.isFileRootComponent())
376 warnIfElementIsUnsupported(ForRootElements);
377 warnIfElementIsUnsupported(ForElements);
378
379 if (QString id = resolveElementToId(element, element); !id.isEmpty()) {
380 static constexpr std::array unsupportedNames = {
381 "action"_L1, "alias"_L1, "anchors"_L1, "as"_L1, "baseState"_L1,
382 "bool"_L1, "border"_L1, "bottom"_L1, "break"_L1, "case"_L1,
383 "catch"_L1, "clip"_L1, "color"_L1, "continue"_L1, "data"_L1,
384 "date"_L1, "debugger"_L1, "default"_L1, "delete"_L1, "do"_L1,
385 "double"_L1, "else"_L1, "enabled"_L1, "enumeration"_L1, "finally"_L1,
386 "flow"_L1, "focus"_L1, "font"_L1, "for"_L1, "function"_L1,
387 "height"_L1, "id"_L1, "if"_L1, "import"_L1, "in"_L1,
388 "instanceof"_L1, "int"_L1, "item"_L1, "layer"_L1, "left"_L1,
389 "list"_L1, "margin"_L1, "matrix4x4"_L1, "new"_L1, "opacity"_L1,
390 "padding"_L1, "parent"_L1, "point"_L1, "print"_L1, "quaternion"_L1,
391 "real"_L1, "rect"_L1, "return"_L1, "right"_L1, "scale"_L1,
392 "shaderInfo"_L1, "size"_L1, "source"_L1, "sprite"_L1, "spriteSequence"_L1,
393 "state"_L1, "string"_L1, "switch"_L1, "text"_L1, "texture"_L1,
394 "this"_L1, "throw"_L1, "time"_L1, "top"_L1, "try"_L1,
395 "typeof"_L1, "url"_L1, "var"_L1, "variant"_L1, "vector"_L1,
396 "vector2d"_L1, "vector3d"_L1, "vector4d"_L1, "visible"_L1, "void"_L1,
397 "while"_L1, "width"_L1, "with"_L1, "x"_L1, "y"_L1,
398 "z"_L1,
399 };
400
401 Q_ASSERT(std::is_sorted(unsupportedNames.begin(), unsupportedNames.end()));
402 if (std::binary_search(unsupportedNames.cbegin(), unsupportedNames.cend(), id)) {
403 emitWarning(
404 u"This id (%1) might be ambiguous and is not supported in a UI file (.ui.qml)."_s
405 .arg(id),
406 ErrInvalidIdeInVisualDesigner, element.idSourceLocation());
407 }
408 }
409
410 if (std::none_of(m_supportFunctions.cbegin(), m_supportFunctions.cend(),
411 [&element](const Element &base) { return base && element.inherits(base); })) {
412 complainAboutFunctions(element);
413 }
414}
415
416QT_END_NAMESPACE
417
418#include "moc_qdslintplugin.cpp"
void onCall(const Element &element, const QString &propertyName, const Element &readScope, SourceLocation location) override
Executes whenever a property or method is called.
FunctionCallValidator(PassManager *manager)
void onCall(const QQmlSA::Element &element, const QString &propertyName, const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override
Executes whenever a property or method is called.
QdsBindingValidator(PassManager *manager, const Element &)
void onRead(const QQmlSA::Element &element, const QString &propertyName, const QQmlSA::Element &readScope, QQmlSA::SourceLocation location) override
Executes whenever a property is read.
void onWrite(const QQmlSA::Element &element, const QString &propertyName, const QQmlSA::Element &value, const QQmlSA::Element &writeScope, QQmlSA::SourceLocation location) override
Executes whenever a property is written to.
void run(const Element &element) override
Executes if shouldRun() returns true.
QdsElementValidator(PassManager *passManager)
\inmodule QtQmlCompiler
Combined button and popup list for selecting options.
constexpr LoggerWarningId ErrInvalidIdeInVisualDesigner
constexpr LoggerWarningId ErrUnsupportedTypeInQmlUi
constexpr LoggerWarningId ErrFunctionsNotSupportedInQmlUi
constexpr LoggerWarningId WarnImperativeCodeNotEditableInVisualDesigner
constexpr LoggerWarningId WarnReferenceToParentItemNotSupportedByVisualDesigner
constexpr LoggerWarningId qmlTranslationFunctionMismatch
constexpr LoggerWarningId ErrUnsupportedRootTypeInQmlUi