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::endVisit(UiProgram *ast)
764{
765 QQmlJSImportVisitor::endVisit(ast);
766 checkFileSelections();
767}
768
769static constexpr QLatin1String s_method = "method"_L1;
770static constexpr QLatin1String s_signal = "signal"_L1;
771static constexpr QLatin1String s_property = "property"_L1;
772
775
776static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
777 const QQmlJS::SourceLocation &location,
778 QQmlJSLogger *logger)
779{
780 Q_ASSERT(base);
781 if (!base->hasMethod(name))
782 return;
783
784 static constexpr QLatin1String warningMessage =
785 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
786 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
787 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
788 logger->log(warningMessage.arg(isSignal ? "Signal"_L1 : "Method"_L1, name,
789 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
790 qmlShadow, location);
791}
792
793static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
794 const QQmlJS::SourceLocation &location,
795 OverrideInformations overrideFlags, QQmlJSLogger *logger)
796{
797 Q_ASSERT(base);
798 const bool hasOverride = overrideFlags.testFlag(WithOverride);
799 if (!base->hasProperty(name)) {
800 if (!hasOverride)
801 return;
802 logger->log(
803 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
804 name),
805 qmlPropertyOverride, location);
806 return;
807 }
808
809 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
810 const auto shadowedProperty = owner->ownProperty(name);
811 if (shadowedProperty.isFinal()) {
812 logger->log(
813 (!hasOverride
814 ? "Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
815 : "Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
816 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
817 qmlPropertyOverride, location);
818 return;
819 }
820
821 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
822 if (hasOverride || overrideFlags.testFlag(WithFinal))
823 return;
824
825 logger->log(
826 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
827 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
828 qmlPropertyOverride, location);
829 return;
830 }
831
832 if (hasOverride) {
833 logger->log(
834 "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
835 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
836 qmlPropertyOverride, location);
837 return;
838 }
839 logger->log("Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
840 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
841 qmlPropertyOverride, location);
842}
843
844static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name,
845 QLatin1String type, const QQmlJS::SourceLocation &location,
846 OverrideInformations overrideFlags, QQmlJSLogger *logger)
847{
848 static constexpr QLatin1String duplicateMessage =
849 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
850 if (const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
851 logger->log(duplicateMessage.arg(type, name,
852 methods.front().methodType() == QQmlSA::MethodType::Signal
853 ? s_signal
854 : s_method),
855 qmlDuplicatedName, location);
856 }
857 if (scope->hasOwnProperty(name))
858 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
859
860 const QQmlJSScope::ConstPtr base = scope->baseType();
861 if (!base)
862 return;
863
864 warnForMethodShadowingInBase(base, name, location, logger);
865 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
866}
867
868void LinterVisitor::handleRenamedType(UiQualifiedId *qualifiedId)
869{
870 m_renamedComponents.handleRenamedType(
871 m_rootScopeImports.type(qualifiedId->name.toString()).scope, qualifiedId->name,
872 qualifiedId->identifierToken, m_logger);
873}
874
875bool LinterVisitor::visit(Type *type)
876{
877 const bool result = QQmlJSImportVisitor::visit(type);
878
879 handleRenamedType(type->typeId);
880
881 return result;
882}
883
884void LinterVisitor::handleRecursivelyInstantiatedType(UiQualifiedId *qualifiedId)
885{
886 // It should be ok to reference inline components or enums inside of the current file
887 if (qualifiedId->next)
888 return;
889
890 auto logWarning = [&qualifiedId, this]() {
891 m_logger->log("Type \"%1\" can't be instantiated recursively"_L1.arg(qualifiedId->name),
892 qmlTypeInstantiatedRecursively, qualifiedId->identifierToken);
893 };
894
895 const QString name = qualifiedId->name.toString();
896 if (m_rootScopeImports.names().contains(m_exportedRootScope, name))
897 logWarning();
898
899 // note: inline components can't be renamed via qmldir entries
900 if (const auto inlineComponentName = std::get_if<InlineComponentNameType>(&m_currentRootName);
901 inlineComponentName && name == *inlineComponentName) {
902 logWarning();
903 }
904}
905
906bool LinterVisitor::visit(QQmlJS::AST::UiPragma *pragma)
907{
908 const bool result = QQmlJSImportVisitor::visit(pragma);
909
910 // The QML Engine ignores this pragma, so __don't__ set m_exportedRootScope's singleton flag with its value.
911 if (pragma->name == u"Singleton")
912 m_rootIsSingleton = true;
913
914 return result;
915}
916
917void LinterVisitor::checkSingletonRoot()
918{
919 const bool hasQmldirSingletonEntry = m_exportedRootScope->isSingleton(); // set by importer
920 const bool hasSingletonPragma = m_rootIsSingleton;
921
922 if (hasQmldirSingletonEntry == hasSingletonPragma)
923 return;
924
925 if (hasQmldirSingletonEntry && !hasSingletonPragma) {
926 m_logger->log("Type %1 declared as singleton in qmldir but missing pragma Singleton"_L1.arg(
927 m_exportedRootScope->internalName()),
928 qmlImport, QQmlJS::SourceLocation());
929 return;
930 }
931 Q_ASSERT(!hasQmldirSingletonEntry && hasSingletonPragma);
932 m_logger->log("Type %1 not declared as singleton in qmldir but using pragma Singleton"_L1.arg(
933 m_exportedRootScope->internalName()),
934 qmlImport, QQmlJS::SourceLocation());
935}
936
937bool LinterVisitor::visit(QQmlJS::AST::UiObjectDefinition *objectDefinition)
938{
939 handleRenamedType(objectDefinition->qualifiedTypeNameId);
940 handleRecursivelyInstantiatedType(objectDefinition->qualifiedTypeNameId);
941 if (!rootScopeIsValid() && !objectDefinition->qualifiedTypeNameId->name.front().isLower())
942 checkSingletonRoot();
943
944 return QQmlJSImportVisitor::visit(objectDefinition);
945}
946
947bool LinterVisitor::visit(UiPublicMember *publicMember)
948{
949 switch (publicMember->type) {
950 case UiPublicMember::Signal: {
951 const QString signalName = publicMember->name.toString();
952 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
953 WithoutOverride, m_logger);
954 break;
955 }
956 case QQmlJS::AST::UiPublicMember::Property: {
957 const QString propertyName = publicMember->name.toString();
958 OverrideInformations flags;
959 flags.setFlag(WithOverride, publicMember->isOverride());
960 flags.setFlag(WithFinal, publicMember->isFinal());
961 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
962 flags, m_logger);
963 handleRenamedType(publicMember->memberType);
964 break;
965 }
966 }
967 return QQmlJSImportVisitor::visit(publicMember);
968}
969
970bool LinterVisitor::visit(FunctionExpression *fexpr)
971{
972 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
973 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
974 WithoutOverride, m_logger);
975 }
976 return QQmlJSImportVisitor::visit(fexpr);
977}
978
979bool LinterVisitor::visit(FunctionDeclaration *fdecl)
980{
981 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
982 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
983 WithoutOverride, m_logger);
984 }
985 return QQmlJSImportVisitor::visit(fdecl);
986}
987
988/* This is a _rough_ heuristic; only meant for qmllint to avoid warnings about common constructs.
989 We might want to improve it in the future if it causes issues
990*/
991static bool compatibilityHeuristicForFileSelector(const QQmlJSScope::ConstPtr &scope1,
992 const QQmlJSScope::ConstPtr &scope2)
993{
994 for (const auto &[propertyName, prop] : scope1->properties().asKeyValueRange())
995 if (!scope2->hasProperty(propertyName))
996 return false;
997 for (const auto &[methodName, method] : scope1->methods().asKeyValueRange())
998 if (!scope2->hasMethod(methodName))
999 return false;
1000 return true;
1001}
1002
1003// heuristic to check file selected files for "compability" to the unselected file.
1004void LinterVisitor::checkFileSelections()
1005{
1006 const QQmlJS::FileSelectorInfo info =
1007 m_rootScopeImports.contextualTypes().fileSelectorInfoFor(m_exportedRootScope);
1008
1009 if (info.fileSelectedTypes.isEmpty() || info.mainType.isNull())
1010 return;
1011
1012 const QString name = m_rootScopeImports.name(m_exportedRootScope);
1013
1014 if (info.mainType == m_exportedRootScope) {
1015 // current has fileselectors -> check all fileselectors for compatiblity
1016 for (const auto &fileSelected : info.fileSelectedTypes) {
1017 if (compatibilityHeuristicForFileSelector(m_exportedRootScope,
1018 fileSelected.type.scope)) {
1019 m_logger->log(
1020 "Type %1 is ambiguous due to file selector usage, ignoring %2."_L1.arg(
1021 name, fileSelected.type.scope->filePath()),
1022 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1023 continue;
1024 }
1025 m_logger->log("Type %1 has a potentially incompatible file-selected variant %2."_L1.arg(
1026 name, fileSelected.type.scope->filePath()),
1027 qmlImport, m_exportedRootScope->sourceLocation());
1028 }
1029 return;
1030 }
1031
1032 // current is fileselected -> only check against "main" type for compatibility
1033 if (compatibilityHeuristicForFileSelector(info.mainType, m_exportedRootScope)) {
1034 m_logger->log(
1035 "File-selected type %1 is ambiguous due to file selector usage, this file will be ignored in favour of %2."_L1
1036 .arg(name, info.mainType->filePath()),
1037 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1038 return;
1039 }
1040 m_logger->log("File-selected type %1 is potentially incompatible with %2."_L1.arg(
1041 name, info.mainType->filePath()),
1042 qmlImport, m_exportedRootScope->sourceLocation());
1043}
1044
1045} // namespace QQmlJS
1046
1047QT_END_NAMESPACE
void postVisit(QQmlJS::AST::Node *) 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
bool safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier) override
void endVisit(QQmlJS::AST::UiProgram *ast) override
bool visit(QQmlJS::AST::StringLiteral *) override
QQmlJS::AST::Node * astParentOfVisitedNode() const
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.