1/****************************************************************************
2**
3** Copyright (C) 2019 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the tools applications of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "scopetree.h"
30#include "qcoloroutput.h"
31
32#include <QtCore/qqueue.h>
33
34#include <algorithm>
35
36ScopeTree::ScopeTree(ScopeType type, QString name, ScopeTree *parentScope)
37 : m_parentScope(parentScope), m_name(std::move(name)), m_scopeType(type) {}
38
39ScopeTree::Ptr ScopeTree::createNewChildScope(ScopeType type, const QString &name)
40{
41 Q_ASSERT(type != ScopeType::QMLScope
42 || !m_parentScope
43 || m_parentScope->m_scopeType == ScopeType::QMLScope
44 || m_parentScope->m_name == "global");
45 auto childScope = ScopeTree::Ptr(new ScopeTree{type, name, this});
46 m_childScopes.push_back(t: childScope);
47 return childScope;
48}
49
50void ScopeTree::insertJSIdentifier(const QString &id, QQmlJS::AST::VariableScope scope)
51{
52 Q_ASSERT(m_scopeType != ScopeType::QMLScope);
53 if (scope == QQmlJS::AST::VariableScope::Var) {
54 auto targetScope = this;
55 while (targetScope->scopeType() != ScopeType::JSFunctionScope) {
56 targetScope = targetScope->m_parentScope;
57 }
58 targetScope->m_jsIdentifiers.insert(value: id);
59 } else {
60 m_jsIdentifiers.insert(value: id);
61 }
62}
63
64void ScopeTree::insertSignalIdentifier(const QString &id, const MetaMethod &method,
65 const QQmlJS::SourceLocation &loc,
66 bool hasMultilineHandlerBody)
67{
68 Q_ASSERT(m_scopeType == ScopeType::QMLScope);
69 m_injectedSignalIdentifiers.insert(akey: id, avalue: {.method: method, .loc: loc, .hasMultilineHandlerBody: hasMultilineHandlerBody});
70}
71
72void ScopeTree::insertPropertyIdentifier(const MetaProperty &property)
73{
74 addProperty(prop: property);
75 MetaMethod method(property.propertyName() + QLatin1String("Changed"), "void");
76 addMethod(method);
77}
78
79void ScopeTree::addUnmatchedSignalHandler(const QString &handler,
80 const QQmlJS::SourceLocation &location)
81{
82 m_unmatchedSignalHandlers.append(t: qMakePair(x: handler, y: location));
83}
84
85bool ScopeTree::isIdInCurrentScope(const QString &id) const
86{
87 return isIdInCurrentQMlScopes(id) || isIdInCurrentJSScopes(id);
88}
89
90void ScopeTree::addIdToAccessed(const QString &id, const QQmlJS::SourceLocation &location) {
91 m_currentFieldMember = new FieldMemberList {.m_name: id, .m_parentType: QString(), .m_location: location, .m_child: {}};
92 m_accessedIdentifiers.push_back(x: std::unique_ptr<FieldMemberList>(m_currentFieldMember));
93}
94
95void ScopeTree::accessMember(const QString &name, const QString &parentType,
96 const QQmlJS::SourceLocation &location)
97{
98 Q_ASSERT(m_currentFieldMember);
99 auto *fieldMember = new FieldMemberList {.m_name: name, .m_parentType: parentType, .m_location: location, .m_child: {}};
100 m_currentFieldMember->m_child.reset(p: fieldMember);
101 m_currentFieldMember = fieldMember;
102}
103
104void ScopeTree::resetMemberScope()
105{
106 m_currentFieldMember = nullptr;
107}
108
109bool ScopeTree::isVisualRootScope() const
110{
111 return m_parentScope && m_parentScope->m_parentScope
112 && m_parentScope->m_parentScope->m_parentScope == nullptr;
113}
114
115class IssueLocationWithContext
116{
117public:
118 IssueLocationWithContext(const QString &code, const QQmlJS::SourceLocation &location) {
119 int before = std::max(a: 0,b: code.lastIndexOf(c: '\n', from: location.offset));
120 m_beforeText = code.midRef(position: before + 1, n: int(location.offset - (before + 1)));
121 m_issueText = code.midRef(position: location.offset, n: location.length);
122 int after = code.indexOf(c: '\n', from: int(location.offset + location.length));
123 m_afterText = code.midRef(position: int(location.offset + location.length),
124 n: int(after - (location.offset+location.length)));
125 }
126
127 QStringRef beforeText() const { return m_beforeText; }
128 QStringRef issueText() const { return m_issueText; }
129 QStringRef afterText() const { return m_afterText; }
130
131private:
132 QStringRef m_beforeText;
133 QStringRef m_issueText;
134 QStringRef m_afterText;
135};
136
137static const QStringList unknownBuiltins = {
138 // TODO: "string" should be added to builtins.qmltypes, and the special handling below removed
139 QStringLiteral("alias"), // TODO: we cannot properly resolve aliases, yet
140 QStringLiteral("QRectF"), // TODO: should be added to builtins.qmltypes
141 QStringLiteral("QFont"), // TODO: should be added to builtins.qmltypes
142 QStringLiteral("QJSValue"), // We cannot say anything intelligent about untyped JS values.
143 QStringLiteral("variant"), // Same for generic variants
144};
145
146bool ScopeTree::checkMemberAccess(
147 const QString &code,
148 FieldMemberList *members,
149 const ScopeTree *scope,
150 const QHash<QString, ScopeTree::ConstPtr> &types,
151 ColorOutput& colorOut) const
152{
153 if (!members->m_child)
154 return true;
155
156 Q_ASSERT(scope != nullptr);
157
158 const QString scopeName = scope->name().isEmpty() ? scope->className() : scope->name();
159 const auto &access = members->m_child;
160
161 const auto scopeIt = scope->m_properties.find(akey: access->m_name);
162 if (scopeIt != scope->m_properties.end()) {
163 const QString typeName = access->m_parentType.isEmpty() ? scopeIt->typeName()
164 : access->m_parentType;
165 if (scopeIt->isList() || typeName == QLatin1String("string")) {
166 if (access->m_child && access->m_child->m_name != QLatin1String("length")) {
167 colorOut.write(message: "Warning: ", color: Warning);
168 colorOut.write(
169 message: QString::fromLatin1(
170 str: "\"%1\" is a %2. You cannot access \"%3\" on it at %4:%5\n")
171 .arg(a: access->m_name)
172 .arg(a: QLatin1String(scopeIt->isList() ? "list" : "string"))
173 .arg(a: access->m_child->m_name)
174 .arg(a: access->m_child->m_location.startLine)
175 .arg(a: access->m_child->m_location.startColumn), color: Normal);
176 printContext(colorOut, code, location: access->m_child->m_location);
177 return false;
178 }
179 return true;
180 }
181
182 if (!access->m_child)
183 return true;
184
185 if (const ScopeTree *type = scopeIt->type()) {
186 if (access->m_parentType.isEmpty())
187 return checkMemberAccess(code, members: access.get(), scope: type, types, colorOut);
188 }
189
190 if (unknownBuiltins.contains(str: typeName))
191 return true;
192
193 const auto it = types.find(akey: typeName);
194 if (it != types.end())
195 return checkMemberAccess(code, members: access.get(), scope: it->get(), types, colorOut);
196
197 colorOut.write(message: "Warning: ", color: Warning);
198 colorOut.write(
199 message: QString::fromLatin1(str: "Type \"%1\" of member \"%2\" not found at %3:%4.\n")
200 .arg(a: typeName)
201 .arg(a: access->m_name)
202 .arg(a: access->m_location.startLine)
203 .arg(a: access->m_location.startColumn), color: Normal);
204 printContext(colorOut, code, location: access->m_location);
205 return false;
206 }
207
208 const auto scopeMethodIt = scope->m_methods.find(akey: access->m_name);
209 if (scopeMethodIt != scope->m_methods.end())
210 return true; // Access to property of JS function
211
212 for (const auto &enumerator : scope->m_enums) {
213 for (const QString &key : enumerator.keys()) {
214 if (access->m_name != key)
215 continue;
216
217 if (!access->m_child)
218 return true;
219
220 colorOut.write(message: "Warning: ", color: Warning);
221 colorOut.write(message: QString::fromLatin1(
222 str: "\"%1\" is an enum value. You cannot access \"%2\" on it at %3:%4\n")
223 .arg(a: access->m_name)
224 .arg(a: access->m_child->m_name)
225 .arg(a: access->m_child->m_location.startLine)
226 .arg(a: access->m_child->m_location.startColumn), color: Normal);
227 printContext(colorOut, code, location: access->m_child->m_location);
228 return false;
229 }
230 }
231
232 auto type = types.value(akey: access->m_parentType.isEmpty() ? scopeName : access->m_parentType);
233 while (type) {
234 const auto typeIt = type->m_properties.find(akey: access->m_name);
235 if (typeIt != type->m_properties.end()) {
236 const ScopeTree *propType = typeIt->type();
237 return checkMemberAccess(code, members: access.get(),
238 scope: propType ? propType : types.value(akey: typeIt->typeName()).get(),
239 types, colorOut);
240 }
241
242 const auto typeMethodIt = type->m_methods.find(akey: access->m_name);
243 if (typeMethodIt != type->m_methods.end()) {
244 if (access->m_child == nullptr)
245 return true;
246
247 colorOut.write(message: "Warning: ", color: Warning);
248 colorOut.write(message: QString::fromLatin1(
249 str: "\"%1\" is a method. You cannot access \"%2\" on it at %3:%4\n")
250 .arg(a: access->m_name)
251 .arg(a: access->m_child->m_name)
252 .arg(a: access->m_child->m_location.startLine)
253 .arg(a: access->m_child->m_location.startColumn), color: Normal);
254 printContext(colorOut, code, location: access->m_child->m_location);
255 return false;
256 }
257
258 type = types.value(akey: type->superclassName());
259 }
260
261 if (access->m_name.front().isUpper() && scope->scopeType() == ScopeType::QMLScope) {
262 // may be an attached type
263 const auto it = types.find(akey: access->m_name);
264 if (it != types.end() && !(*it)->attachedTypeName().isEmpty()) {
265 const auto attached = types.find(akey: (*it)->attachedTypeName());
266 if (attached != types.end())
267 return checkMemberAccess(code, members: access.get(), scope: attached->get(), types, colorOut);
268 }
269 }
270
271 colorOut.write(message: "Warning: ", color: Warning);
272 colorOut.write(message: QString::fromLatin1(
273 str: "Property \"%1\" not found on type \"%2\" at %3:%4\n")
274 .arg(a: access->m_name)
275 .arg(a: scopeName)
276 .arg(a: access->m_location.startLine)
277 .arg(a: access->m_location.startColumn), color: Normal);
278 printContext(colorOut, code, location: access->m_location);
279 return false;
280}
281
282bool ScopeTree::recheckIdentifiers(
283 const QString &code,
284 const QHash<QString, const ScopeTree *> &qmlIDs,
285 const QHash<QString, ScopeTree::ConstPtr> &types,
286 const ScopeTree *root, const QString &rootId,
287 ColorOutput& colorOut) const
288{
289 bool noUnqualifiedIdentifier = true;
290
291 // revisit all scopes
292 QQueue<const ScopeTree *> workQueue;
293 workQueue.enqueue(t: this);
294 while (!workQueue.empty()) {
295 const ScopeTree *currentScope = workQueue.dequeue();
296 for (const auto &handler : currentScope->m_unmatchedSignalHandlers) {
297 colorOut.write(message: "Warning: ", color: Warning);
298 colorOut.write(message: QString::fromLatin1(
299 str: "no matching signal found for handler \"%1\" at %2:%3\n")
300 .arg(a: handler.first).arg(a: handler.second.startLine)
301 .arg(a: handler.second.startColumn), color: Normal);
302 printContext(colorOut, code, location: handler.second);
303 }
304
305 for (const auto &memberAccessTree : qAsConst(t: currentScope->m_accessedIdentifiers)) {
306 if (currentScope->isIdInCurrentJSScopes(id: memberAccessTree->m_name))
307 continue;
308
309 auto it = qmlIDs.find(akey: memberAccessTree->m_name);
310 if (it != qmlIDs.end()) {
311 if (*it != nullptr) {
312 if (!checkMemberAccess(code, members: memberAccessTree.get(), scope: *it, types, colorOut))
313 noUnqualifiedIdentifier = false;
314 continue;
315 } else if (memberAccessTree->m_child
316 && memberAccessTree->m_child->m_name.front().isUpper()) {
317 // It could be a qualified type name
318 const QString qualified = memberAccessTree->m_name + QLatin1Char('.')
319 + memberAccessTree->m_child->m_name;
320 const auto typeIt = types.find(akey: qualified);
321 if (typeIt != types.end()) {
322 if (!checkMemberAccess(code, members: memberAccessTree->m_child.get(), scope: typeIt->get(),
323 types, colorOut)) {
324 noUnqualifiedIdentifier = false;
325 }
326 continue;
327 }
328 }
329 }
330
331 auto qmlScope = currentScope->currentQMLScope();
332 if (qmlScope->methods().contains(akey: memberAccessTree->m_name)) {
333 // a property of a JavaScript function
334 continue;
335 }
336
337 const auto qmlIt = qmlScope->m_properties.find(akey: memberAccessTree->m_name);
338 if (qmlIt != qmlScope->m_properties.end()) {
339 if (!memberAccessTree->m_child || unknownBuiltins.contains(str: qmlIt->typeName()))
340 continue;
341
342 if (!qmlIt->type()) {
343 colorOut.write(message: "Warning: ", color: Warning);
344 colorOut.write(message: QString::fromLatin1(
345 str: "Type of property \"%2\" not found at %3:%4\n")
346 .arg(a: memberAccessTree->m_name)
347 .arg(a: memberAccessTree->m_location.startLine)
348 .arg(a: memberAccessTree->m_location.startColumn), color: Normal);
349 printContext(colorOut, code, location: memberAccessTree->m_location);
350 noUnqualifiedIdentifier = false;
351 } else if (!checkMemberAccess(code, members: memberAccessTree.get(), scope: qmlIt->type(), types,
352 colorOut)) {
353 noUnqualifiedIdentifier = false;
354 }
355
356 continue;
357 }
358
359 // TODO: Lots of builtins are missing
360 if (memberAccessTree->m_name == "Qt")
361 continue;
362
363 const auto typeIt = types.find(akey: memberAccessTree->m_name);
364 if (typeIt != types.end()) {
365 if (!checkMemberAccess(code, members: memberAccessTree.get(), scope: typeIt->get(), types,
366 colorOut)) {
367 noUnqualifiedIdentifier = false;
368 }
369 continue;
370 }
371
372 noUnqualifiedIdentifier = false;
373 colorOut.write(message: "Warning: ", color: Warning);
374 auto location = memberAccessTree->m_location;
375 colorOut.write(message: QString::fromLatin1(str: "unqualified access at %1:%2\n")
376 .arg(a: location.startLine).arg(a: location.startColumn),
377 color: Normal);
378
379 printContext(colorOut, code, location);
380
381 // root(JS) --> program(qml) --> (first element)
382 const auto firstElement = root->m_childScopes[0]->m_childScopes[0];
383 if (firstElement->m_properties.contains(akey: memberAccessTree->m_name)
384 || firstElement->m_methods.contains(akey: memberAccessTree->m_name)
385 || firstElement->m_enums.contains(akey: memberAccessTree->m_name)) {
386 colorOut.write(message: "Note: ", color: Info);
387 colorOut.write(message: memberAccessTree->m_name + QLatin1String(" is a member of the root element\n"), color: Normal );
388 colorOut.write(message: QLatin1String(" You can qualify the access with its id to avoid this warning:\n"), color: Normal);
389 if (rootId == QLatin1String("<id>")) {
390 colorOut.write(message: "Note: ", color: Warning);
391 colorOut.write(message: ("You first have to give the root element an id\n"));
392 }
393 IssueLocationWithContext issueLocationWithContext {code, location};
394 colorOut.write(message: issueLocationWithContext.beforeText().toString(), color: Normal);
395 colorOut.write(message: rootId + QLatin1Char('.'), color: Hint);
396 colorOut.write(message: issueLocationWithContext.issueText().toString(), color: Normal);
397 colorOut.write(message: issueLocationWithContext.afterText() + QLatin1Char('\n'), color: Normal);
398 } else if (currentScope->isIdInjectedFromSignal(id: memberAccessTree->m_name)) {
399 auto methodUsages = currentScope->currentQMLScope()->m_injectedSignalIdentifiers
400 .values(akey: memberAccessTree->m_name);
401 auto location = memberAccessTree->m_location;
402 // sort the list of signal handlers by their occurrence in the source code
403 // then, we select the first one whose location is after the unqualified id
404 // and go one step backwards to get the one which we actually need
405 std::sort(first: methodUsages.begin(), last: methodUsages.end(),
406 comp: [](const MethodUsage &m1, const MethodUsage &m2) {
407 return m1.loc.startLine < m2.loc.startLine
408 || (m1.loc.startLine == m2.loc.startLine
409 && m1.loc.startColumn < m2.loc.startColumn);
410 });
411 auto oneBehindIt = std::find_if(first: methodUsages.begin(), last: methodUsages.end(),
412 pred: [&location](const MethodUsage &methodUsage) {
413 return location.startLine < methodUsage.loc.startLine
414 || (location.startLine == methodUsage.loc.startLine
415 && location.startColumn < methodUsage.loc.startColumn);
416 });
417 auto methodUsage = *(--oneBehindIt);
418 colorOut.write(message: "Note:", color: Info);
419 colorOut.write(
420 message: memberAccessTree->m_name + QString::fromLatin1(
421 str: " is accessible in this scope because "
422 "you are handling a signal at %1:%2\n")
423 .arg(a: methodUsage.loc.startLine).arg(a: methodUsage.loc.startColumn),
424 color: Normal);
425 colorOut.write(message: "Consider using a function instead\n", color: Normal);
426 IssueLocationWithContext context {code, methodUsage.loc};
427 colorOut.write(message: context.beforeText() + QLatin1Char(' '));
428 colorOut.write(message: methodUsage.hasMultilineHandlerBody ? "function(" : "(", color: Hint);
429 const auto parameters = methodUsage.method.parameterNames();
430 for (int numParams = parameters.size(); numParams > 0; --numParams) {
431 colorOut.write(message: parameters.at(i: parameters.size() - numParams), color: Hint);
432 if (numParams > 1)
433 colorOut.write(message: ", ", color: Hint);
434 }
435 colorOut.write(message: methodUsage.hasMultilineHandlerBody ? ")" : ") => ", color: Hint);
436 colorOut.write(message: " {...", color: Normal);
437 }
438 colorOut.write(message: "\n\n\n", color: Normal);
439 }
440 for (auto const &childScope: currentScope->m_childScopes)
441 workQueue.enqueue(t: childScope.get());
442 }
443 return noUnqualifiedIdentifier;
444}
445
446bool ScopeTree::isIdInCurrentQMlScopes(const QString &id) const
447{
448 const auto *qmlScope = currentQMLScope();
449 return qmlScope->m_properties.contains(akey: id)
450 || qmlScope->m_methods.contains(akey: id)
451 || qmlScope->m_enums.contains(akey: id);
452}
453
454bool ScopeTree::isIdInCurrentJSScopes(const QString &id) const
455{
456 auto jsScope = this;
457 while (jsScope) {
458 if (jsScope->m_scopeType != ScopeType::QMLScope && jsScope->m_jsIdentifiers.contains(value: id))
459 return true;
460 jsScope = jsScope->m_parentScope;
461 }
462 return false;
463}
464
465bool ScopeTree::isIdInjectedFromSignal(const QString &id) const
466{
467 return currentQMLScope()->m_injectedSignalIdentifiers.contains(akey: id);
468}
469
470const ScopeTree *ScopeTree::currentQMLScope() const
471{
472 auto qmlScope = this;
473 while (qmlScope && qmlScope->m_scopeType != ScopeType::QMLScope)
474 qmlScope = qmlScope->m_parentScope;
475 return qmlScope;
476}
477
478void ScopeTree::printContext(ColorOutput &colorOut, const QString &code,
479 const QQmlJS::SourceLocation &location) const
480{
481 IssueLocationWithContext issueLocationWithContext {code, location};
482 colorOut.write(message: issueLocationWithContext.beforeText().toString(), color: Normal);
483 colorOut.write(message: issueLocationWithContext.issueText().toString(), color: Error);
484 colorOut.write(message: issueLocationWithContext.afterText().toString() + QLatin1Char('\n'), color: Normal);
485 int tabCount = issueLocationWithContext.beforeText().count(c: QLatin1Char('\t'));
486 colorOut.write(message: QString(" ").repeated(times: issueLocationWithContext.beforeText().length() - tabCount)
487 + QString("\t").repeated(times: tabCount)
488 + QString("^").repeated(times: location.length)
489 + QLatin1Char('\n'), color: Normal);
490}
491
492void ScopeTree::addExport(const QString &name, const QString &package,
493 const ComponentVersion &version)
494{
495 m_exports.append(t: Export(package, name, version, 0));
496}
497
498void ScopeTree::setExportMetaObjectRevision(int exportIndex, int metaObjectRevision)
499{
500 m_exports[exportIndex].setMetaObjectRevision(metaObjectRevision);
501}
502
503void ScopeTree::updateParentProperty(const ScopeTree *scope)
504{
505 auto it = m_properties.find(akey: QLatin1String("parent"));
506 if (it != m_properties.end()
507 && scope->name() != QLatin1String("Component")
508 && scope->name() != QLatin1String("program"))
509 it->setType(scope);
510}
511
512ScopeTree::Export::Export(QString package, QString type, const ComponentVersion &version,
513 int metaObjectRevision) :
514 m_package(std::move(package)),
515 m_type(std::move(type)),
516 m_version(version),
517 m_metaObjectRevision(metaObjectRevision)
518{
519}
520
521bool ScopeTree::Export::isValid() const
522{
523 return m_version.isValid() || !m_package.isEmpty() || !m_type.isEmpty();
524}
525

source code of qtdeclarative/tools/qmllint/scopetree.cpp