Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmljslintervisitor.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:significant
4
6
7#include <private/qqmljsutils_p.h>
8
10
11using namespace Qt::StringLiterals;
12using namespace QQmlJS::AST;
13
14namespace QQmlJS {
15/*!
16 \internal
17 \class QQmlJS::LinterVisitor
18 Extends QQmlJSImportVisitor with extra warnings that are required for linting but unrelated to
19 QQmlJSImportVisitor actual task that is constructing QQmlJSScopes. One example of such warnings
20 are purely syntactic checks, or style-checks warnings that don't make sense during compilation.
21 */
22
23LinterVisitor::LinterVisitor(
24 QQmlJSImporter *importer, QQmlJSLogger *logger,
25 const QString &implicitImportDirectory, const QStringList &qmldirFiles,
26 QQmlJS::Engine *engine)
29{
30}
31
32void LinterVisitor::leaveEnvironment()
33{
34 const auto leaveEnv = qScopeGuard([this] { QQmlJSImportVisitor::leaveEnvironment(); });
35
36 if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
37 return;
38
39 if (auto base = m_currentScope->baseType()) {
40 if (base->internalName() == u"QQmlComponent"_s) {
41 const auto nChildren = std::count_if(
42 m_currentScope->childScopesBegin(), m_currentScope->childScopesEnd(),
43 [](const QQmlJSScope::ConstPtr &scope) {
44 return scope->scopeType() == QQmlSA::ScopeType::QMLScope;
45 });
46 if (nChildren != 1) {
47 m_logger->log("Components must have exactly one child"_L1,
48 qmlComponentChildrenCount, m_currentScope->sourceLocation());
49 }
50 }
51 }
52}
53
54bool LinterVisitor::visit(StringLiteral *sl)
55{
56 QQmlJSImportVisitor::visit(sl);
57 const QString s = m_logger->code().mid(sl->literalToken.begin(), sl->literalToken.length);
58
59 if (s.contains(QLatin1Char('\r')) || s.contains(QLatin1Char('\n')) || s.contains(QChar(0x2028u))
60 || s.contains(QChar(0x2029u))) {
61 QString templateString;
62
63 bool escaped = false;
64 const QChar stringQuote = s[0];
65 for (qsizetype i = 1; i < s.size() - 1; i++) {
66 const QChar c = s[i];
67
68 if (c == u'\\') {
69 escaped = !escaped;
70 } else if (escaped) {
71 // If we encounter an escaped quote, unescape it since we use backticks here
72 if (c == stringQuote)
73 templateString.chop(1);
74
75 escaped = false;
76 } else {
77 if (c == u'`')
78 templateString += u'\\';
79 if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{')
80 templateString += u'\\';
81 }
82
83 templateString += c;
84 }
85
86 QQmlJSDocumentEdit documentEdit{
87 m_logger->filePath(), sl->literalToken, u"`" % templateString % u"`"
88 };
89 QQmlJSFixSuggestion suggestion = { "Use a template literal instead."_L1, sl->literalToken,
90 documentEdit };
91 suggestion.setAutoApplicable();
92 m_logger->log(QStringLiteral("String contains unescaped line terminator which is "
93 "deprecated."),
94 qmlMultilineStrings, sl->literalToken, true, true, suggestion);
95 }
96 return true;
97}
98
99bool LinterVisitor::preVisit(Node *n)
100{
101 m_ancestryIncludingCurrentNode.push_back(n);
102 return true;
103}
104
105void LinterVisitor::postVisit(Node *n)
106{
107 Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n);
108 m_ancestryIncludingCurrentNode.pop_back();
109}
110
111Node *LinterVisitor::astParentOfVisitedNode() const
112{
113 if (m_ancestryIncludingCurrentNode.size() < 2)
114 return nullptr;
115 return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2];
116}
117
118bool LinterVisitor::visit(CommaExpression *expression)
119{
120 QQmlJSImportVisitor::visit(expression);
121 if (!expression->left || !expression->right)
122 return true;
123
124 // don't warn about commas in "for" statements
125 if (cast<ForStatement *>(astParentOfVisitedNode()))
126 return true;
127
128 m_logger->log("Do not use comma expressions."_L1, qmlComma, expression->commaToken);
129 return true;
130}
131
132static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger)
133{
134 static constexpr std::array literals{ "Boolean"_L1, "Function"_L1, "JSON"_L1,
135 "Math"_L1, "Number"_L1, "String"_L1 };
136
137 const IdentifierExpression *identifier = cast<IdentifierExpression *>(expression->base);
138 if (!identifier)
139 return;
140
141 if (std::find(literals.cbegin(), literals.cend(), identifier->name) != literals.cend()) {
142 logger->log("Do not use '%1' as a constructor."_L1.arg(identifier->name),
143 qmlLiteralConstructor, identifier->identifierToken);
144 }
145 if (identifier->name == "Array"_L1 && expression->arguments && expression->arguments->next) {
146 const auto fullRange = combine(expression->newToken, expression->rparenToken);
147 const QList<QQmlJSDocumentEdit> edits = {
148 { logger->filePath(), combine(expression->newToken, expression->lparenToken), "["_L1 },
149 { logger->filePath(), expression->rparenToken, "]"_L1 },
150 };
151 QQmlJSFixSuggestion fix("Replace with array literal"_L1, fullRange, edits);
152 fix.setAutoApplicable(true);
153 logger->log("Array has confusing semantics, use an array literal ([]) instead."_L1,
154 qmlLiteralConstructor, identifier->identifierToken, true, true, fix);
155 }
156}
157
158bool LinterVisitor::visit(NewMemberExpression *expression)
159{
160 QQmlJSImportVisitor::visit(expression);
161 warnAboutLiteralConstructors(expression, m_logger);
162 return true;
163}
164
165bool LinterVisitor::visit(VoidExpression *ast)
166{
167 QQmlJSImportVisitor::visit(ast);
168 m_logger->log("Do not use void expressions."_L1, qmlVoid, ast->voidToken);
169 return true;
170}
171
172static SourceLocation confusingPluses(BinaryExpression *exp)
173{
174 Q_ASSERT(exp->op == QSOperator::Add);
175
176 SourceLocation location = exp->operatorToken;
177
178 // a++ + b
179 if (auto increment = cast<PostIncrementExpression *>(exp->left))
180 location = combine(increment->incrementToken, location);
181 // a + +b
182 if (auto unary = cast<UnaryPlusExpression *>(exp->right))
183 location = combine(location, unary->plusToken);
184 // a + ++b
185 if (auto increment = cast<PreIncrementExpression *>(exp->right))
186 location = combine(location, increment->incrementToken);
187
188 if (location == exp->operatorToken)
189 return SourceLocation{};
190
191 return location;
192}
193
194static SourceLocation confusingMinuses(BinaryExpression *exp)
195{
196 Q_ASSERT(exp->op == QSOperator::Sub);
197
198 SourceLocation location = exp->operatorToken;
199
200 // a-- - b
201 if (auto decrement = cast<PostDecrementExpression *>(exp->left))
202 location = combine(decrement->decrementToken, location);
203 // a - -b
204 if (auto unary = cast<UnaryMinusExpression *>(exp->right))
205 location = combine(location, unary->minusToken);
206 // a - --b
207 if (auto decrement = cast<PreDecrementExpression *>(exp->right))
208 location = combine(location, decrement->decrementToken);
209
210 if (location == exp->operatorToken)
211 return SourceLocation{};
212
213 return location;
214}
215
216bool LinterVisitor::visit(BinaryExpression *exp)
217{
218 QQmlJSImportVisitor::visit(exp);
219 switch (exp->op) {
220 case QSOperator::Add:
221 if (SourceLocation loc = confusingPluses(exp); loc.isValid())
222 m_logger->log("Confusing pluses."_L1, qmlConfusingPluses, loc);
223 break;
224 case QSOperator::Sub:
225 if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
226 m_logger->log("Confusing minuses."_L1, qmlConfusingMinuses, loc);
227 break;
228 default:
229 break;
230 }
231
232 return true;
233}
234
235bool LinterVisitor::visit(QQmlJS::AST::UiImport *import)
236{
237 QQmlJSImportVisitor::visit(import);
238
239 const auto locAndName = [](const UiImport *i) {
240 if (!i->importUri)
241 return std::make_pair(i->fileNameToken, i->fileName.toString());
242
243 QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
244 if (i->importIdToken.isValid())
245 l = combine(l, i->importIdToken);
246 else if (i->version)
247 l = combine(l, i->version->minorToken);
248 else
249 l = combine(l, i->importUri->lastSourceLocation());
250
251 return std::make_pair(l, i->importUri->toString());
252 };
253
254 SeenImport i(import);
255 if (const auto it = m_seenImports.constFind(i); it != m_seenImports.constEnd()) {
256 const auto locAndNameImport = locAndName(import);
257 const auto locAndNameSeen = locAndName(it->uiImport);
258 m_logger->log("Duplicate import '%1'"_L1.arg(locAndNameImport.second),
259 qmlDuplicateImport, locAndNameImport.first);
260 m_logger->log("Note: previous import '%1' here"_L1.arg(locAndNameSeen.second),
261 qmlDuplicateImport, locAndNameSeen.first, true, true, {},
262 locAndName(import).first.startLine);
263 }
264
265 m_seenImports.insert(i);
266 return true;
267}
268
269void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
270 const QQmlJS::SourceLocation &location)
271{
272 m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
273 location);
274 for (const auto *member = members; member; member = member->next) {
275 if (member->member.toString() == key) {
276 m_logger->log(u"Note: previous declaration of '%1' here"_s.arg(key),
277 qmlDuplicateEnumEntries, member->memberToken);
278 return;
279 }
280 }
281}
282
283bool LinterVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied)
284{
285 QQmlJSImportVisitor::visit(uied);
286
287 if (m_currentScope->isInlineComponent()) {
288 m_logger->log(u"Enums declared inside of inline component are ignored."_s,
289 qmlInlineComponentEnums, uied->firstSourceLocation());
290 } else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No
291 && !m_currentScope->isFileRootComponent()) {
292 m_logger->log(u"Enum declared outside the root element. It won't be accessible."_s,
293 qmlNonRootEnums, uied->firstSourceLocation());
294 }
295
296 QHash<QStringView, const QQmlJS::AST::UiEnumMemberList *> seen;
297 for (const auto *member = uied->members; member; member = member->next) {
298 QStringView key = member->member;
299 if (!key.front().isUpper()) {
300 m_logger->log(u"Enum keys should start with an uppercase."_s, qmlEnumKeyCase,
301 member->memberToken);
302 }
303
304 if (seen.contains(key))
305 handleDuplicateEnums(uied->members, key, member->memberToken);
306 else
307 seen[member->member] = member;
308
309 if (uied->name == key) {
310 m_logger->log("Enum entry should be named differently than the enum itself to avoid "
311 "confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
312 }
313 }
314
315 return true;
316}
317
318static bool allCodePathsReturnInsideCase(Node *statement)
319{
320 using namespace AST;
321 if (!statement)
322 return false;
323
324 switch (statement->kind) {
325 case Node::Kind_Block: {
326 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
327 }
328 case Node::Kind_BreakStatement:
329 return true;
330 case Node::Kind_CaseBlock: {
331 const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
332 if (caseBlock->defaultClause)
333 return allCodePathsReturnInsideCase(caseBlock->defaultClause);
334 return allCodePathsReturnInsideCase(caseBlock->clauses);
335 }
336 case Node::Kind_CaseClause:
337 return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
338 case Node::Kind_CaseClauses: {
339 for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
340 caseClauses = caseClauses->next) {
341 if (!allCodePathsReturnInsideCase(caseClauses->clause))
342 return false;
343 }
344 return true;
345 }
346 case Node::Kind_ContinueStatement:
347 // allCodePathsReturn() doesn't recurse into loops, so any encountered `continue` should
348 // belong to a loop outside the switch statement.
349 return true;
350 case Node::Kind_DefaultClause:
351 return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
352 case Node::Kind_IfStatement: {
353 const auto *ifStatement = cast<IfStatement *>(statement);
354 return allCodePathsReturnInsideCase(ifStatement->ok)
355 && allCodePathsReturnInsideCase(ifStatement->ko);
356 }
357 case Node::Kind_LabelledStatement:
358 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
359 case Node::Kind_ReturnStatement:
360 return true;
361 case Node::Kind_StatementList: {
362 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
363 if (allCodePathsReturnInsideCase(list->statement))
364 return true;
365 }
366 return false;
367 }
368 case Node::Kind_SwitchStatement:
369 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
370 case Node::Kind_ThrowStatement:
371 return true;
372 case Node::Kind_TryStatement: {
373 auto *tryStatement = cast<TryStatement *>(statement);
374 if (allCodePathsReturnInsideCase(tryStatement->statement))
375 return true;
376 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
377 }
378 case Node::Kind_WithStatement:
379 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
380 default:
381 break;
382 }
383 return false;
384}
385
386void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
387 SourceLocation nextLoc)
388{
389 if (!statements || !nextLoc.isValid())
390 return;
391
392 if (allCodePathsReturnInsideCase(statements))
393 return;
394
395 quint32 afterLastStatement = 0;
396 for (StatementList *it = statements; it; it = it->next) {
397 if (!it->next) {
398 afterLastStatement = it->statement->lastSourceLocation().end();
399 }
400 }
401
402 const auto &comments = m_engine->comments();
403 auto it = std::find_if(comments.cbegin(), comments.cend(),
404 [&](auto c) { return afterLastStatement < c.offset; });
405 auto end = std::find_if(it, comments.cend(),
406 [&](auto c) { return c.offset >= nextLoc.offset; });
407
408 for (; it != end; ++it) {
409 const QString &commentText = m_engine->code().mid(it->offset, it->length);
410 if (commentText.contains("fall through"_L1, Qt::CaseInsensitive)
411 || commentText.contains("fall-through"_L1, Qt::CaseInsensitive)
412 || commentText.contains("fallthrough"_L1, Qt::CaseInsensitive)) {
413 return;
414 }
415 }
416
417 m_logger->log(
418 "Non-empty case block potentially falls through to the next case or default statement. "
419 "Add \"// fallthrough\" at the end of the block to silence this warning."_L1,
420 qmlUnterminatedCase, errorLoc);
421}
422
423bool LinterVisitor::visit(QQmlJS::AST::CaseBlock *block)
424{
425 QQmlJSImportVisitor::visit(block);
426
427 std::vector<std::pair<SourceLocation, StatementList *>> clauses;
428 for (CaseClauses *it = block->clauses; it; it = it->next)
429 clauses.push_back({ it->clause->caseToken, it->clause->statements });
430 if (block->defaultClause)
431 clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
432 for (CaseClauses *it = block->moreClauses; it; it = it->next)
433 clauses.push_back({ it->clause->caseToken, it->clause->statements });
434
435 // check all but the last clause for fallthrough
436 for (size_t i = 0; i < clauses.size() - 1; ++i) {
437 const SourceLocation nextToken = clauses[i + 1].first;
438 checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
439 }
440 return true;
441}
442
443static QList<const Statement *> possibleLastStatements(const StatementList *ast);
444static QList<const Statement *> possibleLastStatements(const CaseBlock *ast)
445{
446 QList<const Statement *> lasts;
447
448 for (const auto *clause = ast->clauses; clause; clause = clause->next)
449 lasts << possibleLastStatements(clause->clause->statements);
450 if (ast->defaultClause)
451 lasts << possibleLastStatements(ast->defaultClause->statements);
452 for (const auto *clause = ast->moreClauses; clause; clause = clause->next)
453 lasts << possibleLastStatements(clause->clause->statements);
454
455 return lasts;
456}
457
458static QList<const Statement *> possibleLastStatements(const Statement *ast)
459{
460 if (const auto *s = cast<const Block *>(ast))
461 return possibleLastStatements(s->statements) << s;
462 if (const auto *s = cast<const BreakStatement *>(ast))
463 return { s };
464 if (const auto *s = cast<const ContinueStatement *>(ast))
465 return { s };
466 if (const auto *s = cast<const DebuggerStatement *>(ast))
467 return { s };
468 if (const auto *s = cast<const DoWhileStatement *>(ast))
469 return possibleLastStatements(s->statement) << s;
470 if (const auto *s = cast<const EmptyStatement *>(ast))
471 return { s };
472 if (const auto *s = cast<const ExportDeclaration *>(ast))
473 return { s };
474 if (const auto *s = cast<const ExpressionStatement *>(ast))
475 return { s };
476 if (const auto *s = cast<const ForEachStatement *>(ast))
477 return possibleLastStatements(s->statement) << s;
478 if (const auto *s = cast<const ForStatement *>(ast))
479 return possibleLastStatements(s->statement) << s;
480 if (const auto *s = cast<const IfStatement *>(ast)) {
481 auto lasts = possibleLastStatements(s->ok);
482 if (s->ko)
483 lasts << possibleLastStatements(s->ko);
484 return lasts << s;
485 }
486 if (const auto *s = cast<const ImportDeclaration *>(ast))
487 return { s };
488 if (const auto *s = cast<const LabelledStatement *>(ast))
489 return possibleLastStatements(s->statement) << ast;
490 if (const auto *s = cast<const ReturnStatement *>(ast))
491 return { s };
492 if (const auto *s = cast<const SwitchStatement *>(ast))
493 return possibleLastStatements(s->block) << s;
494 if (const auto *s = cast<const ThrowStatement *>(ast))
495 return { s };
496 if (const auto *s = cast<const TryStatement *>(ast))
497 return { s };
498 if (const auto *s = cast<const VariableStatement *>(ast))
499 return { s };
500 if (const auto *s = cast<const WhileStatement *>(ast))
501 return possibleLastStatements(s->statement) << s;
502 if (const auto *s = cast<const WithStatement *>(ast))
503 return possibleLastStatements(s->statement) << s;
504
505 Q_UNREACHABLE_RETURN({});
506}
507
508static QList<const Statement *> possibleLastStatements(const StatementList *ast)
509{
510 if (!ast)
511 return {};
512 for (; ast->next; ast = ast->next) { }
513 const auto *statement = ast->statement;
514
515 // Can't store FunctionDeclaration as a statement. See StatementList.
516 if (cast<const FunctionDeclaration *>(statement))
517 return {};
518
519 return possibleLastStatements(static_cast<const Statement *>(statement));
520}
521
522static bool isUselessExpressionStatement_impl(const ExpressionNode *ast)
523{
524 switch (ast->kind) {
525 case Node::Kind_CallExpression:
526 case Node::Kind_DeleteExpression:
527 case Node::Kind_NewExpression:
528 case Node::Kind_PreDecrementExpression:
529 case Node::Kind_PreIncrementExpression:
530 case Node::Kind_PostDecrementExpression:
531 case Node::Kind_PostIncrementExpression:
532 case Node::Kind_YieldExpression:
533 case Node::Kind_FunctionExpression:
534 return false;
535 case Node::Kind_NumericLiteral:
536 case Node::Kind_StringLiteral:
537 case Node::Kind_FalseLiteral:
538 case Node::Kind_TrueLiteral:
539 case Node::Kind_NullExpression:
540 case Node::Kind_Undefined:
541 case Node::Kind_RegExpLiteral:
542 case Node::Kind_SuperLiteral:
543 case Node::Kind_ThisExpression:
544 case Node::Kind_FieldMemberExpression:
545 case Node::Kind_IdentifierExpression:
546 case Node::Kind_TypeOfExpression:
547 return true;
548 default:
549 break;
550 }
551
552 if (const auto *e = cast<const NestedExpression *>(ast))
553 return isUselessExpressionStatement_impl(e->expression);
554 if (const auto *e = cast<const NotExpression *>(ast))
555 return isUselessExpressionStatement_impl(e->expression);
556 if (const auto *e = cast<const TildeExpression *>(ast))
557 return isUselessExpressionStatement_impl(e->expression);
558 if (const auto *e = cast<const UnaryMinusExpression *>(ast))
559 return isUselessExpressionStatement_impl(e->expression);
560 if (const auto *e = cast<const UnaryPlusExpression *>(ast))
561 return isUselessExpressionStatement_impl(e->expression);
562 if (const auto *e = cast<const ConditionalExpression *>(ast))
563 return isUselessExpressionStatement_impl(e->ok) && isUselessExpressionStatement_impl(e->ko);
564
565 if (const BinaryExpression *binary = cast<const BinaryExpression *>(ast)) {
566 switch (binary->op) {
567 case QSOperator::InplaceAnd:
568 case QSOperator::Assign:
569 case QSOperator::InplaceSub:
570 case QSOperator::InplaceDiv:
571 case QSOperator::InplaceExp:
572 case QSOperator::InplaceAdd:
573 case QSOperator::InplaceLeftShift:
574 case QSOperator::InplaceMod:
575 case QSOperator::InplaceMul:
576 case QSOperator::InplaceOr:
577 case QSOperator::InplaceRightShift:
578 case QSOperator::InplaceURightShift:
579 case QSOperator::InplaceXor:
580 return false;
581 default:
582 return isUselessExpressionStatement_impl(binary->left)
583 && isUselessExpressionStatement_impl(binary->right);
584 }
585 }
586
587 return false;
588}
589
590/*!
591\internal
592
593This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
594*/
595static bool isUselessExpressionStatement(const ExpressionStatement *ast)
596{
597 return isUselessExpressionStatement_impl(ast->expression);
598}
599
600void LinterVisitor::handleUselessExpressionStatement(const ExpressionStatement *ast)
601{
602 // property binding, signal handler, or function declaration
603 const auto it = std::find_if(m_ancestryIncludingCurrentNode.crbegin(),
604 m_ancestryIncludingCurrentNode.crend(),
605 [](auto it) {
606 return it->kind == Node::Kind_UiPublicMember
607 || it->kind == Node::Kind_FunctionDeclaration
608 || it->kind == Node::Kind_UiScriptBinding;
609 });
610
611 if (it == m_ancestryIncludingCurrentNode.crend())
612 return;
613
614 // A useless ExpressionStatement in *last position* inside a property binding is not useless
615 const auto isLastExprStat = [](const ExpressionStatement *statement, const Statement *base) {
616 const auto lasts = possibleLastStatements(base);
617 return lasts.contains(statement);
618 };
619
620 if (const auto *usb = cast<UiScriptBinding *>(*it); usb && usb->qualifiedId) {
621 if (usb->qualifiedId->toString() == "id"_L1)
622 return;
623 if (usb->qualifiedId->next)
624 return; // group/attached property, give up
625 if (m_savedBindingOuterScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope)
626 return; // group property, give up
627
628 QQmlJSScope::Ptr object = m_currentScope;
629 while (object && object->scopeType() != QQmlSA::ScopeType::QMLScope)
630 object = object->parentScope();
631
632 if (!object)
633 return;
634
635 if (m_propertyBindings.contains(object)) {
636 for (const auto &entry : m_propertyBindings[object]) {
637 if (entry.data == usb->qualifiedId->toString()) {
638 if (isLastExprStat(ast, usb->statement))
639 return;
640 else
641 break;
642 }
643 }
644 }
645 }
646
647 const auto *upm = cast<const UiPublicMember *>(*it);
648 if (upm && upm->type == AST::UiPublicMember::Property && upm->statement) {
649 if (isLastExprStat(ast, upm->statement))
650 return;
651 }
652
653 if (isUselessExpressionStatement(ast)) {
654 m_logger->log("Expression statement has no obvious effect."_L1,
655 qmlConfusingExpressionStatement,
656 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
657 }
658}
659
660bool LinterVisitor::visit(ExpressionStatement *ast)
661{
662 QQmlJSImportVisitor::visit(ast);
663 handleUselessExpressionStatement(ast);
664 return true;
665}
666
667bool LinterVisitor::safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier)
668{
669 if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
670 identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
671 // var is generally not great, but we don't want to emit this warning if you
672 // are in the single, toplevel block of a binding
673 Q_ASSERT(!scope->parentScope().isNull()); // lexical scope should always have a parent
674 auto parentScopeType = scope->parentScope()->scopeType();
675 bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
676 || parentScopeType == QQmlSA::ScopeType::SignalHandlerFunctionScope;
677 if (!inTopLevelBindingBlockScope) {
678 m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
679 u"Replace it with const or let to silence the warning\n"_s,
680 qmlBlockScopeVarDeclaration, identifier.location);
681 }
682 } else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
683 const QQmlJSScope *scopePtr = scope.get();
684 std::pair<const QQmlJSScope*, QString> misplaced { scopePtr, name };
685 if (misplacedJSIdentifiers.contains(misplaced))
686 return false; // we only want to warn once
687 misplacedJSIdentifiers.insert(misplaced);
688 m_logger->log(u"JavaScript declarations are not allowed in QML elements"_s, qmlSyntax,
689 identifier.location);
690 return false;
691 }
692 return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
693}
694
696 const QString &name, const QQmlJS::AST::Statement *statement,
697 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
698{
699 if (statement && statement->kind == (int)AST::Node::Kind::Kind_Block) {
700 const auto *block = static_cast<const AST::Block *>(statement);
701 if (!block->statements && associatedPropertyDefinition) {
702 m_logger->log("Unintentional empty block, use ({}) for empty object literal"_L1,
703 qmlUnintentionalEmptyBlock,
704 combine(block->lbraceToken, block->rbraceToken));
705 }
706 }
707
708 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
709}
710
711void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding,
712 const UiPublicMember *associatedPropertyDefinition)
713{
714 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
715 return;
716
717 if (!associatedPropertyDefinition->isReadonly())
718 return;
719
720 const auto &prop = m_currentScope->property(binding.propertyName());
721 const auto log = [&](const QString &preferredType) {
722 m_logger->log("Prefer more specific type %1 over var"_L1.arg(preferredType),
723 qmlPreferNonVarProperties, prop.sourceLocation());
724 };
725
726 if (prop.typeName() != "QVariant"_L1)
727 return;
728
729 switch (binding.bindingType()) {
730 case QQmlSA::BindingType::BoolLiteral: {
731 log("bool"_L1);
732 break;
733 }
734 case QQmlSA::BindingType::NumberLiteral: {
735 double v = binding.numberValue();
736 auto loc = binding.sourceLocation();
737 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
738 if (literal.contains(u'.') || double(int(v)) != v)
739 log("real or double"_L1);
740 else
741 log("int"_L1);
742 break;
743 }
744 case QQmlSA::BindingType::StringLiteral: {
745 log("string"_L1);
746 break;
747 }
748 default: {
749 break;
750 }
751 }
752}
753
754bool LinterVisitor::visit(UiProgram *ast)
755{
756 const bool result = QQmlJSImportVisitor::visit(ast);
757
758 m_renamedComponents.setScopeToName(&m_rootScopeImports.names());
759
760 return result;
761}
762
763void LinterVisitor::checkUnusedImports()
764{
765 auto unusedImports = m_importLocations;
766 for (const QString &type : std::as_const(m_usedTypes)) {
767 const auto &importLocations = m_importTypeLocationMap.values(type);
768 for (const auto &importLocation : importLocations)
769 unusedImports.remove(importLocation);
770
771 // If there are no more unused imports left we can abort early
772 if (unusedImports.isEmpty())
773 break;
774 }
775
776 const auto &imports = m_importStaticModuleLocationMap.values();
777 for (const QQmlJS::SourceLocation &import : imports)
778 unusedImports.remove(import);
779
780 for (const auto &import : unusedImports) {
781 m_logger->log(QString::fromLatin1("Unused import"), qmlUnusedImports, import);
782 }
783}
784
785void LinterVisitor::endVisit(UiProgram *ast)
786{
787 QQmlJSImportVisitor::endVisit(ast);
788 checkFileSelections();
789 checkUnusedImports();
790}
791
792static constexpr QLatin1String s_method = "method"_L1;
793static constexpr QLatin1String s_signal = "signal"_L1;
794static constexpr QLatin1String s_property = "property"_L1;
795
798
799static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
800 const QQmlJS::SourceLocation &location,
801 QQmlJSLogger *logger)
802{
803 Q_ASSERT(base);
804 if (!base->hasMethod(name))
805 return;
806
807 static constexpr QLatin1String warningMessage =
808 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
809 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
810 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
811 logger->log(warningMessage.arg(isSignal ? "Signal"_L1 : "Method"_L1, name,
812 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
813 qmlShadow, location);
814}
815
816static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
817 const QQmlJS::SourceLocation &location,
818 OverrideInformations overrideFlags, QQmlJSLogger *logger)
819{
820 Q_ASSERT(base);
821 const bool hasOverride = overrideFlags.testFlag(WithOverride);
822 if (!base->hasProperty(name)) {
823 if (!hasOverride)
824 return;
825 logger->log(
826 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
827 name),
828 qmlPropertyOverride, location);
829 return;
830 }
831
832 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
833 const auto shadowedProperty = owner->ownProperty(name);
834 if (shadowedProperty.isFinal()) {
835 logger->log(
836 (!hasOverride
837 ? "Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
838 : "Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
839 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
840 qmlPropertyOverride, location);
841 return;
842 }
843
844 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
845 if (hasOverride || overrideFlags.testFlag(WithFinal))
846 return;
847
848 logger->log(
849 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
850 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
851 qmlPropertyOverride, location);
852 return;
853 }
854
855 if (hasOverride) {
856 logger->log(
857 "Member \"%1\" overrides a non-virtual member from base type \"%2\", use a different name or mark the property as virtual in the base type."_L1
858 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
859 qmlPropertyOverride, location);
860 return;
861 }
862 logger->log("Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
863 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
864 qmlPropertyOverride, location);
865}
866
867static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name,
868 QLatin1String type, const QQmlJS::SourceLocation &location,
869 OverrideInformations overrideFlags, QQmlJSLogger *logger)
870{
871 static constexpr QLatin1String duplicateMessage =
872 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
873 if (const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
874 logger->log(duplicateMessage.arg(type, name,
875 methods.front().methodType() == QQmlSA::MethodType::Signal
876 ? s_signal
877 : s_method),
878 qmlDuplicatedName, location);
879 }
880 if (scope->hasOwnProperty(name))
881 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
882
883 const QQmlJSScope::ConstPtr base = scope->baseType();
884 if (!base)
885 return;
886
887 warnForMethodShadowingInBase(base, name, location, logger);
888 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
889}
890
891void LinterVisitor::handleRenamedType(UiQualifiedId *qualifiedId)
892{
893 m_renamedComponents.handleRenamedType(
894 m_rootScopeImports.type(qualifiedId->name.toString()).scope, qualifiedId->name,
895 qualifiedId->identifierToken, m_logger);
896}
897
898bool LinterVisitor::visit(Type *type)
899{
900 const bool result = QQmlJSImportVisitor::visit(type);
901
902 handleRenamedType(type->typeId);
903
904 return result;
905}
906
907void LinterVisitor::handleRecursivelyInstantiatedType(UiQualifiedId *qualifiedId)
908{
909 // It should be ok to reference inline components or enums inside of the current file
910 if (qualifiedId->next)
911 return;
912
913 auto logWarning = [&qualifiedId, this]() {
914 m_logger->log("Type \"%1\" can't be instantiated recursively"_L1.arg(qualifiedId->name),
915 qmlTypeInstantiatedRecursively, qualifiedId->identifierToken);
916 };
917
918 const QString name = qualifiedId->name.toString();
919 if (m_rootScopeImports.names().contains(m_exportedRootScope, name))
920 logWarning();
921
922 // note: inline components can't be renamed via qmldir entries
923 if (const auto inlineComponentName = std::get_if<InlineComponentNameType>(&m_currentRootName);
924 inlineComponentName && name == *inlineComponentName) {
925 logWarning();
926 }
927}
928
929bool LinterVisitor::visit(QQmlJS::AST::UiPragma *pragma)
930{
931 const bool result = QQmlJSImportVisitor::visit(pragma);
932
933 // The QML Engine ignores this pragma, so __don't__ set m_exportedRootScope's singleton flag with its value.
934 if (pragma->name == u"Singleton")
935 m_rootIsSingleton = true;
936
937 return result;
938}
939
940void LinterVisitor::checkSingletonRoot()
941{
942 const bool hasQmldirSingletonEntry = m_exportedRootScope->isSingleton(); // set by importer
943 const bool hasSingletonPragma = m_rootIsSingleton;
944
945 if (hasQmldirSingletonEntry == hasSingletonPragma)
946 return;
947
948 if (hasQmldirSingletonEntry && !hasSingletonPragma) {
949 m_logger->log("Type %1 declared as singleton in qmldir but missing pragma Singleton"_L1.arg(
950 m_exportedRootScope->internalName()),
951 qmlImport, QQmlJS::SourceLocation());
952 return;
953 }
954 Q_ASSERT(!hasQmldirSingletonEntry && hasSingletonPragma);
955 m_logger->log("Type %1 not declared as singleton in qmldir but using pragma Singleton"_L1.arg(
956 m_exportedRootScope->internalName()),
957 qmlImport, QQmlJS::SourceLocation());
958}
959
960bool LinterVisitor::visit(QQmlJS::AST::UiObjectDefinition *objectDefinition)
961{
962 handleRenamedType(objectDefinition->qualifiedTypeNameId);
963 handleRecursivelyInstantiatedType(objectDefinition->qualifiedTypeNameId);
964 if (!rootScopeIsValid() && !objectDefinition->qualifiedTypeNameId->name.front().isLower())
965 checkSingletonRoot();
966
967 return QQmlJSImportVisitor::visit(objectDefinition);
968}
969
970bool LinterVisitor::visit(UiPublicMember *publicMember)
971{
972 switch (publicMember->type) {
973 case UiPublicMember::Signal: {
974 const QString signalName = publicMember->name.toString();
975 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
976 WithoutOverride, m_logger);
977 break;
978 }
979 case QQmlJS::AST::UiPublicMember::Property: {
980 const QString propertyName = publicMember->name.toString();
981 OverrideInformations flags;
982 flags.setFlag(WithOverride, publicMember->isOverride());
983 flags.setFlag(WithFinal, publicMember->isFinal());
984 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
985 flags, m_logger);
986 handleRenamedType(publicMember->memberType);
987
988 const QString typeName = publicMember->memberType->toString();
989 if (typeName != "alias"_L1) {
990 if (m_rootScopeImports.hasType(typeName)
991 && !m_rootScopeImports.type(typeName).scope.isNull()) {
992 if (m_importTypeLocationMap.contains(typeName))
993 m_usedTypes.insert(typeName);
994 }
995 }
996 break;
997 }
998 }
999 return QQmlJSImportVisitor::visit(publicMember);
1000}
1001
1002bool LinterVisitor::visit(FunctionExpression *fexpr)
1003{
1004 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
1005 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
1006 WithoutOverride, m_logger);
1007 }
1008 return QQmlJSImportVisitor::visit(fexpr);
1009}
1010
1011bool LinterVisitor::visit(FunctionDeclaration *fdecl)
1012{
1013 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
1014 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
1015 WithoutOverride, m_logger);
1016 }
1017 return QQmlJSImportVisitor::visit(fdecl);
1018}
1019
1020/* This is a _rough_ heuristic; only meant for qmllint to avoid warnings about common constructs.
1021 We might want to improve it in the future if it causes issues
1022*/
1023static bool compatibilityHeuristicForFileSelector(const QQmlJSScope::ConstPtr &scope1,
1024 const QQmlJSScope::ConstPtr &scope2)
1025{
1026 for (const auto &[propertyName, prop] : scope1->properties().asKeyValueRange())
1027 if (!scope2->hasProperty(propertyName))
1028 return false;
1029 for (const auto &[methodName, method] : scope1->methods().asKeyValueRange())
1030 if (!scope2->hasMethod(methodName))
1031 return false;
1032 return true;
1033}
1034
1035// heuristic to check file selected files for "compability" to the unselected file.
1036void LinterVisitor::checkFileSelections()
1037{
1038 const QQmlJS::FileSelectorInfo info =
1039 m_rootScopeImports.contextualTypes().fileSelectorInfoFor(m_exportedRootScope);
1040
1041 if (info.fileSelectedTypes.isEmpty() || info.mainType.isNull())
1042 return;
1043
1044 const QString name = m_rootScopeImports.name(m_exportedRootScope);
1045
1046 if (info.mainType == m_exportedRootScope) {
1047 // current has fileselectors -> check all fileselectors for compatiblity
1048 for (const auto &fileSelected : info.fileSelectedTypes) {
1049 if (compatibilityHeuristicForFileSelector(m_exportedRootScope,
1050 fileSelected.type.scope)) {
1051 m_logger->log(
1052 "Type %1 is ambiguous due to file selector usage, ignoring %2."_L1.arg(
1053 name, fileSelected.type.scope->filePath()),
1054 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1055 continue;
1056 }
1057 m_logger->log("Type %1 has a potentially incompatible file-selected variant %2."_L1.arg(
1058 name, fileSelected.type.scope->filePath()),
1059 qmlImport, m_exportedRootScope->sourceLocation());
1060 }
1061 return;
1062 }
1063
1064 // current is fileselected -> only check against "main" type for compatibility
1065 if (compatibilityHeuristicForFileSelector(info.mainType, m_exportedRootScope)) {
1066 m_logger->log(
1067 "File-selected type %1 is ambiguous due to file selector usage, this file will be ignored in favour of %2."_L1
1068 .arg(name, info.mainType->filePath()),
1069 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1070 return;
1071 }
1072 m_logger->log("File-selected type %1 is potentially incompatible with %2."_L1.arg(
1073 name, info.mainType->filePath()),
1074 qmlImport, m_exportedRootScope->sourceLocation());
1075}
1076
1077void LinterVisitor::addImportWithLocation(const QString &name, const QQmlJS::SourceLocation &loc,
1078 bool hadWarnings)
1079{
1080 if (m_importTypeLocationMap.contains(name)
1081 && m_importTypeLocationMap.values(name).contains(loc)) {
1082 return;
1083 }
1084
1085 m_importTypeLocationMap.insert(name, loc);
1086
1087 // If the import had warnings it may be "unused" because we haven't found all of its types.
1088 // If the type's location is not valid it's a builtin.
1089 // We don't need to complain about those being unused.
1090 if (!hadWarnings && loc.isValid())
1091 m_importLocations.insert(loc);
1092}
1093
1094void LinterVisitor::addStaticImportWithLocation(const QString &name,
1095 const QQmlJS::SourceLocation &loc,
1096 bool isDependency)
1097{
1098 // Always prefer a direct import of static module to it being imported as a dependency
1099 if (isDependency && m_importStaticModuleLocationMap.contains(name))
1100 return;
1101 m_importStaticModuleLocationMap[name] = loc;
1102}
1103
1104bool LinterVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp)
1105{
1106 const QString name = idexp->name.toString();
1107 if (m_importTypeLocationMap.contains(name)) {
1108 m_usedTypes.insert(name);
1109 }
1110
1111 return true;
1112}
1113
1114void LinterVisitor::endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember)
1115{
1116 // This is a rather rough approximation of "used type" but the "unused import"
1117 // info message doesn't have to be 100% accurate.
1118 const QString name = fieldMember->name.toString();
1119 if (m_importTypeLocationMap.contains(name)) {
1120 const QQmlJSImportedScope type = m_rootScopeImports.type(name);
1121 if (type.scope.isNull()) {
1122 if (m_rootScopeImports.hasType(name))
1123 m_usedTypes.insert(name);
1124 } else if (!type.scope->attachedTypeName().isEmpty()) {
1125 m_usedTypes.insert(name);
1126 }
1127 }
1128}
1129
1130} // namespace QQmlJS
1131
1132QT_END_NAMESPACE
void postVisit(QQmlJS::AST::Node *) override
void addStaticImportWithLocation(const QString &name, const QQmlJS::SourceLocation &loc, bool isDependency) override
void handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding, const AST::UiPublicMember *associatedPropertyDefinition) override
LinterVisitor(QQmlJSImporter *importer, QQmlJSLogger *logger, const QString &implicitImportDirectory, const QStringList &qmldirFiles=QStringList(), QQmlJS::Engine *engine=nullptr)
BindingExpressionParseResult parseBindingExpression(const QString &name, const QQmlJS::AST::Statement *statement, const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition=nullptr) override
void addImportWithLocation(const QString &name, const QQmlJS::SourceLocation &loc, bool hadWarnings) override
bool safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier) override
bool visit(QQmlJS::AST::StringLiteral *) override
QQmlJS::AST::Node * astParentOfVisitedNode() const
void endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember) override
bool preVisit(QQmlJS::AST::Node *) override
static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name, const QQmlJS::SourceLocation &location, QQmlJSLogger *logger)
static bool isUselessExpressionStatement_impl(const ExpressionNode *ast)
static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name, QLatin1String type, const QQmlJS::SourceLocation &location, OverrideInformations overrideFlags, QQmlJSLogger *logger)
static constexpr QLatin1String s_method
static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger)
static SourceLocation confusingPluses(BinaryExpression *exp)
static bool isUselessExpressionStatement(const ExpressionStatement *ast)
static bool allCodePathsReturnInsideCase(Node *statement)
Q_DECLARE_FLAGS(OverrideInformations, OverrideInformation)
static bool compatibilityHeuristicForFileSelector(const QQmlJSScope::ConstPtr &scope1, const QQmlJSScope::ConstPtr &scope2)
static SourceLocation confusingMinuses(BinaryExpression *exp)
static QList< const Statement * > possibleLastStatements(const StatementList *ast)
static constexpr QLatin1String s_property
static constexpr QLatin1String s_signal
static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name, const QQmlJS::SourceLocation &location, OverrideInformations overrideFlags, QQmlJSLogger *logger)
Combined button and popup list for selecting options.