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 "findunqualified.h"
30#include "importedmembersvisitor.h"
31#include "scopetree.h"
32#include "typedescriptionreader.h"
33
34#include <QtQml/private/qqmljsast_p.h>
35#include <QtQml/private/qqmljslexer_p.h>
36#include <QtQml/private/qqmljsparser_p.h>
37#include <QtQml/private/qv4codegen_p.h>
38#include <QtQml/private/qqmldirparser_p.h>
39
40#include <QtCore/qfile.h>
41#include <QtCore/qdiriterator.h>
42#include <QtCore/qscopedvaluerollback.h>
43
44static const QString prefixedName(const QString &prefix, const QString &name)
45{
46 Q_ASSERT(!prefix.endsWith('.'));
47 return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name);
48}
49
50static QQmlDirParser createQmldirParserForFile(const QString &filename)
51{
52 QFile f(filename);
53 f.open(flags: QFile::ReadOnly);
54 QQmlDirParser parser;
55 parser.parse(source: f.readAll());
56 return parser;
57}
58
59static TypeDescriptionReader createQmltypesReaderForFile(const QString &filename)
60{
61 QFile f(filename);
62 f.open(flags: QFile::ReadOnly);
63 TypeDescriptionReader reader { filename, f.readAll() };
64 return reader;
65}
66
67void FindUnqualifiedIDVisitor::enterEnvironment(ScopeType type, const QString &name)
68{
69 m_currentScope = m_currentScope->createNewChildScope(type, name).get();
70}
71
72void FindUnqualifiedIDVisitor::leaveEnvironment()
73{
74 m_currentScope = m_currentScope->parentScope();
75}
76
77void FindUnqualifiedIDVisitor::parseHeaders(QQmlJS::AST::UiHeaderItemList *header)
78{
79 using namespace QQmlJS::AST;
80
81 while (header) {
82 if (auto import = cast<UiImport *>(ast: header->headerItem)) {
83 if (import->version) {
84 QString path;
85 auto uri = import->importUri;
86 while (uri) {
87 path.append(s: uri->name);
88 path.append(s: "/");
89 uri = uri->next;
90 }
91 path.chop(n: 1);
92 importHelper(module: path,
93 prefix: import->asToken.isValid() ? import->importId.toString() : QString(),
94 major: import->version->majorVersion,
95 minor: import->version->minorVersion);
96 }
97 }
98 header = header->next;
99 }
100}
101
102ScopeTree *FindUnqualifiedIDVisitor::parseProgram(QQmlJS::AST::Program *program,
103 const QString &name)
104{
105 using namespace QQmlJS::AST;
106 ScopeTree *result = new ScopeTree(ScopeType::JSLexicalScope, name);
107 for (auto *statement = program->statements; statement; statement = statement->next) {
108 if (auto *function = cast<FunctionDeclaration *>(ast: statement->statement)) {
109 MetaMethod method(function->name.toString());
110 method.setMethodType(MetaMethod::Method);
111 for (auto *parameters = function->formals; parameters; parameters = parameters->next)
112 method.addParameter(name: parameters->element->bindingIdentifier.toString(), type: "");
113 result->addMethod(method);
114 }
115 }
116 return result;
117}
118
119enum ImportVersion { FullyVersioned, PartiallyVersioned, Unversioned, BasePath };
120
121QStringList completeImportPaths(const QString &uri, const QString &basePath, int vmaj, int vmin)
122{
123 static const QLatin1Char Slash('/');
124 static const QLatin1Char Backslash('\\');
125
126 const QVector<QStringRef> parts = uri.splitRef(sep: QLatin1Char('.'), behavior: Qt::SkipEmptyParts);
127
128 QStringList qmlDirPathsPaths;
129 // fully & partially versioned parts + 1 unversioned for each base path
130 qmlDirPathsPaths.reserve(alloc: 2 * parts.count() + 1);
131
132 auto versionString = [](int vmaj, int vmin, ImportVersion version)
133 {
134 if (version == FullyVersioned) {
135 // extension with fully encoded version number (eg. MyModule.3.2)
136 return QString::fromLatin1(str: ".%1.%2").arg(a: vmaj).arg(a: vmin);
137 }
138 if (version == PartiallyVersioned) {
139 // extension with encoded version major (eg. MyModule.3)
140 return QString::fromLatin1(str: ".%1").arg(a: vmaj);
141 }
142 // else extension without version number (eg. MyModule)
143 return QString();
144 };
145 auto joinStringRefs = [](const QVector<QStringRef> &refs, const QChar &sep) {
146 QString str;
147 for (auto it = refs.cbegin(); it != refs.cend(); ++it) {
148 if (it != refs.cbegin())
149 str += sep;
150 str += *it;
151 }
152 return str;
153 };
154
155 const ImportVersion initial = (vmin >= 0)
156 ? FullyVersioned
157 : (vmaj >= 0 ? PartiallyVersioned : Unversioned);
158 for (int version = initial; version <= BasePath; ++version) {
159 const QString ver = versionString(vmaj, vmin, static_cast<ImportVersion>(version));
160
161 QString dir = basePath;
162 if (!dir.endsWith(c: Slash) && !dir.endsWith(c: Backslash))
163 dir += Slash;
164
165 if (version == BasePath) {
166 qmlDirPathsPaths += dir;
167 } else {
168 // append to the end
169 qmlDirPathsPaths += dir + joinStringRefs(parts, Slash) + ver;
170 }
171
172 if (version < Unversioned) {
173 // insert in the middle
174 for (int index = parts.count() - 2; index >= 0; --index) {
175 qmlDirPathsPaths += dir + joinStringRefs(parts.mid(pos: 0, len: index + 1), Slash)
176 + ver + Slash
177 + joinStringRefs(parts.mid(pos: index + 1), Slash);
178 }
179 }
180 }
181 return qmlDirPathsPaths;
182}
183
184static const QLatin1String SlashQmldir = QLatin1String("/qmldir");
185static const QLatin1String SlashPluginsDotQmltypes = QLatin1String("/plugins.qmltypes");
186
187void FindUnqualifiedIDVisitor::readQmltypes(const QString &filename,
188 FindUnqualifiedIDVisitor::Import &result)
189{
190 auto reader = createQmltypesReaderForFile(filename);
191 auto succ = reader(&result.objects, &result.moduleApis, &result.dependencies);
192 if (!succ)
193 m_colorOut.writeUncolored(message: reader.errorMessage());
194}
195
196FindUnqualifiedIDVisitor::Import FindUnqualifiedIDVisitor::readQmldir(const QString &path)
197{
198 Import result;
199 auto reader = createQmldirParserForFile(filename: path + SlashQmldir);
200 const auto imports = reader.imports();
201 for (const QString &import : imports)
202 result.dependencies.append(t: import);
203
204 QHash<QString, ScopeTree *> qmlComponents;
205 const auto components = reader.components();
206 for (auto it = components.begin(), end = components.end(); it != end; ++it) {
207 const QString filePath = path + QLatin1Char('/') + it->fileName;
208 if (!QFile::exists(fileName: filePath)) {
209 m_colorOut.write(message: QLatin1String("warning: "), color: Warning);
210 m_colorOut.write(message: it->fileName + QLatin1String(" is listed as component in ")
211 + path + SlashQmldir
212 + QLatin1String(" but does not exist.\n"));
213 continue;
214 }
215
216 auto mo = qmlComponents.find(akey: it.key());
217 if (mo == qmlComponents.end())
218 mo = qmlComponents.insert(akey: it.key(), avalue: localFile2ScopeTree(filePath));
219
220 (*mo)->addExport(
221 name: it.key(), package: reader.typeNamespace(),
222 version: ComponentVersion(it->majorVersion, it->minorVersion));
223 }
224 for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it)
225 result.objects.insert( akey: it.key(), avalue: ScopeTree::ConstPtr(it.value()));
226
227 if (!reader.plugins().isEmpty() && QFile::exists(fileName: path + SlashPluginsDotQmltypes))
228 readQmltypes(filename: path + SlashPluginsDotQmltypes, result);
229
230 return result;
231}
232
233void FindUnqualifiedIDVisitor::processImport(const QString &prefix, const FindUnqualifiedIDVisitor::Import &import)
234{
235 for (auto const &dependency : qAsConst(t: import.dependencies)) {
236 auto const split = dependency.split(sep: " ");
237 auto const &id = split.at(i: 0);
238 if (split.length() > 1) {
239 const auto version = split.at(i: 1).split(sep: '.');
240 importHelper(module: id, prefix: QString(),
241 major: version.at(i: 0).toInt(),
242 minor: version.length() > 1 ? version.at(i: 1).toInt() : -1);
243 } else {
244 importHelper(module: id, prefix: QString(), major: -1, minor: -1);
245 }
246
247
248 }
249
250 // add objects
251 for (auto it = import.objects.begin(); it != import.objects.end(); ++it) {
252 const auto &val = it.value();
253 m_types[it.key()] = val;
254 m_exportedName2Scope.insert(akey: prefixedName(prefix, name: val->className()), avalue: val);
255
256 const auto exports = val->exports();
257 for (const auto &valExport : exports)
258 m_exportedName2Scope.insert(akey: prefixedName(prefix, name: valExport.type()), avalue: val);
259
260 const auto enums = val->enums();
261 for (const auto &valEnum : enums)
262 m_currentScope->addEnum(fakeEnum: valEnum);
263 }
264}
265
266void FindUnqualifiedIDVisitor::importHelper(const QString &module, const QString &prefix,
267 int major, int minor)
268{
269 const QString id = QString(module).replace(before: QLatin1Char('/'), after: QLatin1Char('.'));
270 QPair<QString, QString> importId { id, prefix };
271 if (m_alreadySeenImports.contains(value: importId))
272 return;
273 m_alreadySeenImports.insert(value: importId);
274
275 for (const QString &qmltypeDir : m_qmltypeDirs) {
276 auto qmltypesPaths = completeImportPaths(uri: id, basePath: qmltypeDir, vmaj: major, vmin: minor);
277
278 for (auto const &qmltypesPath : qmltypesPaths) {
279 if (QFile::exists(fileName: qmltypesPath + SlashQmldir)) {
280 processImport(prefix, import: readQmldir(path: qmltypesPath));
281
282 // break so that we don't import unversioned qml components
283 // in addition to versioned ones
284 break;
285 }
286
287 if (!m_qmltypeFiles.isEmpty())
288 continue;
289
290 Import result;
291
292 QDirIterator it { qmltypesPath, QStringList() << QLatin1String("*.qmltypes"), QDir::Files };
293
294 while (it.hasNext())
295 readQmltypes(filename: it.next(), result);
296
297 processImport(prefix, import: result);
298 }
299 }
300
301 if (!m_qmltypeFiles.isEmpty())
302 {
303 Import result;
304
305 for (const auto &qmltypeFile : m_qmltypeFiles)
306 readQmltypes(filename: qmltypeFile, result);
307
308 processImport(prefix: "", import: result);
309 }
310}
311
312ScopeTree *FindUnqualifiedIDVisitor::localFile2ScopeTree(const QString &filePath)
313{
314 using namespace QQmlJS::AST;
315 const QFileInfo info { filePath };
316 QString baseName = info.baseName();
317 const QString scopeName = baseName.endsWith(s: ".ui") ? baseName.chopped(n: 3) : baseName;
318
319 QQmlJS::Engine engine;
320 QQmlJS::Lexer lexer(&engine);
321
322 const QString lowerSuffix = info.suffix().toLower();
323 const bool isESModule = lowerSuffix == QLatin1String("mjs");
324 const bool isJavaScript = isESModule || lowerSuffix == QLatin1String("js");
325
326 QFile file(filePath);
327 if (!file.open(flags: QFile::ReadOnly)) {
328 return new ScopeTree(isJavaScript ? ScopeType::JSLexicalScope : ScopeType::QMLScope,
329 scopeName);
330 }
331
332 QString code = file.readAll();
333 file.close();
334
335 lexer.setCode(code, /*line = */ lineno: 1, /*qmlMode=*/ !isJavaScript);
336 QQmlJS::Parser parser(&engine);
337
338 const bool success = isJavaScript ? (isESModule ? parser.parseModule()
339 : parser.parseProgram())
340 : parser.parse();
341 if (!success) {
342 return new ScopeTree(isJavaScript ? ScopeType::JSLexicalScope : ScopeType::QMLScope,
343 scopeName);
344 }
345
346 if (!isJavaScript) {
347 QQmlJS::AST::UiProgram *program = parser.ast();
348 parseHeaders(header: program->headers);
349 ImportedMembersVisitor membersVisitor(&m_colorOut);
350 program->members->accept(visitor: &membersVisitor);
351 return membersVisitor.result(scopeName);
352 }
353
354 // TODO: Anything special to do with ES modules here?
355 return parseProgram(program: QQmlJS::AST::cast<QQmlJS::AST::Program *>(parser.rootNode()), name: scopeName);
356}
357
358void FindUnqualifiedIDVisitor::importFileOrDirectory(const QString &fileOrDirectory,
359 const QString &prefix)
360{
361 QString name = fileOrDirectory;
362
363 if (QFileInfo(name).isRelative())
364 name = QDir(QFileInfo { m_filePath }.path()).filePath(fileName: name);
365
366 if (QFileInfo(name).isFile()) {
367 m_exportedName2Scope.insert(akey: prefix, avalue: ScopeTree::ConstPtr(localFile2ScopeTree(filePath: name)));
368 return;
369 }
370
371 QDirIterator it { name, QStringList() << QLatin1String("*.qml"), QDir::NoFilter };
372 while (it.hasNext()) {
373 ScopeTree::ConstPtr scope(localFile2ScopeTree(filePath: it.next()));
374 if (!scope->className().isEmpty())
375 m_exportedName2Scope.insert(akey: prefixedName(prefix, name: scope->className()), avalue: scope);
376 }
377}
378
379void FindUnqualifiedIDVisitor::importExportedNames(const QStringRef &prefix, QString name)
380{
381 QList<ScopeTree::ConstPtr> scopes;
382 for (;;) {
383 ScopeTree::ConstPtr scope = m_exportedName2Scope.value(akey: m_exportedName2Scope.contains(akey: name)
384 ? name
385 : prefix + QLatin1Char('.') + name);
386 if (scope) {
387 if (scopes.contains(t: scope)) {
388 QString inheritenceCycle = name;
389 for (const auto &seen: qAsConst(t&: scopes)) {
390 inheritenceCycle.append(s: QLatin1String(" -> "));
391 inheritenceCycle.append(s: seen->superclassName());
392 }
393
394 m_colorOut.write(message: QLatin1String("Warning: "), color: Warning);
395 m_colorOut.write(message: QString::fromLatin1(str: "%1 is part of an inheritance cycle: %2\n")
396 .arg(a: name)
397 .arg(a: inheritenceCycle));
398 m_unknownImports.insert(value: name);
399 m_visitFailed = true;
400 break;
401 }
402 scopes.append(t: scope);
403 const auto properties = scope->properties();
404 for (auto property : properties) {
405 property.setType(m_exportedName2Scope.value(akey: property.typeName()).get());
406 m_currentScope->insertPropertyIdentifier(prop: property);
407 }
408
409 m_currentScope->addMethods(methods: scope->methods());
410 name = scope->superclassName();
411 if (name.isEmpty() || name == QLatin1String("QObject"))
412 break;
413 } else {
414 m_colorOut.write(message: QLatin1String("warning: "), color: Warning);
415 m_colorOut.write(message: name + QLatin1String(" was not found."
416 " Did you add all import paths?\n"));
417 m_unknownImports.insert(value: name);
418 m_visitFailed = true;
419 break;
420 }
421 }
422}
423
424void FindUnqualifiedIDVisitor::throwRecursionDepthError()
425{
426 m_colorOut.write(QStringLiteral("Error"), color: Error);
427 m_colorOut.write(QStringLiteral("Maximum statement or expression depth exceeded"), color: Error);
428 m_visitFailed = true;
429}
430
431bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiProgram *)
432{
433 enterEnvironment(type: ScopeType::QMLScope, name: "program");
434 QHash<QString, ScopeTree::ConstPtr> objects;
435 QList<ModuleApiInfo> moduleApis;
436 QStringList dependencies;
437 for (auto const &dir : m_qmltypeDirs) {
438 QDirIterator it { dir, QStringList() << QLatin1String("builtins.qmltypes"), QDir::NoFilter,
439 QDirIterator::Subdirectories };
440 while (it.hasNext()) {
441 auto reader = createQmltypesReaderForFile(filename: it.next());
442 auto succ = reader(&objects, &moduleApis, &dependencies);
443 if (!succ)
444 m_colorOut.writeUncolored(message: reader.errorMessage());
445 }
446 }
447
448 if (!m_qmltypeFiles.isEmpty())
449 {
450 for (const auto &qmltypeFile : m_qmltypeFiles) {
451 auto reader = createQmltypesReaderForFile(filename: qmltypeFile);
452 auto succ = reader(&objects, &moduleApis, &dependencies);
453 if (!succ)
454 m_colorOut.writeUncolored(message: reader.errorMessage());
455 }
456 }
457
458 // add builtins
459 for (auto objectIt = objects.begin(); objectIt != objects.end(); ++objectIt) {
460 auto val = objectIt.value();
461 m_types[objectIt.key()] = val;
462
463 const auto exports = val->exports();
464 for (const auto &valExport : exports)
465 m_exportedName2Scope.insert(akey: valExport.type(), avalue: val);
466
467 const auto enums = val->enums();
468 for (const auto &valEnum : enums)
469 m_currentScope->addEnum(fakeEnum: valEnum);
470 }
471 // add "self" (as we only ever check the first part of a qualified identifier, we get away with
472 // using an empty ScopeTree
473 m_exportedName2Scope.insert(akey: QFileInfo { m_filePath }.baseName(), avalue: {});
474
475 importFileOrDirectory(fileOrDirectory: ".", prefix: QString());
476 return true;
477}
478
479void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiProgram *)
480{
481 leaveEnvironment();
482}
483
484bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassExpression *ast)
485{
486 enterEnvironment(type: ScopeType::JSFunctionScope, name: ast->name.toString());
487 return true;
488}
489
490void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassExpression *)
491{
492 leaveEnvironment();
493}
494
495bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassDeclaration *ast)
496{
497 enterEnvironment(type: ScopeType::JSFunctionScope, name: ast->name.toString());
498 return true;
499}
500
501void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassDeclaration *)
502{
503 leaveEnvironment();
504}
505
506bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForStatement *)
507{
508 enterEnvironment(type: ScopeType::JSLexicalScope, name: "forloop");
509 return true;
510}
511
512void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForStatement *)
513{
514 leaveEnvironment();
515}
516
517bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForEachStatement *)
518{
519 enterEnvironment(type: ScopeType::JSLexicalScope, name: "foreachloop");
520 return true;
521}
522
523void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForEachStatement *)
524{
525 leaveEnvironment();
526}
527
528bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Block *)
529{
530 enterEnvironment(type: ScopeType::JSLexicalScope, name: "block");
531 return true;
532}
533
534void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Block *)
535{
536 leaveEnvironment();
537}
538
539bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::CaseBlock *)
540{
541 enterEnvironment(type: ScopeType::JSLexicalScope, name: "case");
542 return true;
543}
544
545void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::CaseBlock *)
546{
547 leaveEnvironment();
548}
549
550bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Catch *catchStatement)
551{
552 enterEnvironment(type: ScopeType::JSLexicalScope, name: "catch");
553 m_currentScope->insertJSIdentifier(id: catchStatement->patternElement->bindingIdentifier.toString(),
554 scope: QQmlJS::AST::VariableScope::Let);
555 return true;
556}
557
558void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Catch *)
559{
560 leaveEnvironment();
561}
562
563bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::WithStatement *withStatement)
564{
565 m_colorOut.write(message: QString::fromLatin1(str: "Warning: "), color: Warning);
566 m_colorOut.write(message: QString::fromLatin1(
567 str: "%1:%2: with statements are strongly discouraged in QML "
568 "and might cause false positives when analysing unqalified identifiers\n")
569 .arg(a: withStatement->firstSourceLocation().startLine)
570 .arg(a: withStatement->firstSourceLocation().startColumn),
571 color: Normal);
572 enterEnvironment(type: ScopeType::JSLexicalScope, name: "with");
573 return true;
574}
575
576void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::WithStatement *)
577{
578 leaveEnvironment();
579}
580
581static QString signalName(const QStringRef &handlerName)
582{
583 if (handlerName.startsWith(s: "on") && handlerName.size() > 2) {
584 QString signal = handlerName.mid(pos: 2).toString();
585 for (int i = 0; i < signal.length(); ++i) {
586 QCharRef ch = signal[i];
587 if (ch.isLower())
588 return QString();
589 if (ch.isUpper()) {
590 ch = ch.toLower();
591 return signal;
592 }
593 }
594 }
595 return QString();
596}
597
598bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiScriptBinding *uisb)
599{
600 using namespace QQmlJS::AST;
601 auto name = uisb->qualifiedId->name;
602 if (name == QLatin1String("id")) {
603 // found id
604 auto expstat = cast<ExpressionStatement *>(ast: uisb->statement);
605 auto identexp = cast<IdentifierExpression *>(ast: expstat->expression);
606 QString elementName = m_currentScope->name();
607 m_qmlid2scope.insert(akey: identexp->name.toString(), avalue: m_currentScope);
608 if (m_currentScope->isVisualRootScope())
609 m_rootId = identexp->name.toString();
610 } else {
611 const QString signal = signalName(handlerName: name);
612 if (signal.isEmpty())
613 return true;
614
615 if (!m_currentScope->methods().contains(akey: signal)) {
616 m_currentScope->addUnmatchedSignalHandler(handler: name.toString(), location: uisb->firstSourceLocation());
617 return true;
618 }
619
620 const auto statement = uisb->statement;
621 if (statement->kind == Node::Kind::Kind_ExpressionStatement) {
622 if (cast<ExpressionStatement *>(ast: statement)->expression->asFunctionDefinition()) {
623 // functions are already handled
624 // they do not get names inserted according to the signal, but access their formal
625 // parameters
626 return true;
627 }
628 }
629
630 auto method = m_currentScope->methods()[signal];
631 for (auto const &param : method.parameterNames()) {
632 const auto firstSourceLocation = statement->firstSourceLocation();
633 bool hasMultilineStatementBody
634 = statement->lastSourceLocation().startLine > firstSourceLocation.startLine;
635 m_currentScope->insertSignalIdentifier(id: param, method, loc: firstSourceLocation,
636 hasMultilineHandlerBody: hasMultilineStatementBody);
637 }
638 return true;
639 }
640 return true;
641}
642
643bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiPublicMember *uipm)
644{
645 // property bool inactive: !active
646 // extract name inactive
647 MetaProperty property(
648 uipm->name.toString(),
649 // TODO: signals, complex types etc.
650 uipm->memberType ? uipm->memberType->name.toString() : QString(),
651 uipm->typeModifier == QLatin1String("list"),
652 !uipm->isReadonlyMember,
653 false,
654 uipm->memberType ? (uipm->memberType->name == QLatin1String("alias")) : false,
655 0);
656 property.setType(m_exportedName2Scope.value(akey: property.typeName()).get());
657 m_currentScope->insertPropertyIdentifier(prop: property);
658 return true;
659}
660
661bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp)
662{
663 auto name = idexp->name;
664 m_currentScope->addIdToAccessed(id: name.toString(), location: idexp->firstSourceLocation());
665 m_fieldMemberBase = idexp;
666 return true;
667}
668
669FindUnqualifiedIDVisitor::FindUnqualifiedIDVisitor(QStringList qmltypeDirs, QStringList qmltypeFiles, QString code,
670 QString fileName, bool silent)
671 : m_rootScope(new ScopeTree { ScopeType::JSFunctionScope, "global" }),
672 m_currentScope(m_rootScope.get()),
673 m_qmltypeDirs(std::move(qmltypeDirs)),
674 m_qmltypeFiles(std::move(qmltypeFiles)),
675 m_code(std::move(code)),
676 m_rootId(QLatin1String("<id>")),
677 m_filePath(std::move(fileName)),
678 m_colorOut(silent)
679{
680 // setup color output
681 m_colorOut.insertMapping(colorID: Error, colorCode: ColorOutput::RedForeground);
682 m_colorOut.insertMapping(colorID: Warning, colorCode: ColorOutput::PurpleForeground);
683 m_colorOut.insertMapping(colorID: Info, colorCode: ColorOutput::BlueForeground);
684 m_colorOut.insertMapping(colorID: Normal, colorCode: ColorOutput::DefaultColor);
685 m_colorOut.insertMapping(colorID: Hint, colorCode: ColorOutput::GreenForeground);
686 QLatin1String jsGlobVars[] = {
687 /* Not listed on the MDN page; browser and QML extensions: */
688 // console/debug api
689 QLatin1String("console"), QLatin1String("print"),
690 // garbage collector
691 QLatin1String("gc"),
692 // i18n
693 QLatin1String("qsTr"), QLatin1String("qsTrId"), QLatin1String("QT_TR_NOOP"),
694 QLatin1String("QT_TRANSLATE_NOOP"), QLatin1String("QT_TRID_NOOP"),
695 // XMLHttpRequest
696 QLatin1String("XMLHttpRequest")
697 };
698 for (const char **globalName = QV4::Compiler::Codegen::s_globalNames;
699 *globalName != nullptr;
700 ++globalName) {
701 m_currentScope->insertJSIdentifier(id: QString::fromLatin1(str: *globalName),
702 scope: QQmlJS::AST::VariableScope::Const);
703 }
704 for (const auto& jsGlobVar: jsGlobVars)
705 m_currentScope->insertJSIdentifier(id: jsGlobVar, scope: QQmlJS::AST::VariableScope::Const);
706}
707
708bool FindUnqualifiedIDVisitor::check()
709{
710 if (m_visitFailed)
711 return false;
712
713 // now that all ids are known, revisit any Connections whose target were perviously unknown
714 for (auto const &outstandingConnection: m_outstandingConnections) {
715 auto targetScope = m_qmlid2scope[outstandingConnection.targetName];
716 if (outstandingConnection.scope)
717 outstandingConnection.scope->addMethods(methods: targetScope->methods());
718 QScopedValueRollback<ScopeTree*> rollback(m_currentScope, outstandingConnection.scope);
719 outstandingConnection.uiod->initializer->accept(visitor: this);
720 }
721 return m_rootScope->recheckIdentifiers(code: m_code, qmlIDs: m_qmlid2scope, types: m_exportedName2Scope,
722 root: m_rootScope.get(), rootId: m_rootId, colorOut&: m_colorOut);
723}
724
725bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::VariableDeclarationList *vdl)
726{
727 while (vdl) {
728 m_currentScope->insertJSIdentifier(id: vdl->declaration->bindingIdentifier.toString(),
729 scope: vdl->declaration->scope);
730 vdl = vdl->next;
731 }
732 return true;
733}
734
735void FindUnqualifiedIDVisitor::visitFunctionExpressionHelper(QQmlJS::AST::FunctionExpression *fexpr)
736{
737 using namespace QQmlJS::AST;
738 auto name = fexpr->name.toString();
739 if (!name.isEmpty()) {
740 if (m_currentScope->scopeType() == ScopeType::QMLScope)
741 m_currentScope->addMethod(method: MetaMethod(name, QLatin1String("void")));
742 else
743 m_currentScope->insertJSIdentifier(id: name, scope: VariableScope::Const);
744 enterEnvironment(type: ScopeType::JSFunctionScope, name);
745 } else {
746 enterEnvironment(type: ScopeType::JSFunctionScope, name: QLatin1String("<anon>"));
747 }
748}
749
750bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionExpression *fexpr)
751{
752 visitFunctionExpressionHelper(fexpr);
753 return true;
754}
755
756void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionExpression *)
757{
758 leaveEnvironment();
759}
760
761bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionDeclaration *fdecl)
762{
763 visitFunctionExpressionHelper(fexpr: fdecl);
764 return true;
765}
766
767void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *)
768{
769 leaveEnvironment();
770}
771
772bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FormalParameterList *fpl)
773{
774 for (auto const &boundName : fpl->boundNames()) {
775 m_currentScope->insertJSIdentifier(id: boundName.id, scope: QQmlJS::AST::VariableScope::Const);
776 }
777 return true;
778}
779
780bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiImport *import)
781{
782 // construct path
783 QString prefix = QLatin1String("");
784 if (import->asToken.isValid()) {
785 prefix += import->importId;
786 }
787 auto dirname = import->fileName.toString();
788 if (!dirname.isEmpty())
789 importFileOrDirectory(fileOrDirectory: dirname, prefix);
790
791 QString path {};
792 if (!import->importId.isEmpty()) {
793 // TODO: do not put imported ids into the same space as qml IDs
794 const QString importId = import->importId.toString();
795 m_qmlid2scope.insert(akey: importId, avalue: m_exportedName2Scope.value(akey: importId).get());
796 }
797 if (import->version) {
798 auto uri = import->importUri;
799 while (uri) {
800 path.append(s: uri->name);
801 path.append(s: "/");
802 uri = uri->next;
803 }
804 path.chop(n: 1);
805
806 importHelper(module: path, prefix, major: import->version->majorVersion, minor: import->version->minorVersion);
807 }
808 return true;
809}
810
811bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied)
812{
813 MetaEnum qmlEnum(uied->name.toString());
814 for (const auto *member = uied->members; member; member = member->next)
815 qmlEnum.addKey(key: member->member.toString());
816 m_currentScope->addEnum(fakeEnum: qmlEnum);
817 return true;
818}
819
820bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiObjectBinding *uiob)
821{
822 // property QtObject __styleData: QtObject {...}
823
824 QString name {};
825 auto id = uiob->qualifiedTypeNameId;
826 QStringRef prefix = uiob->qualifiedTypeNameId->name;
827 while (id) {
828 name += id->name.toString() + QLatin1Char('.');
829 id = id->next;
830 }
831 name.chop(n: 1);
832
833 MetaProperty prop(uiob->qualifiedId->name.toString(), name, false, true, true,
834 name == QLatin1String("alias"), 0);
835 prop.setType(m_exportedName2Scope.value(akey: uiob->qualifiedTypeNameId->name.toString()).get());
836 m_currentScope->addProperty(prop);
837
838 enterEnvironment(type: ScopeType::QMLScope, name);
839 importExportedNames(prefix, name);
840 return true;
841}
842
843void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectBinding *uiob)
844{
845 const auto childScope = m_currentScope;
846 leaveEnvironment();
847 MetaProperty property(uiob->qualifiedId->name.toString(),
848 uiob->qualifiedTypeNameId->name.toString(),
849 false, true, true,
850 uiob->qualifiedTypeNameId->name == QLatin1String("alias"),
851 0);
852 property.setType(childScope);
853 m_currentScope->addProperty(prop: property);
854}
855
856bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiObjectDefinition *uiod)
857{
858 using namespace QQmlJS::AST;
859
860 QString name {};
861 auto id = uiod->qualifiedTypeNameId;
862 QStringRef prefix = uiod->qualifiedTypeNameId->name;
863 while (id) {
864 name += id->name.toString() + QLatin1Char('.');
865 id = id->next;
866 }
867 name.chop(n: 1);
868 enterEnvironment(type: ScopeType::QMLScope, name);
869 if (name.isLower())
870 return false; // Ignore grouped properties for now
871
872 importExportedNames(prefix, name);
873 if (name.endsWith(s: "Connections")) {
874 QString target;
875 auto member = uiod->initializer->members;
876 while (member) {
877 if (member->member->kind == QQmlJS::AST::Node::Kind_UiScriptBinding) {
878 auto asBinding = static_cast<QQmlJS::AST::UiScriptBinding*>(member->member);
879 if (asBinding->qualifiedId->name == QLatin1String("target")) {
880 if (asBinding->statement->kind == QQmlJS::AST::Node::Kind_ExpressionStatement) {
881 auto expr = static_cast<QQmlJS::AST::ExpressionStatement*>(asBinding->statement)->expression;
882 if (auto idexpr = QQmlJS::AST::cast<QQmlJS::AST::IdentifierExpression*>(ast: expr)) {
883 target = idexpr->name.toString();
884 } else {
885 // more complex expressions are not supported
886 }
887 }
888 break;
889 }
890 }
891 member = member->next;
892 }
893 const ScopeTree *targetScope;
894 if (target.isEmpty()) {
895 // no target set, connection comes from parentF
896 ScopeTree* scope = m_currentScope;
897 do {
898 scope = scope->parentScope(); // TODO: rename method
899 } while (scope->scopeType() != ScopeType::QMLScope);
900 targetScope = m_exportedName2Scope.value(akey: scope->name()).get();
901 } else {
902 // there was a target, check if we already can find it
903 auto scopeIt = m_qmlid2scope.find(akey: target);
904 if (scopeIt != m_qmlid2scope.end()) {
905 targetScope = *scopeIt;
906 } else {
907 m_outstandingConnections.push_back(t: {.targetName: target, .scope: m_currentScope, .uiod: uiod});
908 return false; // visit children later once target is known
909 }
910 }
911 if (targetScope)
912 m_currentScope->addMethods(methods: targetScope->methods());
913 }
914 return true;
915}
916
917bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::PatternElement *element)
918{
919 if (element->isVariableDeclaration()) {
920 QQmlJS::AST::BoundNames names;
921 element->boundNames(names: &names);
922 for (const auto &name : names)
923 m_currentScope->insertJSIdentifier(id: name.id, scope: element->scope);
924 }
925
926 return true;
927}
928
929void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *)
930{
931 auto childScope = m_currentScope;
932 leaveEnvironment();
933 childScope->updateParentProperty(scope: m_currentScope);
934}
935
936bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FieldMemberExpression *)
937{
938 return true;
939}
940
941void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember)
942{
943 using namespace QQmlJS::AST;
944 ExpressionNode *base = fieldMember->base;
945 while (auto *nested = cast<NestedExpression *>(ast: base))
946 base = nested->expression;
947
948 if (m_fieldMemberBase == base) {
949 QString type;
950 if (auto *binary = cast<BinaryExpression *>(ast: base)) {
951 if (binary->op == QSOperator::As) {
952 // This is terrible. It's fixed in 6.0.
953 if (auto *right = cast<Type *>(ast: static_cast<Node *>(binary->right)))
954 type = right->toString();
955 }
956 }
957 m_currentScope->accessMember(name: fieldMember->name.toString(),
958 parentType: type,
959 location: fieldMember->identifierToken);
960 m_fieldMemberBase = fieldMember;
961 } else {
962 m_fieldMemberBase = nullptr;
963 }
964}
965
966bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::BinaryExpression *)
967{
968 return true;
969}
970
971void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::BinaryExpression *binExp)
972{
973 if (binExp->op == QSOperator::As && m_fieldMemberBase == binExp->left)
974 m_fieldMemberBase = binExp;
975 else
976 m_fieldMemberBase = nullptr;
977}
978

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