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 QQmlJSFixSuggestion suggestion = { "Use a template literal instead."_L1, sl->literalToken,
87 u"`" % templateString % u"`" };
88 suggestion.setAutoApplicable();
89 m_logger->log(QStringLiteral("String contains unescaped line terminator which is "
90 "deprecated."),
91 qmlMultilineStrings, sl->literalToken, true, true, suggestion);
92 }
93 return true;
94}
95
96bool LinterVisitor::preVisit(Node *n)
97{
98 m_ancestryIncludingCurrentNode.push_back(n);
99 return true;
100}
101
102void LinterVisitor::postVisit(Node *n)
103{
104 Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n);
105 m_ancestryIncludingCurrentNode.pop_back();
106}
107
108Node *LinterVisitor::astParentOfVisitedNode() const
109{
110 if (m_ancestryIncludingCurrentNode.size() < 2)
111 return nullptr;
112 return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2];
113}
114
115bool LinterVisitor::visit(CommaExpression *expression)
116{
117 QQmlJSImportVisitor::visit(expression);
118 if (!expression->left || !expression->right)
119 return true;
120
121 // don't warn about commas in "for" statements
122 if (cast<ForStatement *>(astParentOfVisitedNode()))
123 return true;
124
125 m_logger->log("Do not use comma expressions."_L1, qmlComma, expression->commaToken);
126 return true;
127}
128
129static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger)
130{
131 static constexpr std::array literals{ "Boolean"_L1, "Function"_L1, "JSON"_L1,
132 "Math"_L1, "Number"_L1, "String"_L1 };
133
134 const IdentifierExpression *identifier = cast<IdentifierExpression *>(expression->base);
135 if (!identifier)
136 return;
137
138 if (std::find(literals.cbegin(), literals.cend(), identifier->name) != literals.cend()) {
139 logger->log("Do not use '%1' as a constructor."_L1.arg(identifier->name),
140 qmlLiteralConstructor, identifier->identifierToken);
141 }
142 if (identifier->name == "Array"_L1 && expression->arguments && expression->arguments->next) {
143 const auto fullRange = combine(expression->newToken, expression->rparenToken);
144 const auto parensRange = combine(expression->lparenToken, expression->rparenToken);
145 const auto parens = QStringView(logger->code()).mid(parensRange.offset, parensRange.length);
146 const auto insideParens = parens.mid(1, parens.length() - 2);
147 const QString newCode = u'[' + insideParens + u']';
148 QQmlJSFixSuggestion fix("Replace with array literal"_L1, fullRange, newCode);
149 fix.setAutoApplicable(true);
150 logger->log("Array has confusing semantics, use an array literal ([]) instead."_L1,
151 qmlLiteralConstructor, identifier->identifierToken, true, true, fix);
152 }
153}
154
155bool LinterVisitor::visit(NewMemberExpression *expression)
156{
157 QQmlJSImportVisitor::visit(expression);
158 warnAboutLiteralConstructors(expression, m_logger);
159 return true;
160}
161
162bool LinterVisitor::visit(VoidExpression *ast)
163{
164 QQmlJSImportVisitor::visit(ast);
165 m_logger->log("Do not use void expressions."_L1, qmlVoid, ast->voidToken);
166 return true;
167}
168
169static SourceLocation confusingPluses(BinaryExpression *exp)
170{
171 Q_ASSERT(exp->op == QSOperator::Add);
172
173 SourceLocation location = exp->operatorToken;
174
175 // a++ + b
176 if (auto increment = cast<PostIncrementExpression *>(exp->left))
177 location = combine(increment->incrementToken, location);
178 // a + +b
179 if (auto unary = cast<UnaryPlusExpression *>(exp->right))
180 location = combine(location, unary->plusToken);
181 // a + ++b
182 if (auto increment = cast<PreIncrementExpression *>(exp->right))
183 location = combine(location, increment->incrementToken);
184
185 if (location == exp->operatorToken)
186 return SourceLocation{};
187
188 return location;
189}
190
191static SourceLocation confusingMinuses(BinaryExpression *exp)
192{
193 Q_ASSERT(exp->op == QSOperator::Sub);
194
195 SourceLocation location = exp->operatorToken;
196
197 // a-- - b
198 if (auto decrement = cast<PostDecrementExpression *>(exp->left))
199 location = combine(decrement->decrementToken, location);
200 // a - -b
201 if (auto unary = cast<UnaryMinusExpression *>(exp->right))
202 location = combine(location, unary->minusToken);
203 // a - --b
204 if (auto decrement = cast<PreDecrementExpression *>(exp->right))
205 location = combine(location, decrement->decrementToken);
206
207 if (location == exp->operatorToken)
208 return SourceLocation{};
209
210 return location;
211}
212
213bool LinterVisitor::visit(BinaryExpression *exp)
214{
215 QQmlJSImportVisitor::visit(exp);
216 switch (exp->op) {
217 case QSOperator::Add:
218 if (SourceLocation loc = confusingPluses(exp); loc.isValid())
219 m_logger->log("Confusing pluses."_L1, qmlConfusingPluses, loc);
220 break;
221 case QSOperator::Sub:
222 if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
223 m_logger->log("Confusing minuses."_L1, qmlConfusingMinuses, loc);
224 break;
225 default:
226 break;
227 }
228
229 return true;
230}
231
232bool LinterVisitor::visit(QQmlJS::AST::UiImport *import)
233{
234 QQmlJSImportVisitor::visit(import);
235
236 const auto locAndName = [](const UiImport *i) {
237 if (!i->importUri)
238 return std::make_pair(i->fileNameToken, i->fileName.toString());
239
240 QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
241 if (i->importIdToken.isValid())
242 l = combine(l, i->importIdToken);
243 else if (i->version)
244 l = combine(l, i->version->minorToken);
245 else
246 l = combine(l, i->importUri->lastSourceLocation());
247
248 return std::make_pair(l, i->importUri->toString());
249 };
250
251 SeenImport i(import);
252 if (const auto it = m_seenImports.constFind(i); it != m_seenImports.constEnd()) {
253 const auto locAndNameImport = locAndName(import);
254 const auto locAndNameSeen = locAndName(it->uiImport);
255 m_logger->log("Duplicate import '%1'"_L1.arg(locAndNameImport.second),
256 qmlDuplicateImport, locAndNameImport.first);
257 m_logger->log("Note: previous import '%1' here"_L1.arg(locAndNameSeen.second),
258 qmlDuplicateImport, locAndNameSeen.first, true, true, {},
259 locAndName(import).first.startLine);
260 }
261
262 m_seenImports.insert(i);
263 return true;
264}
265
266void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
267 const QQmlJS::SourceLocation &location)
268{
269 m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
270 location);
271 for (const auto *member = members; member; member = member->next) {
272 if (member->member.toString() == key) {
273 m_logger->log(u"Note: previous declaration of '%1' here"_s.arg(key),
274 qmlDuplicateEnumEntries, member->memberToken);
275 return;
276 }
277 }
278}
279
280bool LinterVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied)
281{
282 QQmlJSImportVisitor::visit(uied);
283
284 if (m_currentScope->isInlineComponent()) {
285 m_logger->log(u"Enums declared inside of inline component are ignored."_s,
286 qmlInlineComponentEnums, uied->firstSourceLocation());
287 } else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No
288 && !m_currentScope->isFileRootComponent()) {
289 m_logger->log(u"Enum declared outside the root element. It won't be accessible."_s,
290 qmlNonRootEnums, uied->firstSourceLocation());
291 }
292
293 QHash<QStringView, const QQmlJS::AST::UiEnumMemberList *> seen;
294 for (const auto *member = uied->members; member; member = member->next) {
295 QStringView key = member->member;
296 if (!key.front().isUpper()) {
297 m_logger->log(u"Enum keys should start with an uppercase."_s, qmlEnumKeyCase,
298 member->memberToken);
299 }
300
301 if (seen.contains(key))
302 handleDuplicateEnums(uied->members, key, member->memberToken);
303 else
304 seen[member->member] = member;
305
306 if (uied->name == key) {
307 m_logger->log("Enum entry should be named differently than the enum itself to avoid "
308 "confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
309 }
310 }
311
312 return true;
313}
314
315static bool allCodePathsReturnInsideCase(Node *statement)
316{
317 using namespace AST;
318 if (!statement)
319 return false;
320
321 switch (statement->kind) {
322 case Node::Kind_Block: {
323 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
324 }
325 case Node::Kind_BreakStatement:
326 return true;
327 case Node::Kind_CaseBlock: {
328 const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
329 if (caseBlock->defaultClause)
330 return allCodePathsReturnInsideCase(caseBlock->defaultClause);
331 return allCodePathsReturnInsideCase(caseBlock->clauses);
332 }
333 case Node::Kind_CaseClause:
334 return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
335 case Node::Kind_CaseClauses: {
336 for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
337 caseClauses = caseClauses->next) {
338 if (!allCodePathsReturnInsideCase(caseClauses->clause))
339 return false;
340 }
341 return true;
342 }
343 case Node::Kind_ContinueStatement:
344 // allCodePathsReturn() doesn't recurse into loops, so any encountered `continue` should
345 // belong to a loop outside the switch statement.
346 return true;
347 case Node::Kind_DefaultClause:
348 return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
349 case Node::Kind_IfStatement: {
350 const auto *ifStatement = cast<IfStatement *>(statement);
351 return allCodePathsReturnInsideCase(ifStatement->ok)
352 && allCodePathsReturnInsideCase(ifStatement->ko);
353 }
354 case Node::Kind_LabelledStatement:
355 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
356 case Node::Kind_ReturnStatement:
357 return true;
358 case Node::Kind_StatementList: {
359 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
360 if (allCodePathsReturnInsideCase(list->statement))
361 return true;
362 }
363 return false;
364 }
365 case Node::Kind_SwitchStatement:
366 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
367 case Node::Kind_ThrowStatement:
368 return true;
369 case Node::Kind_TryStatement: {
370 auto *tryStatement = cast<TryStatement *>(statement);
371 if (allCodePathsReturnInsideCase(tryStatement->statement))
372 return true;
373 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
374 }
375 case Node::Kind_WithStatement:
376 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
377 default:
378 break;
379 }
380 return false;
381}
382
383void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
384 SourceLocation nextLoc)
385{
386 if (!statements || !nextLoc.isValid())
387 return;
388
389 if (allCodePathsReturnInsideCase(statements))
390 return;
391
392 quint32 afterLastStatement = 0;
393 for (StatementList *it = statements; it; it = it->next) {
394 if (!it->next) {
395 afterLastStatement = it->statement->lastSourceLocation().end();
396 }
397 }
398
399 const auto &comments = m_engine->comments();
400 auto it = std::find_if(comments.cbegin(), comments.cend(),
401 [&](auto c) { return afterLastStatement < c.offset; });
402 auto end = std::find_if(it, comments.cend(),
403 [&](auto c) { return c.offset >= nextLoc.offset; });
404
405 for (; it != end; ++it) {
406 const QString &commentText = m_engine->code().mid(it->offset, it->length);
407 if (commentText.contains("fall through"_L1)
408 || commentText.contains("fall-through"_L1)
409 || commentText.contains("fallthrough"_L1)) {
410 return;
411 }
412 }
413
414 m_logger->log(
415 "Non-empty case block potentially falls through to the next case or default statement. "
416 "Add \"// fallthrough\" at the end of the block to silence this warning."_L1,
417 qmlUnterminatedCase, errorLoc);
418}
419
420bool LinterVisitor::visit(QQmlJS::AST::CaseBlock *block)
421{
422 QQmlJSImportVisitor::visit(block);
423
424 std::vector<std::pair<SourceLocation, StatementList *>> clauses;
425 for (CaseClauses *it = block->clauses; it; it = it->next)
426 clauses.push_back({ it->clause->caseToken, it->clause->statements });
427 if (block->defaultClause)
428 clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
429 for (CaseClauses *it = block->moreClauses; it; it = it->next)
430 clauses.push_back({ it->clause->caseToken, it->clause->statements });
431
432 // check all but the last clause for fallthrough
433 for (size_t i = 0; i < clauses.size() - 1; ++i) {
434 const SourceLocation nextToken = clauses[i + 1].first;
435 checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
436 }
437 return true;
438}
439
440static QList<const Statement *> possibleLastStatements(const StatementList *ast);
441static QList<const Statement *> possibleLastStatements(const CaseBlock *ast)
442{
443 QList<const Statement *> lasts;
444
445 for (const auto *clause = ast->clauses; clause; clause = clause->next)
446 lasts << possibleLastStatements(clause->clause->statements);
447 if (ast->defaultClause)
448 lasts << possibleLastStatements(ast->defaultClause->statements);
449 for (const auto *clause = ast->moreClauses; clause; clause = clause->next)
450 lasts << possibleLastStatements(clause->clause->statements);
451
452 return lasts;
453}
454
455static QList<const Statement *> possibleLastStatements(const Statement *ast)
456{
457 if (const auto *s = cast<const Block *>(ast))
458 return possibleLastStatements(s->statements) << s;
459 if (const auto *s = cast<const BreakStatement *>(ast))
460 return { s };
461 if (const auto *s = cast<const ContinueStatement *>(ast))
462 return { s };
463 if (const auto *s = cast<const DebuggerStatement *>(ast))
464 return { s };
465 if (const auto *s = cast<const DoWhileStatement *>(ast))
466 return possibleLastStatements(s->statement) << s;
467 if (const auto *s = cast<const EmptyStatement *>(ast))
468 return { s };
469 if (const auto *s = cast<const ExportDeclaration *>(ast))
470 return { s };
471 if (const auto *s = cast<const ExpressionStatement *>(ast))
472 return { s };
473 if (const auto *s = cast<const ForEachStatement *>(ast))
474 return possibleLastStatements(s->statement) << s;
475 if (const auto *s = cast<const ForStatement *>(ast))
476 return possibleLastStatements(s->statement) << s;
477 if (const auto *s = cast<const IfStatement *>(ast)) {
478 auto lasts = possibleLastStatements(s->ok);
479 if (s->ko)
480 lasts << possibleLastStatements(s->ko);
481 return lasts << s;
482 }
483 if (const auto *s = cast<const ImportDeclaration *>(ast))
484 return { s };
485 if (const auto *s = cast<const LabelledStatement *>(ast))
486 return possibleLastStatements(s->statement) << ast;
487 if (const auto *s = cast<const ReturnStatement *>(ast))
488 return { s };
489 if (const auto *s = cast<const SwitchStatement *>(ast))
490 return possibleLastStatements(s->block) << s;
491 if (const auto *s = cast<const ThrowStatement *>(ast))
492 return { s };
493 if (const auto *s = cast<const TryStatement *>(ast))
494 return { s };
495 if (const auto *s = cast<const VariableStatement *>(ast))
496 return { s };
497 if (const auto *s = cast<const WhileStatement *>(ast))
498 return possibleLastStatements(s->statement) << s;
499 if (const auto *s = cast<const WithStatement *>(ast))
500 return possibleLastStatements(s->statement) << s;
501
502 Q_UNREACHABLE_RETURN({});
503}
504
505static QList<const Statement *> possibleLastStatements(const StatementList *ast)
506{
507 Q_ASSERT(ast);
508 for (; ast->next; ast = ast->next) { }
509 const auto *statement = ast->statement;
510
511 // Can't store FunctionDeclaration as a statement. See StatementList.
512 if (cast<const FunctionDeclaration *>(statement))
513 return {};
514
515 return possibleLastStatements(static_cast<const Statement *>(statement));
516}
517
518static bool isUselessExpressionStatement_impl(const ExpressionNode *ast)
519{
520 switch (ast->kind) {
521 case Node::Kind_CallExpression:
522 case Node::Kind_DeleteExpression:
523 case Node::Kind_NewExpression:
524 case Node::Kind_PreDecrementExpression:
525 case Node::Kind_PreIncrementExpression:
526 case Node::Kind_PostDecrementExpression:
527 case Node::Kind_PostIncrementExpression:
528 case Node::Kind_YieldExpression:
529 case Node::Kind_FunctionExpression:
530 return false;
531 case Node::Kind_NumericLiteral:
532 case Node::Kind_StringLiteral:
533 case Node::Kind_FalseLiteral:
534 case Node::Kind_TrueLiteral:
535 case Node::Kind_NullExpression:
536 case Node::Kind_Undefined:
537 case Node::Kind_RegExpLiteral:
538 case Node::Kind_SuperLiteral:
539 case Node::Kind_ThisExpression:
540 case Node::Kind_FieldMemberExpression:
541 case Node::Kind_IdentifierExpression:
542 case Node::Kind_TypeOfExpression:
543 return true;
544 default:
545 break;
546 }
547
548 if (const auto *e = cast<const NestedExpression *>(ast))
549 return isUselessExpressionStatement_impl(e->expression);
550 if (const auto *e = cast<const NotExpression *>(ast))
551 return isUselessExpressionStatement_impl(e->expression);
552 if (const auto *e = cast<const TildeExpression *>(ast))
553 return isUselessExpressionStatement_impl(e->expression);
554 if (const auto *e = cast<const UnaryMinusExpression *>(ast))
555 return isUselessExpressionStatement_impl(e->expression);
556 if (const auto *e = cast<const UnaryPlusExpression *>(ast))
557 return isUselessExpressionStatement_impl(e->expression);
558 if (const auto *e = cast<const ConditionalExpression *>(ast))
559 return isUselessExpressionStatement_impl(e->ok) && isUselessExpressionStatement_impl(e->ko);
560
561 if (const BinaryExpression *binary = cast<const BinaryExpression *>(ast)) {
562 switch (binary->op) {
563 case QSOperator::InplaceAnd:
564 case QSOperator::Assign:
565 case QSOperator::InplaceSub:
566 case QSOperator::InplaceDiv:
567 case QSOperator::InplaceExp:
568 case QSOperator::InplaceAdd:
569 case QSOperator::InplaceLeftShift:
570 case QSOperator::InplaceMod:
571 case QSOperator::InplaceMul:
572 case QSOperator::InplaceOr:
573 case QSOperator::InplaceRightShift:
574 case QSOperator::InplaceURightShift:
575 case QSOperator::InplaceXor:
576 return false;
577 default:
578 return isUselessExpressionStatement_impl(binary->left)
579 && isUselessExpressionStatement_impl(binary->right);
580 }
581 }
582
583 return false;
584}
585
586/*!
587\internal
588
589This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
590*/
591static bool isUselessExpressionStatement(const ExpressionStatement *ast)
592{
593 return isUselessExpressionStatement_impl(ast->expression);
594}
595
596void LinterVisitor::handleUselessExpressionStatement(const ExpressionStatement *ast)
597{
598 // property binding, signal handler, or function declaration
599 const auto it = std::find_if(m_ancestryIncludingCurrentNode.crbegin(),
600 m_ancestryIncludingCurrentNode.crend(),
601 [](auto it) {
602 return it->kind == Node::Kind_UiPublicMember
603 || it->kind == Node::Kind_FunctionDeclaration
604 || it->kind == Node::Kind_UiScriptBinding;
605 });
606
607 if (it == m_ancestryIncludingCurrentNode.crend())
608 return;
609
610 // A useless ExpressionStatement in *last position* inside a property binding is not useless
611 const auto isLastExprStat = [](const ExpressionStatement *statement, const Statement *base) {
612 const auto lasts = possibleLastStatements(base);
613 return lasts.contains(statement);
614 };
615
616 if (const auto *usb = cast<UiScriptBinding *>(*it); usb && usb->qualifiedId) {
617 if (usb->qualifiedId->toString() == "id"_L1)
618 return;
619 if (usb->qualifiedId->next)
620 return; // group/attached property, give up
621 if (m_savedBindingOuterScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope)
622 return; // group property, give up
623
624 QQmlJSScope::Ptr object = m_currentScope;
625 while (object && object->scopeType() != QQmlSA::ScopeType::QMLScope)
626 object = object->parentScope();
627
628 if (!object)
629 return;
630
631 if (m_propertyBindings.contains(object)) {
632 for (const auto &entry : m_propertyBindings[object]) {
633 if (entry.data == usb->qualifiedId->toString()) {
634 if (isLastExprStat(ast, usb->statement))
635 return;
636 else
637 break;
638 }
639 }
640 }
641 }
642
643 const auto *upm = cast<const UiPublicMember *>(*it);
644 if (upm && upm->type == AST::UiPublicMember::Property && upm->statement) {
645 if (isLastExprStat(ast, upm->statement))
646 return;
647 }
648
649 if (isUselessExpressionStatement(ast)) {
650 m_logger->log("Expression statement has no obvious effect."_L1,
651 qmlConfusingExpressionStatement,
652 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
653 }
654}
655
656bool LinterVisitor::visit(ExpressionStatement *ast)
657{
658 QQmlJSImportVisitor::visit(ast);
659 handleUselessExpressionStatement(ast);
660 return true;
661}
662
663bool LinterVisitor::safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier)
664{
665 if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
666 identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
667 // var is generally not great, but we don't want to emit this warning if you
668 // are in the single, toplevel block of a binding
669 Q_ASSERT(!scope->parentScope().isNull()); // lexical scope should always have a parent
670 auto parentScopeType = scope->parentScope()->scopeType();
671 bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
672 || parentScopeType == QQmlSA::ScopeType::SignalHandlerFunctionScope;
673 if (!inTopLevelBindingBlockScope) {
674 m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
675 u"Replace it with const or let to silence the warning\n"_s,
676 qmlBlockScopeVarDeclaration, identifier.location);
677 }
678 } else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
679 const QQmlJSScope *scopePtr = scope.get();
680 std::pair<const QQmlJSScope*, QString> misplaced { scopePtr, name };
681 if (misplacedJSIdentifiers.contains(misplaced))
682 return false; // we only want to warn once
683 misplacedJSIdentifiers.insert(misplaced);
684 m_logger->log(u"JavaScript declarations are not allowed in QML elements"_s, qmlSyntax,
685 identifier.location);
686 return false;
687 }
688 return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
689}
690
692 const QString &name, const QQmlJS::AST::Statement *statement,
693 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
694{
695 if (statement && statement->kind == (int)AST::Node::Kind::Kind_Block) {
696 const auto *block = static_cast<const AST::Block *>(statement);
697 if (!block->statements && associatedPropertyDefinition) {
698 m_logger->log("Unintentional empty block, use ({}) for empty object literal"_L1,
699 qmlUnintentionalEmptyBlock,
700 combine(block->lbraceToken, block->rbraceToken));
701 }
702 }
703
704 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
705}
706
707void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding,
708 const UiPublicMember *associatedPropertyDefinition)
709{
710 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
711 return;
712
713 if (!associatedPropertyDefinition->isReadonly())
714 return;
715
716 const auto &prop = m_currentScope->property(binding.propertyName());
717 const auto log = [&](const QString &preferredType) {
718 m_logger->log("Prefer more specific type %1 over var"_L1.arg(preferredType),
719 qmlPreferNonVarProperties, prop.sourceLocation());
720 };
721
722 if (prop.typeName() != "QVariant"_L1)
723 return;
724
725 switch (binding.bindingType()) {
726 case QQmlSA::BindingType::BoolLiteral: {
727 log("bool"_L1);
728 break;
729 }
730 case QQmlSA::BindingType::NumberLiteral: {
731 double v = binding.numberValue();
732 auto loc = binding.sourceLocation();
733 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
734 if (literal.contains(u'.') || double(int(v)) != v)
735 log("real or double"_L1);
736 else
737 log("int"_L1);
738 break;
739 }
740 case QQmlSA::BindingType::StringLiteral: {
741 log("string"_L1);
742 break;
743 }
744 default: {
745 break;
746 }
747 }
748}
749
750static constexpr QLatin1String s_method = "method"_L1;
751static constexpr QLatin1String s_signal = "signal"_L1;
752static constexpr QLatin1String s_property = "property"_L1;
753
756
757static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
758 const QQmlJS::SourceLocation &location,
759 QQmlJSLogger *logger)
760{
761 Q_ASSERT(base);
762 if (!base->hasMethod(name))
763 return;
764
765 static constexpr QLatin1String warningMessage =
766 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
767 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
768 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
769 logger->log(warningMessage.arg(isSignal ? "Signal"_L1 : "Method"_L1, name,
770 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
771 qmlShadow, location);
772}
773
774static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
775 const QQmlJS::SourceLocation &location,
776 OverrideInformations overrideFlags, QQmlJSLogger *logger)
777{
778 Q_ASSERT(base);
779 const bool hasOverride = overrideFlags.testFlag(WithOverride);
780 if (!base->hasProperty(name)) {
781 if (!hasOverride)
782 return;
783 logger->log(
784 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
785 name),
786 qmlPropertyOverride, location);
787 return;
788 }
789
790 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
791 const auto shadowedProperty = owner->ownProperty(name);
792 if (shadowedProperty.isFinal()) {
793 logger->log(
794 (!hasOverride
795 ? "Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
796 : "Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
797 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
798 qmlPropertyOverride, location);
799 return;
800 }
801
802 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
803 if (hasOverride || overrideFlags.testFlag(WithFinal))
804 return;
805
806 logger->log(
807 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
808 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
809 qmlPropertyOverride, location);
810 return;
811 }
812
813 if (hasOverride) {
814 logger->log(
815 "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
816 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
817 qmlPropertyOverride, location);
818 return;
819 }
820 logger->log("Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
821 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
822 qmlPropertyOverride, location);
823}
824
825static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name,
826 QLatin1String type, const QQmlJS::SourceLocation &location,
827 OverrideInformations overrideFlags, QQmlJSLogger *logger)
828{
829 static constexpr QLatin1String duplicateMessage =
830 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
831 if (const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
832 logger->log(duplicateMessage.arg(type, name,
833 methods.front().methodType() == QQmlSA::MethodType::Signal
834 ? s_signal
835 : s_method),
836 qmlDuplicatedName, location);
837 }
838 if (scope->hasOwnProperty(name))
839 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
840
841 const QQmlJSScope::ConstPtr base = scope->baseType();
842 if (!base)
843 return;
844
845 warnForMethodShadowingInBase(base, name, location, logger);
846 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
847}
848
849bool LinterVisitor::visit(UiPublicMember *publicMember)
850{
851 switch (publicMember->type) {
852 case UiPublicMember::Signal: {
853 const QString signalName = publicMember->name.toString();
854 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
855 WithoutOverride, m_logger);
856 break;
857 }
858 case QQmlJS::AST::UiPublicMember::Property: {
859 const QString propertyName = publicMember->name.toString();
860 OverrideInformations flags;
861 flags.setFlag(WithOverride, publicMember->isOverride());
862 flags.setFlag(WithFinal, publicMember->isFinal());
863 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
864 flags, m_logger);
865 break;
866 }
867 }
868 return QQmlJSImportVisitor::visit(publicMember);
869}
870
871bool LinterVisitor::visit(FunctionExpression *fexpr)
872{
873 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
874 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
875 WithoutOverride, m_logger);
876 }
877 return QQmlJSImportVisitor::visit(fexpr);
878}
879
880bool LinterVisitor::visit(FunctionDeclaration *fdecl)
881{
882 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
883 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
884 WithoutOverride, m_logger);
885 }
886 return QQmlJSImportVisitor::visit(fdecl);
887}
888
889} // namespace QQmlJS
890
891QT_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
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 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.