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 
prefixedName(const QString & prefix,const QString & name)44 static 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 
createQmldirParserForFile(const QString & filename)50 static QQmlDirParser createQmldirParserForFile(const QString &filename)
51 {
52     QFile f(filename);
53     f.open(QFile::ReadOnly);
54     QQmlDirParser parser;
55     parser.parse(f.readAll());
56     return parser;
57 }
58 
createQmltypesReaderForFile(const QString & filename)59 static TypeDescriptionReader createQmltypesReaderForFile(const QString &filename)
60 {
61     QFile f(filename);
62     f.open(QFile::ReadOnly);
63     TypeDescriptionReader reader { filename, f.readAll() };
64     return reader;
65 }
66 
enterEnvironment(ScopeType type,const QString & name)67 void FindUnqualifiedIDVisitor::enterEnvironment(ScopeType type, const QString &name)
68 {
69     m_currentScope = m_currentScope->createNewChildScope(type, name).get();
70 }
71 
leaveEnvironment()72 void FindUnqualifiedIDVisitor::leaveEnvironment()
73 {
74     m_currentScope = m_currentScope->parentScope();
75 }
76 
parseHeaders(QQmlJS::AST::UiHeaderItemList * header)77 void FindUnqualifiedIDVisitor::parseHeaders(QQmlJS::AST::UiHeaderItemList *header)
78 {
79     using namespace QQmlJS::AST;
80 
81     while (header) {
82         if (auto import = cast<UiImport *>(header->headerItem)) {
83             if (import->version) {
84                 QString path;
85                 auto uri = import->importUri;
86                 while (uri) {
87                     path.append(uri->name);
88                     path.append("/");
89                     uri = uri->next;
90                 }
91                 path.chop(1);
92                 importHelper(path,
93                              import->asToken.isValid() ? import->importId.toString() : QString(),
94                              import->version->majorVersion,
95                              import->version->minorVersion);
96             }
97         }
98         header = header->next;
99     }
100 }
101 
parseProgram(QQmlJS::AST::Program * program,const QString & name)102 ScopeTree *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 *>(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(parameters->element->bindingIdentifier.toString(), "");
113             result->addMethod(method);
114         }
115     }
116     return result;
117 }
118 
119 enum ImportVersion { FullyVersioned, PartiallyVersioned, Unversioned, BasePath };
120 
completeImportPaths(const QString & uri,const QString & basePath,int vmaj,int vmin)121 QStringList 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(QLatin1Char('.'), Qt::SkipEmptyParts);
127 
128     QStringList qmlDirPathsPaths;
129     // fully & partially versioned parts + 1 unversioned for each base path
130     qmlDirPathsPaths.reserve(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(".%1.%2").arg(vmaj).arg(vmin);
137         }
138         if (version == PartiallyVersioned) {
139             // extension with encoded version major (eg. MyModule.3)
140             return QString::fromLatin1(".%1").arg(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(Slash) && !dir.endsWith(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(0, index + 1), Slash)
176                         + ver + Slash
177                         + joinStringRefs(parts.mid(index + 1), Slash);
178             }
179         }
180     }
181     return qmlDirPathsPaths;
182 }
183 
184 static const QLatin1String SlashQmldir             = QLatin1String("/qmldir");
185 static const QLatin1String SlashPluginsDotQmltypes = QLatin1String("/plugins.qmltypes");
186 
readQmltypes(const QString & filename,FindUnqualifiedIDVisitor::Import & result)187 void 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(reader.errorMessage());
194 }
195 
readQmldir(const QString & path)196 FindUnqualifiedIDVisitor::Import FindUnqualifiedIDVisitor::readQmldir(const QString &path)
197 {
198     Import result;
199     auto reader = createQmldirParserForFile(path + SlashQmldir);
200     const auto imports = reader.imports();
201     for (const QString &import : imports)
202         result.dependencies.append(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(filePath)) {
209             m_colorOut.write(QLatin1String("warning: "), Warning);
210             m_colorOut.write(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(it.key());
217         if (mo == qmlComponents.end())
218             mo = qmlComponents.insert(it.key(), localFile2ScopeTree(filePath));
219 
220         (*mo)->addExport(
221                     it.key(), reader.typeNamespace(),
222                     ComponentVersion(it->majorVersion, it->minorVersion));
223     }
224     for (auto it = qmlComponents.begin(), end = qmlComponents.end(); it != end; ++it)
225         result.objects.insert( it.key(), ScopeTree::ConstPtr(it.value()));
226 
227     if (!reader.plugins().isEmpty() && QFile::exists(path + SlashPluginsDotQmltypes))
228         readQmltypes(path + SlashPluginsDotQmltypes, result);
229 
230     return result;
231 }
232 
processImport(const QString & prefix,const FindUnqualifiedIDVisitor::Import & import)233 void FindUnqualifiedIDVisitor::processImport(const QString &prefix, const FindUnqualifiedIDVisitor::Import &import)
234 {
235     for (auto const &dependency : qAsConst(import.dependencies)) {
236         auto const split = dependency.split(" ");
237         auto const &id = split.at(0);
238         if (split.length() > 1) {
239             const auto version = split.at(1).split('.');
240             importHelper(id, QString(),
241                          version.at(0).toInt(),
242                          version.length() > 1 ? version.at(1).toInt() : -1);
243         } else {
244             importHelper(id, QString(), -1, -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(prefixedName(prefix, val->className()), val);
255 
256         const auto exports = val->exports();
257         for (const auto &valExport : exports)
258             m_exportedName2Scope.insert(prefixedName(prefix, valExport.type()), val);
259 
260         const auto enums = val->enums();
261         for (const auto &valEnum : enums)
262             m_currentScope->addEnum(valEnum);
263     }
264 }
265 
importHelper(const QString & module,const QString & prefix,int major,int minor)266 void FindUnqualifiedIDVisitor::importHelper(const QString &module, const QString &prefix,
267                                             int major, int minor)
268 {
269     const QString id = QString(module).replace(QLatin1Char('/'), QLatin1Char('.'));
270     QPair<QString, QString> importId { id, prefix };
271     if (m_alreadySeenImports.contains(importId))
272         return;
273     m_alreadySeenImports.insert(importId);
274 
275     for (const QString &qmltypeDir : m_qmltypeDirs) {
276         auto qmltypesPaths = completeImportPaths(id, qmltypeDir, major, minor);
277 
278         for (auto const &qmltypesPath : qmltypesPaths) {
279             if (QFile::exists(qmltypesPath + SlashQmldir)) {
280                 processImport(prefix, readQmldir(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(it.next(), result);
296 
297             processImport(prefix, result);
298         }
299     }
300 
301     if (!m_qmltypeFiles.isEmpty())
302     {
303         Import result;
304 
305         for (const auto &qmltypeFile : m_qmltypeFiles)
306             readQmltypes(qmltypeFile, result);
307 
308         processImport("", result);
309     }
310 }
311 
localFile2ScopeTree(const QString & filePath)312 ScopeTree *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(".ui") ? baseName.chopped(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(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 = */ 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(program->headers);
349         ImportedMembersVisitor membersVisitor(&m_colorOut);
350         program->members->accept(&membersVisitor);
351         return membersVisitor.result(scopeName);
352     }
353 
354     // TODO: Anything special to do with ES modules here?
355     return parseProgram(QQmlJS::AST::cast<QQmlJS::AST::Program *>(parser.rootNode()), scopeName);
356 }
357 
importFileOrDirectory(const QString & fileOrDirectory,const QString & prefix)358 void 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(name);
365 
366     if (QFileInfo(name).isFile()) {
367         m_exportedName2Scope.insert(prefix, ScopeTree::ConstPtr(localFile2ScopeTree(name)));
368         return;
369     }
370 
371     QDirIterator it { name, QStringList() << QLatin1String("*.qml"), QDir::NoFilter };
372     while (it.hasNext()) {
373         ScopeTree::ConstPtr scope(localFile2ScopeTree(it.next()));
374         if (!scope->className().isEmpty())
375             m_exportedName2Scope.insert(prefixedName(prefix, scope->className()), scope);
376     }
377 }
378 
importExportedNames(const QStringRef & prefix,QString name)379 void FindUnqualifiedIDVisitor::importExportedNames(const QStringRef &prefix, QString name)
380 {
381     QList<ScopeTree::ConstPtr> scopes;
382     for (;;) {
383         ScopeTree::ConstPtr scope = m_exportedName2Scope.value(m_exportedName2Scope.contains(name)
384                                                                ? name
385                                                                : prefix + QLatin1Char('.') + name);
386         if (scope) {
387             if (scopes.contains(scope)) {
388                 QString inheritenceCycle = name;
389                 for (const auto &seen: qAsConst(scopes)) {
390                     inheritenceCycle.append(QLatin1String(" -> "));
391                     inheritenceCycle.append(seen->superclassName());
392                 }
393 
394                 m_colorOut.write(QLatin1String("Warning: "), Warning);
395                 m_colorOut.write(QString::fromLatin1("%1 is part of an inheritance cycle: %2\n")
396                                  .arg(name)
397                                  .arg(inheritenceCycle));
398                 m_unknownImports.insert(name);
399                 m_visitFailed = true;
400                 break;
401             }
402             scopes.append(scope);
403             const auto properties = scope->properties();
404             for (auto property : properties) {
405                 property.setType(m_exportedName2Scope.value(property.typeName()).get());
406                 m_currentScope->insertPropertyIdentifier(property);
407             }
408 
409             m_currentScope->addMethods(scope->methods());
410             name = scope->superclassName();
411             if (name.isEmpty() || name == QLatin1String("QObject"))
412                 break;
413         } else {
414             m_colorOut.write(QLatin1String("warning: "), Warning);
415             m_colorOut.write(name + QLatin1String(" was not found."
416                                                   " Did you add all import paths?\n"));
417             m_unknownImports.insert(name);
418             m_visitFailed = true;
419             break;
420         }
421     }
422 }
423 
throwRecursionDepthError()424 void FindUnqualifiedIDVisitor::throwRecursionDepthError()
425 {
426     m_colorOut.write(QStringLiteral("Error"), Error);
427     m_colorOut.write(QStringLiteral("Maximum statement or expression depth exceeded"), Error);
428     m_visitFailed = true;
429 }
430 
visit(QQmlJS::AST::UiProgram *)431 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiProgram *)
432 {
433     enterEnvironment(ScopeType::QMLScope, "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(it.next());
442             auto succ = reader(&objects, &moduleApis, &dependencies);
443             if (!succ)
444                 m_colorOut.writeUncolored(reader.errorMessage());
445         }
446     }
447 
448     if (!m_qmltypeFiles.isEmpty())
449     {
450         for (const auto &qmltypeFile : m_qmltypeFiles) {
451             auto reader = createQmltypesReaderForFile(qmltypeFile);
452             auto succ = reader(&objects, &moduleApis, &dependencies);
453             if (!succ)
454                 m_colorOut.writeUncolored(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(valExport.type(), val);
466 
467         const auto enums = val->enums();
468         for (const auto &valEnum : enums)
469             m_currentScope->addEnum(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(QFileInfo { m_filePath }.baseName(), {});
474 
475     importFileOrDirectory(".", QString());
476     return true;
477 }
478 
endVisit(QQmlJS::AST::UiProgram *)479 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiProgram *)
480 {
481     leaveEnvironment();
482 }
483 
visit(QQmlJS::AST::ClassExpression * ast)484 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassExpression *ast)
485 {
486     enterEnvironment(ScopeType::JSFunctionScope, ast->name.toString());
487     return true;
488 }
489 
endVisit(QQmlJS::AST::ClassExpression *)490 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassExpression *)
491 {
492     leaveEnvironment();
493 }
494 
visit(QQmlJS::AST::ClassDeclaration * ast)495 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ClassDeclaration *ast)
496 {
497     enterEnvironment(ScopeType::JSFunctionScope, ast->name.toString());
498     return true;
499 }
500 
endVisit(QQmlJS::AST::ClassDeclaration *)501 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ClassDeclaration *)
502 {
503     leaveEnvironment();
504 }
505 
visit(QQmlJS::AST::ForStatement *)506 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForStatement *)
507 {
508     enterEnvironment(ScopeType::JSLexicalScope, "forloop");
509     return true;
510 }
511 
endVisit(QQmlJS::AST::ForStatement *)512 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForStatement *)
513 {
514     leaveEnvironment();
515 }
516 
visit(QQmlJS::AST::ForEachStatement *)517 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::ForEachStatement *)
518 {
519     enterEnvironment(ScopeType::JSLexicalScope, "foreachloop");
520     return true;
521 }
522 
endVisit(QQmlJS::AST::ForEachStatement *)523 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::ForEachStatement *)
524 {
525     leaveEnvironment();
526 }
527 
visit(QQmlJS::AST::Block *)528 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Block *)
529 {
530     enterEnvironment(ScopeType::JSLexicalScope, "block");
531     return true;
532 }
533 
endVisit(QQmlJS::AST::Block *)534 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Block *)
535 {
536     leaveEnvironment();
537 }
538 
visit(QQmlJS::AST::CaseBlock *)539 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::CaseBlock *)
540 {
541     enterEnvironment(ScopeType::JSLexicalScope, "case");
542     return true;
543 }
544 
endVisit(QQmlJS::AST::CaseBlock *)545 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::CaseBlock *)
546 {
547     leaveEnvironment();
548 }
549 
visit(QQmlJS::AST::Catch * catchStatement)550 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::Catch *catchStatement)
551 {
552     enterEnvironment(ScopeType::JSLexicalScope, "catch");
553     m_currentScope->insertJSIdentifier(catchStatement->patternElement->bindingIdentifier.toString(),
554                                        QQmlJS::AST::VariableScope::Let);
555     return true;
556 }
557 
endVisit(QQmlJS::AST::Catch *)558 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::Catch *)
559 {
560     leaveEnvironment();
561 }
562 
visit(QQmlJS::AST::WithStatement * withStatement)563 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::WithStatement *withStatement)
564 {
565     m_colorOut.write(QString::fromLatin1("Warning: "), Warning);
566     m_colorOut.write(QString::fromLatin1(
567                          "%1:%2: with statements are strongly discouraged in QML "
568                          "and might cause false positives when analysing unqalified identifiers\n")
569                      .arg(withStatement->firstSourceLocation().startLine)
570                      .arg(withStatement->firstSourceLocation().startColumn),
571                      Normal);
572     enterEnvironment(ScopeType::JSLexicalScope, "with");
573     return true;
574 }
575 
endVisit(QQmlJS::AST::WithStatement *)576 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::WithStatement *)
577 {
578     leaveEnvironment();
579 }
580 
signalName(const QStringRef & handlerName)581 static QString signalName(const QStringRef &handlerName)
582 {
583     if (handlerName.startsWith("on") && handlerName.size() > 2) {
584         QString signal = handlerName.mid(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 
visit(QQmlJS::AST::UiScriptBinding * uisb)598 bool 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 *>(uisb->statement);
605         auto identexp = cast<IdentifierExpression *>(expstat->expression);
606         QString elementName = m_currentScope->name();
607         m_qmlid2scope.insert(identexp->name.toString(), m_currentScope);
608         if (m_currentScope->isVisualRootScope())
609             m_rootId = identexp->name.toString();
610     } else {
611         const QString signal = signalName(name);
612         if (signal.isEmpty())
613             return true;
614 
615         if (!m_currentScope->methods().contains(signal)) {
616             m_currentScope->addUnmatchedSignalHandler(name.toString(), 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 *>(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(param, method, firstSourceLocation,
636                                                    hasMultilineStatementBody);
637         }
638         return true;
639     }
640     return true;
641 }
642 
visit(QQmlJS::AST::UiPublicMember * uipm)643 bool 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(property.typeName()).get());
657     m_currentScope->insertPropertyIdentifier(property);
658     return true;
659 }
660 
visit(QQmlJS::AST::IdentifierExpression * idexp)661 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp)
662 {
663     auto name = idexp->name;
664     m_currentScope->addIdToAccessed(name.toString(), idexp->firstSourceLocation());
665     m_fieldMemberBase = idexp;
666     return true;
667 }
668 
FindUnqualifiedIDVisitor(QStringList qmltypeDirs,QStringList qmltypeFiles,QString code,QString fileName,bool silent)669 FindUnqualifiedIDVisitor::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(Error, ColorOutput::RedForeground);
682     m_colorOut.insertMapping(Warning, ColorOutput::PurpleForeground);
683     m_colorOut.insertMapping(Info, ColorOutput::BlueForeground);
684     m_colorOut.insertMapping(Normal, ColorOutput::DefaultColor);
685     m_colorOut.insertMapping(Hint, 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(QString::fromLatin1(*globalName),
702                                            QQmlJS::AST::VariableScope::Const);
703     }
704     for (const auto& jsGlobVar: jsGlobVars)
705         m_currentScope->insertJSIdentifier(jsGlobVar, QQmlJS::AST::VariableScope::Const);
706 }
707 
check()708 bool 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(targetScope->methods());
718         QScopedValueRollback<ScopeTree*> rollback(m_currentScope, outstandingConnection.scope);
719         outstandingConnection.uiod->initializer->accept(this);
720     }
721     return m_rootScope->recheckIdentifiers(m_code, m_qmlid2scope, m_exportedName2Scope,
722                                            m_rootScope.get(), m_rootId, m_colorOut);
723 }
724 
visit(QQmlJS::AST::VariableDeclarationList * vdl)725 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::VariableDeclarationList *vdl)
726 {
727     while (vdl) {
728         m_currentScope->insertJSIdentifier(vdl->declaration->bindingIdentifier.toString(),
729                                            vdl->declaration->scope);
730         vdl = vdl->next;
731     }
732     return true;
733 }
734 
visitFunctionExpressionHelper(QQmlJS::AST::FunctionExpression * fexpr)735 void 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(MetaMethod(name, QLatin1String("void")));
742         else
743             m_currentScope->insertJSIdentifier(name, VariableScope::Const);
744         enterEnvironment(ScopeType::JSFunctionScope, name);
745     } else {
746         enterEnvironment(ScopeType::JSFunctionScope, QLatin1String("<anon>"));
747     }
748 }
749 
visit(QQmlJS::AST::FunctionExpression * fexpr)750 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionExpression *fexpr)
751 {
752     visitFunctionExpressionHelper(fexpr);
753     return true;
754 }
755 
endVisit(QQmlJS::AST::FunctionExpression *)756 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionExpression *)
757 {
758     leaveEnvironment();
759 }
760 
visit(QQmlJS::AST::FunctionDeclaration * fdecl)761 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FunctionDeclaration *fdecl)
762 {
763     visitFunctionExpressionHelper(fdecl);
764     return true;
765 }
766 
endVisit(QQmlJS::AST::FunctionDeclaration *)767 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FunctionDeclaration *)
768 {
769     leaveEnvironment();
770 }
771 
visit(QQmlJS::AST::FormalParameterList * fpl)772 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FormalParameterList *fpl)
773 {
774     for (auto const &boundName : fpl->boundNames()) {
775         m_currentScope->insertJSIdentifier(boundName.id, QQmlJS::AST::VariableScope::Const);
776     }
777     return true;
778 }
779 
visit(QQmlJS::AST::UiImport * import)780 bool 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(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(importId, m_exportedName2Scope.value(importId).get());
796     }
797     if (import->version) {
798         auto uri = import->importUri;
799         while (uri) {
800             path.append(uri->name);
801             path.append("/");
802             uri = uri->next;
803         }
804         path.chop(1);
805 
806         importHelper(path, prefix, import->version->majorVersion, import->version->minorVersion);
807     }
808     return true;
809 }
810 
visit(QQmlJS::AST::UiEnumDeclaration * uied)811 bool 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(member->member.toString());
816     m_currentScope->addEnum(qmlEnum);
817     return true;
818 }
819 
visit(QQmlJS::AST::UiObjectBinding * uiob)820 bool 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(1);
832 
833     MetaProperty prop(uiob->qualifiedId->name.toString(), name, false, true, true,
834                       name == QLatin1String("alias"), 0);
835     prop.setType(m_exportedName2Scope.value(uiob->qualifiedTypeNameId->name.toString()).get());
836     m_currentScope->addProperty(prop);
837 
838     enterEnvironment(ScopeType::QMLScope, name);
839     importExportedNames(prefix, name);
840     return true;
841 }
842 
endVisit(QQmlJS::AST::UiObjectBinding * uiob)843 void 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(property);
854 }
855 
visit(QQmlJS::AST::UiObjectDefinition * uiod)856 bool 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(1);
868     enterEnvironment(ScopeType::QMLScope, name);
869     if (name.isLower())
870         return false; // Ignore grouped properties for now
871 
872     importExportedNames(prefix, name);
873     if (name.endsWith("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*>(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(scope->name()).get();
901         } else {
902             // there was a target, check if we already can find it
903             auto scopeIt =  m_qmlid2scope.find(target);
904             if (scopeIt != m_qmlid2scope.end()) {
905                 targetScope = *scopeIt;
906             } else {
907                 m_outstandingConnections.push_back({target, m_currentScope, uiod});
908                 return false; // visit children later once target is known
909             }
910         }
911         if (targetScope)
912             m_currentScope->addMethods(targetScope->methods());
913     }
914     return true;
915 }
916 
visit(QQmlJS::AST::PatternElement * element)917 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::PatternElement *element)
918 {
919     if (element->isVariableDeclaration()) {
920         QQmlJS::AST::BoundNames names;
921         element->boundNames(&names);
922         for (const auto &name : names)
923             m_currentScope->insertJSIdentifier(name.id, element->scope);
924     }
925 
926     return true;
927 }
928 
endVisit(QQmlJS::AST::UiObjectDefinition *)929 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::UiObjectDefinition *)
930 {
931     auto childScope = m_currentScope;
932     leaveEnvironment();
933     childScope->updateParentProperty(m_currentScope);
934 }
935 
visit(QQmlJS::AST::FieldMemberExpression *)936 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::FieldMemberExpression *)
937 {
938     return true;
939 }
940 
endVisit(QQmlJS::AST::FieldMemberExpression * fieldMember)941 void FindUnqualifiedIDVisitor::endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember)
942 {
943     using namespace QQmlJS::AST;
944     ExpressionNode *base = fieldMember->base;
945     while (auto *nested = cast<NestedExpression *>(base))
946         base = nested->expression;
947 
948     if (m_fieldMemberBase == base) {
949         QString type;
950         if (auto *binary = cast<BinaryExpression *>(base)) {
951             if (binary->op == QSOperator::As) {
952                 // This is terrible. It's fixed in 6.0.
953                 if (auto *right = cast<Type *>(static_cast<Node *>(binary->right)))
954                     type = right->toString();
955             }
956         }
957         m_currentScope->accessMember(fieldMember->name.toString(),
958                                      type,
959                                      fieldMember->identifierToken);
960         m_fieldMemberBase = fieldMember;
961     } else {
962         m_fieldMemberBase = nullptr;
963     }
964 }
965 
visit(QQmlJS::AST::BinaryExpression *)966 bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::BinaryExpression *)
967 {
968     return true;
969 }
970 
endVisit(QQmlJS::AST::BinaryExpression * binExp)971 void 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