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 ¶m : 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