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 Q_ASSERT(ast);
511 for (; ast->next; ast = ast->next) { }
512 const auto *statement = ast->statement;
513
514 // Can't store FunctionDeclaration as a statement. See StatementList.
515 if (cast<const FunctionDeclaration *>(statement))
516 return {};
517
518 return possibleLastStatements(static_cast<const Statement *>(statement));
519}
520
521static bool isUselessExpressionStatement_impl(const ExpressionNode *ast)
522{
523 switch (ast->kind) {
524 case Node::Kind_CallExpression:
525 case Node::Kind_DeleteExpression:
526 case Node::Kind_NewExpression:
527 case Node::Kind_PreDecrementExpression:
528 case Node::Kind_PreIncrementExpression:
529 case Node::Kind_PostDecrementExpression:
530 case Node::Kind_PostIncrementExpression:
531 case Node::Kind_YieldExpression:
532 case Node::Kind_FunctionExpression:
533 return false;
534 case Node::Kind_NumericLiteral:
535 case Node::Kind_StringLiteral:
536 case Node::Kind_FalseLiteral:
537 case Node::Kind_TrueLiteral:
538 case Node::Kind_NullExpression:
539 case Node::Kind_Undefined:
540 case Node::Kind_RegExpLiteral:
541 case Node::Kind_SuperLiteral:
542 case Node::Kind_ThisExpression:
543 case Node::Kind_FieldMemberExpression:
544 case Node::Kind_IdentifierExpression:
545 case Node::Kind_TypeOfExpression:
546 return true;
547 default:
548 break;
549 }
550
551 if (const auto *e = cast<const NestedExpression *>(ast))
552 return isUselessExpressionStatement_impl(e->expression);
553 if (const auto *e = cast<const NotExpression *>(ast))
554 return isUselessExpressionStatement_impl(e->expression);
555 if (const auto *e = cast<const TildeExpression *>(ast))
556 return isUselessExpressionStatement_impl(e->expression);
557 if (const auto *e = cast<const UnaryMinusExpression *>(ast))
558 return isUselessExpressionStatement_impl(e->expression);
559 if (const auto *e = cast<const UnaryPlusExpression *>(ast))
560 return isUselessExpressionStatement_impl(e->expression);
561 if (const auto *e = cast<const ConditionalExpression *>(ast))
562 return isUselessExpressionStatement_impl(e->ok) && isUselessExpressionStatement_impl(e->ko);
563
564 if (const BinaryExpression *binary = cast<const BinaryExpression *>(ast)) {
565 switch (binary->op) {
566 case QSOperator::InplaceAnd:
567 case QSOperator::Assign:
568 case QSOperator::InplaceSub:
569 case QSOperator::InplaceDiv:
570 case QSOperator::InplaceExp:
571 case QSOperator::InplaceAdd:
572 case QSOperator::InplaceLeftShift:
573 case QSOperator::InplaceMod:
574 case QSOperator::InplaceMul:
575 case QSOperator::InplaceOr:
576 case QSOperator::InplaceRightShift:
577 case QSOperator::InplaceURightShift:
578 case QSOperator::InplaceXor:
579 return false;
580 default:
581 return isUselessExpressionStatement_impl(binary->left)
582 && isUselessExpressionStatement_impl(binary->right);
583 }
584 }
585
586 return false;
587}
588
589/*!
590\internal
591
592This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
593*/
594static bool isUselessExpressionStatement(const ExpressionStatement *ast)
595{
596 return isUselessExpressionStatement_impl(ast->expression);
597}
598
599void LinterVisitor::handleUselessExpressionStatement(const ExpressionStatement *ast)
600{
601 // property binding, signal handler, or function declaration
602 const auto it = std::find_if(m_ancestryIncludingCurrentNode.crbegin(),
603 m_ancestryIncludingCurrentNode.crend(),
604 [](auto it) {
605 return it->kind == Node::Kind_UiPublicMember
606 || it->kind == Node::Kind_FunctionDeclaration
607 || it->kind == Node::Kind_UiScriptBinding;
608 });
609
610 if (it == m_ancestryIncludingCurrentNode.crend())
611 return;
612
613 // A useless ExpressionStatement in *last position* inside a property binding is not useless
614 const auto isLastExprStat = [](const ExpressionStatement *statement, const Statement *base) {
615 const auto lasts = possibleLastStatements(base);
616 return lasts.contains(statement);
617 };
618
619 if (const auto *usb = cast<UiScriptBinding *>(*it); usb && usb->qualifiedId) {
620 if (usb->qualifiedId->toString() == "id"_L1)
621 return;
622 if (usb->qualifiedId->next)
623 return; // group/attached property, give up
624 if (m_savedBindingOuterScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope)
625 return; // group property, give up
626
627 QQmlJSScope::Ptr object = m_currentScope;
628 while (object && object->scopeType() != QQmlSA::ScopeType::QMLScope)
629 object = object->parentScope();
630
631 if (!object)
632 return;
633
634 if (m_propertyBindings.contains(object)) {
635 for (const auto &entry : m_propertyBindings[object]) {
636 if (entry.data == usb->qualifiedId->toString()) {
637 if (isLastExprStat(ast, usb->statement))
638 return;
639 else
640 break;
641 }
642 }
643 }
644 }
645
646 const auto *upm = cast<const UiPublicMember *>(*it);
647 if (upm && upm->type == AST::UiPublicMember::Property && upm->statement) {
648 if (isLastExprStat(ast, upm->statement))
649 return;
650 }
651
652 if (isUselessExpressionStatement(ast)) {
653 m_logger->log("Expression statement has no obvious effect."_L1,
654 qmlConfusingExpressionStatement,
655 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
656 }
657}
658
659bool LinterVisitor::visit(ExpressionStatement *ast)
660{
661 QQmlJSImportVisitor::visit(ast);
662 handleUselessExpressionStatement(ast);
663 return true;
664}
665
666bool LinterVisitor::safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier)
667{
668 if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
669 identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
670 // var is generally not great, but we don't want to emit this warning if you
671 // are in the single, toplevel block of a binding
672 Q_ASSERT(!scope->parentScope().isNull()); // lexical scope should always have a parent
673 auto parentScopeType = scope->parentScope()->scopeType();
674 bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
675 || parentScopeType == QQmlSA::ScopeType::SignalHandlerFunctionScope;
676 if (!inTopLevelBindingBlockScope) {
677 m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
678 u"Replace it with const or let to silence the warning\n"_s,
679 qmlBlockScopeVarDeclaration, identifier.location);
680 }
681 } else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
682 const QQmlJSScope *scopePtr = scope.get();
683 std::pair<const QQmlJSScope*, QString> misplaced { scopePtr, name };
684 if (misplacedJSIdentifiers.contains(misplaced))
685 return false; // we only want to warn once
686 misplacedJSIdentifiers.insert(misplaced);
687 m_logger->log(u"JavaScript declarations are not allowed in QML elements"_s, qmlSyntax,
688 identifier.location);
689 return false;
690 }
691 return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
692}
693
695 const QString &name, const QQmlJS::AST::Statement *statement,
696 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
697{
698 if (statement && statement->kind == (int)AST::Node::Kind::Kind_Block) {
699 const auto *block = static_cast<const AST::Block *>(statement);
700 if (!block->statements && associatedPropertyDefinition) {
701 m_logger->log("Unintentional empty block, use ({}) for empty object literal"_L1,
702 qmlUnintentionalEmptyBlock,
703 combine(block->lbraceToken, block->rbraceToken));
704 }
705 }
706
707 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
708}
709
710void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding,
711 const UiPublicMember *associatedPropertyDefinition)
712{
713 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
714 return;
715
716 if (!associatedPropertyDefinition->isReadonly())
717 return;
718
719 const auto &prop = m_currentScope->property(binding.propertyName());
720 const auto log = [&](const QString &preferredType) {
721 m_logger->log("Prefer more specific type %1 over var"_L1.arg(preferredType),
722 qmlPreferNonVarProperties, prop.sourceLocation());
723 };
724
725 if (prop.typeName() != "QVariant"_L1)
726 return;
727
728 switch (binding.bindingType()) {
729 case QQmlSA::BindingType::BoolLiteral: {
730 log("bool"_L1);
731 break;
732 }
733 case QQmlSA::BindingType::NumberLiteral: {
734 double v = binding.numberValue();
735 auto loc = binding.sourceLocation();
736 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
737 if (literal.contains(u'.') || double(int(v)) != v)
738 log("real or double"_L1);
739 else
740 log("int"_L1);
741 break;
742 }
743 case QQmlSA::BindingType::StringLiteral: {
744 log("string"_L1);
745 break;
746 }
747 default: {
748 break;
749 }
750 }
751}
752
753bool LinterVisitor::visit(UiProgram *ast)
754{
755 const bool result = QQmlJSImportVisitor::visit(ast);
756
757 m_renamedComponents.setScopeToName(&m_rootScopeImports.names());
758
759 return result;
760}
761
762void LinterVisitor::endVisit(UiProgram *ast)
763{
764 QQmlJSImportVisitor::endVisit(ast);
765 checkFileSelections();
766}
767
768static constexpr QLatin1String s_method = "method"_L1;
769static constexpr QLatin1String s_signal = "signal"_L1;
770static constexpr QLatin1String s_property = "property"_L1;
771
774
775static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
776 const QQmlJS::SourceLocation &location,
777 QQmlJSLogger *logger)
778{
779 Q_ASSERT(base);
780 if (!base->hasMethod(name))
781 return;
782
783 static constexpr QLatin1String warningMessage =
784 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
785 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
786 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
787 logger->log(warningMessage.arg(isSignal ? "Signal"_L1 : "Method"_L1, name,
788 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
789 qmlShadow, location);
790}
791
792static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
793 const QQmlJS::SourceLocation &location,
794 OverrideInformations overrideFlags, QQmlJSLogger *logger)
795{
796 Q_ASSERT(base);
797 const bool hasOverride = overrideFlags.testFlag(WithOverride);
798 if (!base->hasProperty(name)) {
799 if (!hasOverride)
800 return;
801 logger->log(
802 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
803 name),
804 qmlPropertyOverride, location);
805 return;
806 }
807
808 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
809 const auto shadowedProperty = owner->ownProperty(name);
810 if (shadowedProperty.isFinal()) {
811 logger->log(
812 (!hasOverride
813 ? "Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
814 : "Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
815 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
816 qmlPropertyOverride, location);
817 return;
818 }
819
820 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
821 if (hasOverride || overrideFlags.testFlag(WithFinal))
822 return;
823
824 logger->log(
825 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
826 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
827 qmlPropertyOverride, location);
828 return;
829 }
830
831 if (hasOverride) {
832 logger->log(
833 "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
834 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
835 qmlPropertyOverride, location);
836 return;
837 }
838 logger->log("Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
839 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
840 qmlPropertyOverride, location);
841}
842
843static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name,
844 QLatin1String type, const QQmlJS::SourceLocation &location,
845 OverrideInformations overrideFlags, QQmlJSLogger *logger)
846{
847 static constexpr QLatin1String duplicateMessage =
848 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
849 if (const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
850 logger->log(duplicateMessage.arg(type, name,
851 methods.front().methodType() == QQmlSA::MethodType::Signal
852 ? s_signal
853 : s_method),
854 qmlDuplicatedName, location);
855 }
856 if (scope->hasOwnProperty(name))
857 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
858
859 const QQmlJSScope::ConstPtr base = scope->baseType();
860 if (!base)
861 return;
862
863 warnForMethodShadowingInBase(base, name, location, logger);
864 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
865}
866
867void LinterVisitor::handleRenamedType(UiQualifiedId *qualifiedId)
868{
869 m_renamedComponents.handleRenamedType(
870 m_rootScopeImports.type(qualifiedId->name.toString()).scope, qualifiedId->name,
871 qualifiedId->identifierToken, m_logger);
872}
873
874bool LinterVisitor::visit(Type *type)
875{
876 const bool result = QQmlJSImportVisitor::visit(type);
877
878 handleRenamedType(type->typeId);
879
880 return result;
881}
882
883void LinterVisitor::handleRecursivelyInstantiatedType(UiQualifiedId *qualifiedId)
884{
885 // It should be ok to reference inline components or enums inside of the current file
886 if (qualifiedId->next)
887 return;
888
889 auto logWarning = [&qualifiedId, this]() {
890 m_logger->log("Type \"%1\" can't be instantiated recursively"_L1.arg(qualifiedId->name),
891 qmlTypeInstantiatedRecursively, qualifiedId->identifierToken);
892 };
893
894 const QString name = qualifiedId->name.toString();
895 if (m_rootScopeImports.names().contains(m_exportedRootScope, name))
896 logWarning();
897
898 // note: inline components can't be renamed via qmldir entries
899 if (const auto inlineComponentName = std::get_if<InlineComponentNameType>(&m_currentRootName);
900 inlineComponentName && name == *inlineComponentName) {
901 logWarning();
902 }
903}
904
905bool LinterVisitor::visit(QQmlJS::AST::UiPragma *pragma)
906{
907 const bool result = QQmlJSImportVisitor::visit(pragma);
908
909 // The QML Engine ignores this pragma, so __don't__ set m_exportedRootScope's singleton flag with its value.
910 if (pragma->name == u"Singleton")
911 m_rootIsSingleton = true;
912
913 return result;
914}
915
916void LinterVisitor::checkSingletonRoot()
917{
918 const bool hasQmldirSingletonEntry = m_exportedRootScope->isSingleton(); // set by importer
919 const bool hasSingletonPragma = m_rootIsSingleton;
920
921 if (hasQmldirSingletonEntry == hasSingletonPragma)
922 return;
923
924 if (hasQmldirSingletonEntry && !hasSingletonPragma) {
925 m_logger->log("Type %1 declared as singleton in qmldir but missing pragma Singleton"_L1.arg(
926 m_exportedRootScope->internalName()),
927 qmlImport, QQmlJS::SourceLocation());
928 return;
929 }
930 Q_ASSERT(!hasQmldirSingletonEntry && hasSingletonPragma);
931 m_logger->log("Type %1 not declared as singleton in qmldir but using pragma Singleton"_L1.arg(
932 m_exportedRootScope->internalName()),
933 qmlImport, QQmlJS::SourceLocation());
934}
935
936bool LinterVisitor::visit(QQmlJS::AST::UiObjectDefinition *objectDefinition)
937{
938 handleRenamedType(objectDefinition->qualifiedTypeNameId);
939 handleRecursivelyInstantiatedType(objectDefinition->qualifiedTypeNameId);
940 if (!rootScopeIsValid() && !objectDefinition->qualifiedTypeNameId->name.front().isLower())
941 checkSingletonRoot();
942
943 return QQmlJSImportVisitor::visit(objectDefinition);
944}
945
946bool LinterVisitor::visit(UiPublicMember *publicMember)
947{
948 switch (publicMember->type) {
949 case UiPublicMember::Signal: {
950 const QString signalName = publicMember->name.toString();
951 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
952 WithoutOverride, m_logger);
953 break;
954 }
955 case QQmlJS::AST::UiPublicMember::Property: {
956 const QString propertyName = publicMember->name.toString();
957 OverrideInformations flags;
958 flags.setFlag(WithOverride, publicMember->isOverride());
959 flags.setFlag(WithFinal, publicMember->isFinal());
960 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
961 flags, m_logger);
962 handleRenamedType(publicMember->memberType);
963 break;
964 }
965 }
966 return QQmlJSImportVisitor::visit(publicMember);
967}
968
969bool LinterVisitor::visit(FunctionExpression *fexpr)
970{
971 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
972 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
973 WithoutOverride, m_logger);
974 }
975 return QQmlJSImportVisitor::visit(fexpr);
976}
977
978bool LinterVisitor::visit(FunctionDeclaration *fdecl)
979{
980 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
981 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
982 WithoutOverride, m_logger);
983 }
984 return QQmlJSImportVisitor::visit(fdecl);
985}
986
987/* This is a _rough_ heuristic; only meant for qmllint to avoid warnings about common constructs.
988 We might want to improve it in the future if it causes issues
989*/
990static bool compatibilityHeuristicForFileSelector(const QQmlJSScope::ConstPtr &scope1,
991 const QQmlJSScope::ConstPtr &scope2)
992{
993 for (const auto &[propertyName, prop] : scope1->properties().asKeyValueRange())
994 if (!scope2->hasProperty(propertyName))
995 return false;
996 for (const auto &[methodName, method] : scope1->methods().asKeyValueRange())
997 if (!scope2->hasMethod(methodName))
998 return false;
999 return true;
1000}
1001
1002// heuristic to check file selected files for "compability" to the unselected file.
1003void LinterVisitor::checkFileSelections()
1004{
1005 const QQmlJS::FileSelectorInfo info =
1006 m_rootScopeImports.contextualTypes().fileSelectorInfoFor(m_exportedRootScope);
1007
1008 if (info.fileSelectedTypes.isEmpty() || info.mainType.isNull())
1009 return;
1010
1011 const QString name = m_rootScopeImports.name(m_exportedRootScope);
1012
1013 if (info.mainType == m_exportedRootScope) {
1014 // current has fileselectors -> check all fileselectors for compatiblity
1015 for (const auto &fileSelected : info.fileSelectedTypes) {
1016 if (compatibilityHeuristicForFileSelector(m_exportedRootScope,
1017 fileSelected.type.scope)) {
1018 m_logger->log(
1019 "Type %1 is ambiguous due to file selector usage, ignoring %2."_L1.arg(
1020 name, fileSelected.type.scope->filePath()),
1021 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1022 continue;
1023 }
1024 m_logger->log("Type %1 has a potentially incompatible file-selected variant %2."_L1.arg(
1025 name, fileSelected.type.scope->filePath()),
1026 qmlImport, m_exportedRootScope->sourceLocation());
1027 }
1028 return;
1029 }
1030
1031 // current is fileselected -> only check against "main" type for compatibility
1032 if (compatibilityHeuristicForFileSelector(info.mainType, m_exportedRootScope)) {
1033 m_logger->log(
1034 "File-selected type %1 is ambiguous due to file selector usage, this file will be ignored in favour of %2."_L1
1035 .arg(name, info.mainType->filePath()),
1036 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1037 return;
1038 }
1039 m_logger->log("File-selected type %1 is potentially incompatible with %2."_L1.arg(
1040 name, info.mainType->filePath()),
1041 qmlImport, m_exportedRootScope->sourceLocation());
1042}
1043
1044} // namespace QQmlJS
1045
1046QT_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.