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
7#include <stack>
8
9QT_BEGIN_NAMESPACE
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)
27 : QQmlJSImportVisitor(importer, logger, implicitImportDirectory, qmldirFiles)
28 , m_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
130{
131 static constexpr std::array literals{ "Boolean"_L1, "Function"_L1, "JSON"_L1,
132 "Math"_L1, "Number"_L1, "String"_L1 };
133
135 if (!identifier)
136 return;
137
139 logger->log("Do not use '%1' as a constructor."_L1.arg(identifier->name),
141 }
142}
143
144bool LinterVisitor::visit(NewMemberExpression *expression)
145{
146 QQmlJSImportVisitor::visit(expression);
147 warnAboutLiteralConstructors(expression, m_logger);
148 return true;
149}
150
151bool LinterVisitor::visit(VoidExpression *ast)
152{
153 QQmlJSImportVisitor::visit(ast);
154 m_logger->log("Do not use void expressions."_L1, qmlVoid, ast->voidToken);
155 return true;
156}
157
158static SourceLocation confusingPluses(BinaryExpression *exp)
159{
160 Q_ASSERT(exp->op == QSOperator::Add);
161
162 SourceLocation location = exp->operatorToken;
163
164 // a++ + b
165 if (auto increment = cast<PostIncrementExpression *>(exp->left))
166 location = combine(increment->incrementToken, location);
167 // a + +b
168 if (auto unary = cast<UnaryPlusExpression *>(exp->right))
169 location = combine(location, unary->plusToken);
170 // a + ++b
171 if (auto increment = cast<PreIncrementExpression *>(exp->right))
172 location = combine(location, increment->incrementToken);
173
174 if (location == exp->operatorToken)
175 return SourceLocation{};
176
177 return location;
178}
179
180static SourceLocation confusingMinuses(BinaryExpression *exp)
181{
182 Q_ASSERT(exp->op == QSOperator::Sub);
183
184 SourceLocation location = exp->operatorToken;
185
186 // a-- - b
187 if (auto decrement = cast<PostDecrementExpression *>(exp->left))
188 location = combine(decrement->decrementToken, location);
189 // a - -b
190 if (auto unary = cast<UnaryMinusExpression *>(exp->right))
191 location = combine(location, unary->minusToken);
192 // a - --b
193 if (auto decrement = cast<PreDecrementExpression *>(exp->right))
194 location = combine(location, decrement->decrementToken);
195
196 if (location == exp->operatorToken)
197 return SourceLocation{};
198
199 return location;
200}
201
202bool LinterVisitor::visit(BinaryExpression *exp)
203{
204 QQmlJSImportVisitor::visit(exp);
205 switch (exp->op) {
206 case QSOperator::Add:
207 if (SourceLocation loc = confusingPluses(exp); loc.isValid())
208 m_logger->log("Confusing pluses."_L1, qmlConfusingPluses, loc);
209 break;
210 case QSOperator::Sub:
211 if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
212 m_logger->log("Confusing minuses."_L1, qmlConfusingMinuses, loc);
213 break;
214 default:
215 break;
216 }
217
218 return true;
219}
220
221bool LinterVisitor::visit(QQmlJS::AST::UiImport *import)
222{
223 QQmlJSImportVisitor::visit(import);
224
225 const auto locAndName = [](const UiImport *i) {
226 if (!i->importUri)
227 return std::make_pair(i->fileNameToken, i->fileName.toString());
228
229 QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
230 if (i->importIdToken.isValid())
231 l = combine(l, i->importIdToken);
232 else if (i->version)
233 l = combine(l, i->version->minorToken);
234 else
235 l = combine(l, i->importUri->lastSourceLocation());
236
237 return std::make_pair(l, i->importUri->toString());
238 };
239
240 SeenImport i(import);
241 if (const auto it = m_seenImports.constFind(i); it != m_seenImports.constEnd()) {
242 const auto locAndNameImport = locAndName(import);
243 const auto locAndNameSeen = locAndName(it->uiImport);
244 m_logger->log("Duplicate import '%1'"_L1.arg(locAndNameImport.second),
245 qmlDuplicateImport, locAndNameImport.first);
246 m_logger->log("Note: previous import '%1' here"_L1.arg(locAndNameSeen.second),
247 qmlDuplicateImport, locAndNameSeen.first, true, true, {},
248 locAndName(import).first.startLine);
249 }
250
251 m_seenImports.insert(i);
252 return true;
253}
254
255void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
256 const QQmlJS::SourceLocation &location)
257{
258 m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
259 location);
260 for (const auto *member = members; member; member = member->next) {
261 if (member->member.toString() == key) {
262 m_logger->log(u"Note: previous declaration of '%1' here"_s.arg(key),
263 qmlDuplicateEnumEntries, member->memberToken);
264 return;
265 }
266 }
267}
268
269bool LinterVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied)
270{
271 QQmlJSImportVisitor::visit(uied);
272
273 if (m_currentScope->isInlineComponent()) {
274 m_logger->log(u"Enums declared inside of inline component are ignored."_s,
275 qmlInlineComponentEnums, uied->firstSourceLocation());
276 } else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No
277 && !m_currentScope->isFileRootComponent()) {
278 m_logger->log(u"Enum declared outside the root element. It won't be accessible."_s,
279 qmlNonRootEnums, uied->firstSourceLocation());
280 }
281
282 QHash<QStringView, const QQmlJS::AST::UiEnumMemberList *> seen;
283 for (const auto *member = uied->members; member; member = member->next) {
284 QStringView key = member->member;
285 if (!key.front().isUpper()) {
286 m_logger->log(u"Enum keys should start with an uppercase."_s, qmlEnumKeyCase,
287 member->memberToken);
288 }
289
290 if (seen.contains(key))
291 handleDuplicateEnums(uied->members, key, member->memberToken);
292 else
293 seen[member->member] = member;
294
295 if (uied->name == key) {
296 m_logger->log("Enum entry should be named differently than the enum itself to avoid "
297 "confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
298 }
299 }
300
301 return true;
302}
303
304static bool allCodePathsReturnInsideCase(Node *statement)
305{
306 using namespace AST;
307 if (!statement)
308 return false;
309
310 switch (statement->kind) {
311 case Node::Kind_Block: {
312 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
313 }
314 case Node::Kind_BreakStatement:
315 return true;
316 case Node::Kind_CaseBlock: {
317 const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
318 if (caseBlock->defaultClause)
319 return allCodePathsReturnInsideCase(caseBlock->defaultClause);
320 return allCodePathsReturnInsideCase(caseBlock->clauses);
321 }
322 case Node::Kind_CaseClause:
323 return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
324 case Node::Kind_CaseClauses: {
325 for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
326 caseClauses = caseClauses->next) {
327 if (!allCodePathsReturnInsideCase(caseClauses->clause))
328 return false;
329 }
330 return true;
331 }
332 case Node::Kind_ContinueStatement:
333 // allCodePathsReturn() doesn't recurse into loops, so any encountered `continue` should
334 // belong to a loop outside the switch statement.
335 return true;
336 case Node::Kind_DefaultClause:
337 return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
338 case Node::Kind_IfStatement: {
339 const auto *ifStatement = cast<IfStatement *>(statement);
340 return allCodePathsReturnInsideCase(ifStatement->ok)
341 && allCodePathsReturnInsideCase(ifStatement->ko);
342 }
343 case Node::Kind_LabelledStatement:
344 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
345 case Node::Kind_ReturnStatement:
346 return true;
347 case Node::Kind_StatementList: {
348 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
349 if (allCodePathsReturnInsideCase(list->statement))
350 return true;
351 }
352 return false;
353 }
354 case Node::Kind_SwitchStatement:
355 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
356 case Node::Kind_ThrowStatement:
357 return true;
358 case Node::Kind_TryStatement: {
359 auto *tryStatement = cast<TryStatement *>(statement);
360 if (allCodePathsReturnInsideCase(tryStatement->statement))
361 return true;
362 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
363 }
364 case Node::Kind_WithStatement:
365 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
366 default:
367 break;
368 }
369 return false;
370}
371
372void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
373 SourceLocation nextLoc)
374{
375 if (!statements || !nextLoc.isValid())
376 return;
377
378 if (allCodePathsReturnInsideCase(statements))
379 return;
380
381 quint32 afterLastStatement = 0;
382 for (StatementList *it = statements; it; it = it->next) {
383 if (!it->next) {
384 afterLastStatement = it->statement->lastSourceLocation().end();
385 }
386 }
387
388 const auto &comments = m_engine->comments();
389 auto it = std::find_if(comments.cbegin(), comments.cend(),
390 [&](auto c) { return afterLastStatement < c.offset; });
391 auto end = std::find_if(it, comments.cend(),
392 [&](auto c) { return c.offset >= nextLoc.offset; });
393
394 for (; it != end; ++it) {
395 const QString &commentText = m_engine->code().mid(it->offset, it->length);
396 if (commentText.contains("fall through"_L1)
397 || commentText.contains("fall-through"_L1)
398 || commentText.contains("fallthrough"_L1)) {
399 return;
400 }
401 }
402
403 m_logger->log(
404 "Non-empty case block potentially falls through to the next case or default statement. "
405 "Add \"// fallthrough\" at the end of the block to silence this warning."_L1,
406 qmlUnterminatedCase, errorLoc);
407}
408
409bool LinterVisitor::visit(QQmlJS::AST::CaseBlock *block)
410{
411 QQmlJSImportVisitor::visit(block);
412
413 std::vector<std::pair<SourceLocation, StatementList *>> clauses;
414 for (CaseClauses *it = block->clauses; it; it = it->next)
415 clauses.push_back({ it->clause->caseToken, it->clause->statements });
416 if (block->defaultClause)
417 clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
418 for (CaseClauses *it = block->moreClauses; it; it = it->next)
419 clauses.push_back({ it->clause->caseToken, it->clause->statements });
420
421 // check all but the last clause for fallthrough
422 for (size_t i = 0; i < clauses.size() - 1; ++i) {
423 const SourceLocation nextToken = clauses[i + 1].first;
424 checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
425 }
426 return true;
427}
428
429/*!
430\internal
431
432This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
433*/
434static bool isUselessExpressionStatement(ExpressionNode *ast)
435{
436 switch (ast->kind) {
437 case Node::Kind_CallExpression:
438 case Node::Kind_DeleteExpression:
439 case Node::Kind_NewExpression:
440 case Node::Kind_PreDecrementExpression:
441 case Node::Kind_PreIncrementExpression:
442 case Node::Kind_PostDecrementExpression:
443 case Node::Kind_PostIncrementExpression:
444 case Node::Kind_YieldExpression:
445 case Node::Kind_FunctionExpression:
446 return false;
447 default:
448 break;
449 };
450 BinaryExpression *binary = cast<BinaryExpression *>(ast);
451 if (!binary)
452 return false;
453
454 switch (binary->op) {
455 case QSOperator::InplaceAnd:
456 case QSOperator::Assign:
457 case QSOperator::InplaceSub:
458 case QSOperator::InplaceDiv:
459 case QSOperator::InplaceExp:
460 case QSOperator::InplaceAdd:
461 case QSOperator::InplaceLeftShift:
462 case QSOperator::InplaceMod:
463 case QSOperator::InplaceMul:
464 case QSOperator::InplaceOr:
465 case QSOperator::InplaceRightShift:
466 case QSOperator::InplaceURightShift:
467 case QSOperator::InplaceXor:
468 return false;
469 default:
470 return true;
471 }
472 Q_UNREACHABLE_RETURN(true);
473}
474
475static bool canHaveUselessExpressionStatement(Node *parent)
476{
477 return parent->kind != Node::Kind_UiScriptBinding && parent->kind != Node::Kind_UiPublicMember;
478}
479
480bool LinterVisitor::visit(ExpressionStatement *ast)
481{
482 QQmlJSImportVisitor::visit(ast);
483
484 if (canHaveUselessExpressionStatement(astParentOfVisitedNode())
485 && isUselessExpressionStatement(ast->expression)) {
486 m_logger->log("Expression statement has no obvious effect."_L1,
487 qmlConfusingExpressionStatement,
488 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
489 }
490
491 return true;
492}
493
494bool LinterVisitor::safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier)
495{
496 if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
497 identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
498 // var is generally not great, but we don't want to emit this warning if you
499 // are in the single, toplevel block of a binding
500 Q_ASSERT(!scope->parentScope().isNull()); // lexical scope should always have a parent
501 auto parentScopeType = scope->parentScope()->scopeType();
502 bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
504 if (!inTopLevelBindingBlockScope) {
505 m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
506 u"Replace it with const or let to silence the warning\n"_s,
507 qmlBlockScopeVarDeclaration, identifier.location);
508 }
509 } else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
510 const QQmlJSScope *scopePtr = scope.get();
511 std::pair<const QQmlJSScope*, QString> misplaced { scopePtr, name };
512 if (misplacedJSIdentifiers.contains(misplaced))
513 return false; // we only want to warn once
514 misplacedJSIdentifiers.insert(misplaced);
515 m_logger->log(u"JavaScript declarations are not allowed in QML elements"_s, qmlSyntax,
516 identifier.location);
517 return false;
518 }
519 return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
520}
521
523 const QString &name, const QQmlJS::AST::Statement *statement,
524 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
525{
526 if (statement && statement->kind == (int)AST::Node::Kind::Kind_Block) {
527 const auto *block = static_cast<const AST::Block *>(statement);
528 if (!block->statements && associatedPropertyDefinition) {
529 m_logger->log("Unintentional empty block, use ({}) for empty object literal"_L1,
530 qmlUnintentionalEmptyBlock,
531 combine(block->lbraceToken, block->rbraceToken));
532 }
533 }
534
535 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
536}
537
538void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding,
539 const UiPublicMember *associatedPropertyDefinition)
540{
541 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
542 return;
543
544 if (!associatedPropertyDefinition->isReadonly())
545 return;
546
547 const auto &prop = m_currentScope->property(binding.propertyName());
548 const auto log = [&](const QString &preferredType) {
549 m_logger->log("Prefer more specific type %1 over var"_L1.arg(preferredType),
550 qmlPreferNonVarProperties, prop.sourceLocation());
551 };
552
553 if (prop.typeName() != "QVariant"_L1)
554 return;
555
556 switch (binding.bindingType()) {
558 log("bool"_L1);
559 break;
560 }
562 double v = binding.numberValue();
563 auto loc = binding.sourceLocation();
564 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
565 if (literal.contains(u'.') || double(int(v)) != v)
566 log("real or double"_L1);
567 else
568 log("int"_L1);
569 break;
570 }
572 log("string"_L1);
573 break;
574 }
575 default: {
576 break;
577 }
578 }
579}
580
581void LinterVisitor::endVisit(UiProgram *ast)
582{
583 QQmlJSImportVisitor::endVisit(ast);
584 checkIdShadows();
585}
586
587static constexpr QLatin1String s_method = "method"_L1;
588static constexpr QLatin1String s_signal = "signal"_L1;
589static constexpr QLatin1String s_property = "property"_L1;
590
593
594static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
595 const QQmlJS::SourceLocation &location,
596 QQmlJSLogger *logger)
597{
598 Q_ASSERT(base);
599 if (!base->hasMethod(name))
600 return;
601
602 static constexpr QLatin1String warningMessage =
603 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
604 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
605 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
606 logger->log(warningMessage.arg(isSignal ? "Signal"_L1 : "Method"_L1, name,
607 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
608 qmlShadow, location);
609}
610
611static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
612 const QQmlJS::SourceLocation &location,
613 OverrideInformations overrideFlags, QQmlJSLogger *logger)
614{
615 Q_ASSERT(base);
616 const bool hasOverride = overrideFlags.testFlag(WithOverride);
617 if (!base->hasProperty(name)) {
618 if (!hasOverride)
619 return;
620 logger->log(
621 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
622 name),
623 qmlPropertyOverride, location);
624 return;
625 }
626
627 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
628 const auto shadowedProperty = owner->ownProperty(name);
629 if (shadowedProperty.isFinal()) {
630 logger->log(
631 (!hasOverride
632 ? "Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
633 : "Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
634 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
635 qmlPropertyOverride, location);
636 return;
637 }
638
639 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
640 if (hasOverride || overrideFlags.testFlag(WithFinal))
641 return;
642
643 logger->log(
644 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
645 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
646 qmlPropertyOverride, location);
647 return;
648 }
649
650 if (hasOverride) {
651 logger->log(
652 "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
653 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
654 qmlPropertyOverride, location);
655 return;
656 }
657 logger->log("Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
658 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
659 qmlPropertyOverride, location);
660}
661
662static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name,
663 QLatin1String type, const QQmlJS::SourceLocation &location,
664 OverrideInformations overrideFlags, QQmlJSLogger *logger)
665{
666 static constexpr QLatin1String duplicateMessage =
667 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
668 if (const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
669 logger->log(duplicateMessage.arg(type, name,
670 methods.front().methodType() == QQmlSA::MethodType::Signal
671 ? s_signal
672 : s_method),
673 qmlDuplicatedName, location);
674 }
675 if (scope->hasOwnProperty(name))
676 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
677
678 const QQmlJSScope::ConstPtr base = scope->baseType();
679 if (!base)
680 return;
681
682 warnForMethodShadowingInBase(base, name, location, logger);
683 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
684}
685
686bool LinterVisitor::visit(UiPublicMember *publicMember)
687{
688 switch (publicMember->type) {
689 case UiPublicMember::Signal: {
690 const QString signalName = publicMember->name.toString();
691 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
692 WithoutOverride, m_logger);
693 break;
694 }
695 case QQmlJS::AST::UiPublicMember::Property: {
696 const QString propertyName = publicMember->name.toString();
697 OverrideInformations flags;
698 flags.setFlag(WithOverride, publicMember->isOverride());
699 flags.setFlag(WithFinal, publicMember->isFinal());
700 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
701 flags, m_logger);
702 break;
703 }
704 }
705 return QQmlJSImportVisitor::visit(publicMember);
706}
707
708bool LinterVisitor::visit(FunctionExpression *fexpr)
709{
710 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
711 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
712 WithoutOverride, m_logger);
713 }
714 return QQmlJSImportVisitor::visit(fexpr);
715}
716
717bool LinterVisitor::visit(FunctionDeclaration *fdecl)
718{
719 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
720 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
721 WithoutOverride, m_logger);
722 }
723 return QQmlJSImportVisitor::visit(fdecl);
724}
725
727void warnForShadowsInCurrentScope(const QQmlJSScope::ConstPtr &scopeWithId, const QString &name,
728 const QQmlJSScope::ConstPtr &currentScope,
729 const QQmlJS::SourceLocation &location, MethodOrProperty mode,
730 QQmlJSLogger *logger)
731{
732 static constexpr QLatin1String warningMessage =
733 "Id \"%1\" shadows %2 \"%1\"%3. Rename the id or the %2."_L1;
734
735 if (mode == Property ? !currentScope->hasProperty(name) : !currentScope->hasMethod(name))
736 return;
737
738 const auto owner = mode == Property ? QQmlJSScope::ownerOfProperty(currentScope, name).scope
739 : QQmlJSScope::ownerOfMethod(currentScope, name).scope;
740 const QString currentScopeName =
741 QQmlJSUtils::getScopeName(currentScope, QQmlSA::ScopeType::QMLScope);
742
743 const QLatin1String memberType = mode == Property
744 ? "property"_L1
745 : (owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal
746 ? "signal"_L1
747 : "method"_L1);
748
749 const QQmlJS::SourceLocation definitionLocation = mode == Property
750 ? currentScope->property(name).sourceLocation()
751 : currentScope->methods(name).front().sourceLocation();
752 auto log = [&](const QString &asdf) {
753 logger->log(warningMessage.arg(name, memberType, asdf), qmlIdShadowsMember, location);
754
755 // add hint if the member clashing with the id is defined in the current file
756 if (owner->filePath() == scopeWithId->filePath()) {
757 logger->log("Note: %1 \"%2\" defined here is shadowed by id \"%2\""_L1.arg(memberType,
758 name),
759 qmlIdShadowsMember, definitionLocation, true, true, {}, location.startLine);
760 } else {
761 logger->log(
762 "Note: type \"%1\" defined here has a %2 \"%3\" shadowed by id \"%3\""_L1.arg(
763 currentScopeName, memberType, name),
764 qmlIdShadowsMember, currentScope->sourceLocation(), true, true, {},
765 location.startLine);
766 }
767 };
768
769 if (currentScope != scopeWithId) {
770 log(" from \"%1\" defined at %2:%3:%4"_L1.arg(
771 currentScopeName, currentScope->filePath(),
772 QString::number(currentScope->sourceLocation().startLine),
773 QString::number(currentScope->sourceLocation().startColumn)));
774 return;
775 }
776 log(" from current type"_L1);
777}
778
779/*!
780\internal
781
782Searches for ids shadowing properties, methods and signals.
783
784An id shadows all properties, methods and signals inside the context the id is defined when
785ComponentBehavior is not set to Bound.
786The id shadows also properties, methods and signals in the child contexts of the context the id
787was defined when ComponentBehavior is set to Bound. Assume here that components are bound for
788clarity.
789
790Compute all possible scopes where an id can shadow properties, methods and signals. All of these
791scopes are inside the component boundary, so represent this set of scopes with the root scope inside
792the component boundary. All descendants of the root scope, that are in the same component
793boundary as the root scope, can have properties, methods and signals shadowed by its id.
794
795Once all roots are computed in "componentRootsToIds", iterate over their descendents to find
796potential clashes of properties and methods with the ids that can be referred from inside that
797component boundary.
798*/
799void LinterVisitor::checkIdShadows()
800{
801 const auto componentRootsToIds = m_scopesById.computeComponentRootsToIds();
802 if (componentRootsToIds.empty())
803 return;
804
805 using It = decltype(componentRootsToIds.begin());
806 auto begin = componentRootsToIds.begin();
807 auto end = componentRootsToIds.end();
808 auto nextKey = [&](It it) -> It {
809 return it == end ? it : componentRootsToIds.upper_bound(it->first);
810 };
811
812 for (auto it = begin, it2 = nextKey(begin); it != end; it = std::exchange(it2, nextKey(it2))) {
813 std::stack<QQmlJSScope::ConstPtr> stack{ { it->first } };
814 while (!stack.empty()) {
815 QQmlJSScope::ConstPtr current = stack.top();
816 stack.pop();
817
818 for (auto scopeWithIdIt = it, scopeWithIdEnd = it2; scopeWithIdIt != scopeWithIdEnd;
819 ++scopeWithIdIt) {
820 warnForShadowsInCurrentScope(scopeWithIdIt->second.scope, scopeWithIdIt->second.id,
821 current,
822 scopeWithIdIt->second.scope->idSourceLocation(),
824 warnForShadowsInCurrentScope(scopeWithIdIt->second.scope, scopeWithIdIt->second.id,
825 current,
826 scopeWithIdIt->second.scope->idSourceLocation(),
827 MethodOrProperty::Method, m_logger);
828 }
829 const auto children = current->childScopes();
830 for (const QQmlJSScope::ConstPtr &child : children)
831 stack.push(child);
832 }
833 }
834}
835
836} // namespace QQmlJS
837
838QT_END_NAMESPACE
void postVisit(QQmlJS::AST::Node *) override
void handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding, const AST::UiPublicMember *associatedPropertyDefinition) override
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(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 allCodePathsReturnInsideCase(Node *statement)
Q_DECLARE_FLAGS(OverrideInformations, OverrideInformation)
static SourceLocation confusingMinuses(BinaryExpression *exp)
void warnForShadowsInCurrentScope(const QQmlJSScope::ConstPtr &scopeWithId, const QString &name, const QQmlJSScope::ConstPtr &currentScope, const QQmlJS::SourceLocation &location, MethodOrProperty mode, QQmlJSLogger *logger)
static constexpr QLatin1String s_property
static constexpr QLatin1String s_signal
static bool canHaveUselessExpressionStatement(Node *parent)
static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name, const QQmlJS::SourceLocation &location, OverrideInformations overrideFlags, QQmlJSLogger *logger)
\inmodule QtQmlCompiler
MethodType
Definition qqmlsa.h:48
QQmlSA::MethodType QQmlJSMetaMethodType