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

source code of qtdeclarative/src/plugins/qmllint/qds/qdslintplugin.cpp