7#include <private/qqmljsutils_p.h>
9#include <QtQml/private/qqmlsignalnames_p.h>
13using namespace Qt::StringLiterals;
14using namespace QQmlJS::AST;
18
19
20
21
22
23
26 QQmlJSImporter *importer, QQmlJSLogger *logger,
27 const QString &implicitImportDirectory,
const QStringList &qmldirFiles,
28 QQmlJS::Engine *engine)
36 const auto leaveEnv = qScopeGuard([
this] { QQmlJSImportVisitor::leaveEnvironment(); });
38 if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
41 if (
auto base = m_currentScope->baseType()) {
42 if (base->internalName() == u"QQmlComponent"_s) {
43 const auto nChildren = std::count_if(
44 m_currentScope->childScopesBegin(), m_currentScope->childScopesEnd(),
45 [](
const QQmlJSScope::ConstPtr &scope) {
46 return scope->scopeType() == QQmlSA::ScopeType::QMLScope;
49 m_logger->log(
"Components must have exactly one child"_L1,
50 qmlComponentChildrenCount, m_currentScope->sourceLocation());
56bool LinterVisitor::
visit(StringLiteral *sl)
58 QQmlJSImportVisitor::visit(sl);
59 const QString s = m_logger->code().mid(sl->literalToken.begin(), sl->literalToken.length);
61 if (s.contains(QLatin1Char(
'\r')) || s.contains(QLatin1Char(
'\n')) || s.contains(QChar(0x2028u))
62 || s.contains(QChar(0x2029u))) {
63 QString templateString;
66 const QChar stringQuote = s[0];
67 for (qsizetype i = 1; i < s.size() - 1; i++) {
75 templateString.chop(1);
80 templateString += u'\\';
81 if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{')
82 templateString += u'\\';
88 QQmlJSDocumentEdit documentEdit{
89 m_logger->filePath(), sl->literalToken, u"`" % templateString % u"`"
91 QQmlJSFixSuggestion suggestion = {
"Use a template literal instead."_L1, sl->literalToken,
93 suggestion.setAutoApplicable();
94 m_logger->log(QStringLiteral(
"String contains unescaped line terminator which is "
96 qmlMultilineStrings, sl->literalToken,
true,
true, suggestion);
103 m_ancestryIncludingCurrentNode.push_back(n);
109 Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n);
110 m_ancestryIncludingCurrentNode.pop_back();
115 if (m_ancestryIncludingCurrentNode.size() < 2)
117 return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2];
120bool LinterVisitor::
visit(CommaExpression *expression)
122 QQmlJSImportVisitor::visit(expression);
123 if (!expression->left || !expression->right)
127 if (cast<ForStatement *>(astParentOfVisitedNode()))
130 m_logger->log(
"Do not use comma expressions."_L1, qmlComma, expression->commaToken);
136 static constexpr std::array literals{
"Boolean"_L1,
"Function"_L1,
"JSON"_L1,
137 "Math"_L1,
"Number"_L1,
"String"_L1 };
139 const IdentifierExpression *identifier = cast<IdentifierExpression *>(expression->base);
143 if (std::find(literals.cbegin(), literals.cend(), identifier->name) != literals.cend()) {
144 logger->log(
"Do not use '%1' as a constructor."_L1.arg(identifier->name),
145 qmlLiteralConstructor, identifier->identifierToken);
147 if (identifier->name ==
"Array"_L1 && expression->arguments && expression->arguments->next) {
148 const auto fullRange = combine(expression->newToken, expression->rparenToken);
149 const QList<QQmlJSDocumentEdit> edits = {
150 { logger->filePath(), combine(expression->newToken, expression->lparenToken),
"["_L1 },
151 { logger->filePath(), expression->rparenToken,
"]"_L1 },
153 QQmlJSFixSuggestion fix(
"Replace with array literal"_L1, fullRange, edits);
154 fix.setAutoApplicable(
true);
155 logger->log(
"Array has confusing semantics, use an array literal ([]) instead."_L1,
156 qmlLiteralConstructor, identifier->identifierToken,
true,
true, fix);
160bool LinterVisitor::
visit(NewMemberExpression *expression)
162 QQmlJSImportVisitor::visit(expression);
163 warnAboutLiteralConstructors(expression, m_logger);
167bool LinterVisitor::
visit(VoidExpression *ast)
169 QQmlJSImportVisitor::visit(ast);
170 m_logger->log(
"Do not use void expressions."_L1, qmlVoid, ast->voidToken);
176 Q_ASSERT(exp->op == QSOperator::Add);
178 SourceLocation location = exp->operatorToken;
181 if (
auto increment = cast<PostIncrementExpression *>(exp->left))
182 location = combine(increment->incrementToken, location);
184 if (
auto unary = cast<UnaryPlusExpression *>(exp->right))
185 location = combine(location, unary->plusToken);
187 if (
auto increment = cast<PreIncrementExpression *>(exp->right))
188 location = combine(location, increment->incrementToken);
190 if (location == exp->operatorToken)
191 return SourceLocation{};
198 Q_ASSERT(exp->op == QSOperator::Sub);
200 SourceLocation location = exp->operatorToken;
203 if (
auto decrement = cast<PostDecrementExpression *>(exp->left))
204 location = combine(decrement->decrementToken, location);
206 if (
auto unary = cast<UnaryMinusExpression *>(exp->right))
207 location = combine(location, unary->minusToken);
209 if (
auto decrement = cast<PreDecrementExpression *>(exp->right))
210 location = combine(location, decrement->decrementToken);
212 if (location == exp->operatorToken)
213 return SourceLocation{};
218bool LinterVisitor::
visit(BinaryExpression *exp)
220 QQmlJSImportVisitor::visit(exp);
222 case QSOperator::Add:
223 if (SourceLocation loc = confusingPluses(exp); loc.isValid())
224 m_logger->log(
"Confusing pluses."_L1, qmlConfusingPluses, loc);
226 case QSOperator::Sub:
227 if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
228 m_logger->log(
"Confusing minuses."_L1, qmlConfusingMinuses, loc);
237bool LinterVisitor::
visit(QQmlJS::AST::UiImport *import)
239 QQmlJSImportVisitor::visit(import);
241 const auto locAndName = [](
const UiImport *i) {
243 return std::make_pair(i->fileNameToken, i->fileName.toString());
245 QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
246 if (i->importIdToken.isValid())
247 l = combine(l, i->importIdToken);
249 l = combine(l, i->version->minorToken);
251 l = combine(l, i->importUri->lastSourceLocation());
253 return std::make_pair(l, i->importUri->toString());
256 SeenImport i(import);
257 if (
const auto it = m_seenImports.constFind(i); it != m_seenImports.constEnd()) {
258 const auto locAndNameImport = locAndName(import);
259 const auto locAndNameSeen = locAndName(it->uiImport);
260 m_logger->log(
"Duplicate import '%1'"_L1.arg(locAndNameImport.second),
261 qmlDuplicateImport, locAndNameImport.first);
262 m_logger->log(
"Note: previous import '%1' here"_L1.arg(locAndNameSeen.second),
263 qmlDuplicateImport, locAndNameSeen.first,
true,
true, {},
264 locAndName(import).first.startLine);
267 m_seenImports.insert(i);
271void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
272 const QQmlJS::SourceLocation &location)
274 m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
276 for (
const auto *member = members; member; member = member->next) {
277 if (member->member.toString() == key) {
278 m_logger->log(u"Note: previous declaration of '%1' here"_s.arg(key),
279 qmlDuplicateEnumEntries, member->memberToken);
285bool LinterVisitor::
visit(QQmlJS::AST::UiEnumDeclaration *uied)
287 QQmlJSImportVisitor::visit(uied);
289 if (m_currentScope->isInlineComponent()) {
290 m_logger->log(u"Enums declared inside of inline component are ignored."_s,
291 qmlInlineComponentEnums, uied->firstSourceLocation());
292 }
else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No
293 && !m_currentScope->isFileRootComponent()) {
294 m_logger->log(u"Enum declared outside the root element. It won't be accessible."_s,
295 qmlNonRootEnums, uied->firstSourceLocation());
298 QHash<QStringView,
const QQmlJS::AST::UiEnumMemberList *> seen;
299 for (
const auto *member = uied->members; member; member = member->next) {
300 QStringView key = member->member;
302 if (seen.contains(key))
303 handleDuplicateEnums(uied->members, key, member->memberToken);
305 seen[member->member] = member;
307 if (uied->name == key) {
308 m_logger->log(
"Enum entry should be named differently than the enum itself to avoid "
309 "confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
322 switch (statement->kind) {
323 case Node::Kind_Block: {
324 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
326 case Node::Kind_BreakStatement:
328 case Node::Kind_CaseBlock: {
329 const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
330 if (caseBlock->defaultClause)
331 return allCodePathsReturnInsideCase(caseBlock->defaultClause);
332 return allCodePathsReturnInsideCase(caseBlock->clauses);
334 case Node::Kind_CaseClause:
335 return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
336 case Node::Kind_CaseClauses: {
337 for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
338 caseClauses = caseClauses->next) {
339 if (!allCodePathsReturnInsideCase(caseClauses->clause))
344 case Node::Kind_ContinueStatement:
348 case Node::Kind_DefaultClause:
349 return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
350 case Node::Kind_IfStatement: {
351 const auto *ifStatement = cast<IfStatement *>(statement);
352 return allCodePathsReturnInsideCase(ifStatement->ok)
353 && allCodePathsReturnInsideCase(ifStatement->ko);
355 case Node::Kind_LabelledStatement:
356 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
357 case Node::Kind_ReturnStatement:
359 case Node::Kind_StatementList: {
360 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
361 if (allCodePathsReturnInsideCase(list->statement))
366 case Node::Kind_SwitchStatement:
367 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
368 case Node::Kind_ThrowStatement:
370 case Node::Kind_TryStatement: {
371 auto *tryStatement = cast<TryStatement *>(statement);
372 if (allCodePathsReturnInsideCase(tryStatement->statement))
374 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
376 case Node::Kind_WithStatement:
377 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
384void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
385 SourceLocation nextLoc)
387 if (!statements || !nextLoc.isValid())
390 if (allCodePathsReturnInsideCase(statements))
393 quint32 afterLastStatement = 0;
394 for (StatementList *it = statements; it; it = it->next) {
396 afterLastStatement = it->statement->lastSourceLocation().end();
400 const auto &comments = m_engine->comments();
401 auto it = std::find_if(comments.cbegin(), comments.cend(),
402 [&](
auto c) {
return afterLastStatement < c.offset; });
403 auto end = std::find_if(it, comments.cend(),
404 [&](
auto c) {
return c.offset >= nextLoc.offset; });
406 for (; it != end; ++it) {
407 const QString &commentText = m_engine->code().mid(it->offset, it->length);
408 if (commentText.contains(
"fall through"_L1, Qt::CaseInsensitive)
409 || commentText.contains(
"fall-through"_L1, Qt::CaseInsensitive)
410 || commentText.contains(
"fallthrough"_L1, Qt::CaseInsensitive)) {
416 "Non-empty case block potentially falls through to the next case or default statement. "
417 "Add \"// fallthrough\" at the end of the block to silence this warning."_L1,
418 qmlUnterminatedCase, errorLoc);
421bool LinterVisitor::
visit(QQmlJS::AST::CaseBlock *block)
423 QQmlJSImportVisitor::visit(block);
425 std::vector<std::pair<SourceLocation, StatementList *>> clauses;
426 for (CaseClauses *it = block->clauses; it; it = it->next)
427 clauses.push_back({ it->clause->caseToken, it->clause->statements });
428 if (block->defaultClause)
429 clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
430 for (CaseClauses *it = block->moreClauses; it; it = it->next)
431 clauses.push_back({ it->clause->caseToken, it->clause->statements });
434 for (size_t i = 0; i < clauses.size() - 1; ++i) {
435 const SourceLocation nextToken = clauses[i + 1].first;
436 checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
444 QList<
const Statement *> lasts;
446 for (
const auto *clause = ast->clauses; clause; clause = clause->next)
447 lasts << possibleLastStatements(clause->clause->statements);
448 if (ast->defaultClause)
449 lasts << possibleLastStatements(ast->defaultClause->statements);
450 for (
const auto *clause = ast->moreClauses; clause; clause = clause->next)
451 lasts << possibleLastStatements(clause->clause->statements);
458 if (
const auto *s = cast<
const Block *>(ast))
459 return possibleLastStatements(s->statements) << s;
460 if (
const auto *s = cast<
const BreakStatement *>(ast))
462 if (
const auto *s = cast<
const ContinueStatement *>(ast))
464 if (
const auto *s = cast<
const DebuggerStatement *>(ast))
466 if (
const auto *s = cast<
const DoWhileStatement *>(ast))
467 return possibleLastStatements(s->statement) << s;
468 if (
const auto *s = cast<
const EmptyStatement *>(ast))
470 if (
const auto *s = cast<
const ExportDeclaration *>(ast))
472 if (
const auto *s = cast<
const ExpressionStatement *>(ast))
474 if (
const auto *s = cast<
const ForEachStatement *>(ast))
475 return possibleLastStatements(s->statement) << s;
476 if (
const auto *s = cast<
const ForStatement *>(ast))
477 return possibleLastStatements(s->statement) << s;
478 if (
const auto *s = cast<
const IfStatement *>(ast)) {
479 auto lasts = possibleLastStatements(s->ok);
481 lasts << possibleLastStatements(s->ko);
484 if (
const auto *s = cast<
const ImportDeclaration *>(ast))
486 if (
const auto *s = cast<
const LabelledStatement *>(ast))
487 return possibleLastStatements(s->statement) << ast;
488 if (
const auto *s = cast<
const ReturnStatement *>(ast))
490 if (
const auto *s = cast<
const SwitchStatement *>(ast))
491 return possibleLastStatements(s->block) << s;
492 if (
const auto *s = cast<
const ThrowStatement *>(ast))
494 if (
const auto *s = cast<
const TryStatement *>(ast))
496 if (
const auto *s = cast<
const VariableStatement *>(ast))
498 if (
const auto *s = cast<
const WhileStatement *>(ast))
499 return possibleLastStatements(s->statement) << s;
500 if (
const auto *s = cast<
const WithStatement *>(ast))
501 return possibleLastStatements(s->statement) << s;
503 Q_UNREACHABLE_RETURN({});
510 for (; ast->next; ast = ast->next) { }
511 const auto *statement = ast->statement;
514 if (cast<
const FunctionDeclaration *>(statement))
517 return possibleLastStatements(
static_cast<
const Statement *>(statement));
523 case Node::Kind_CallExpression:
524 case Node::Kind_DeleteExpression:
525 case Node::Kind_NewExpression:
526 case Node::Kind_PreDecrementExpression:
527 case Node::Kind_PreIncrementExpression:
528 case Node::Kind_PostDecrementExpression:
529 case Node::Kind_PostIncrementExpression:
530 case Node::Kind_YieldExpression:
531 case Node::Kind_FunctionExpression:
533 case Node::Kind_NumericLiteral:
534 case Node::Kind_StringLiteral:
535 case Node::Kind_FalseLiteral:
536 case Node::Kind_TrueLiteral:
537 case Node::Kind_NullExpression:
538 case Node::Kind_Undefined:
539 case Node::Kind_RegExpLiteral:
540 case Node::Kind_SuperLiteral:
541 case Node::Kind_ThisExpression:
542 case Node::Kind_FieldMemberExpression:
543 case Node::Kind_IdentifierExpression:
544 case Node::Kind_TypeOfExpression:
550 if (
const auto *e = cast<
const NestedExpression *>(ast))
551 return isUselessExpressionStatement_impl(e->expression);
552 if (
const auto *e = cast<
const NotExpression *>(ast))
553 return isUselessExpressionStatement_impl(e->expression);
554 if (
const auto *e = cast<
const TildeExpression *>(ast))
555 return isUselessExpressionStatement_impl(e->expression);
556 if (
const auto *e = cast<
const UnaryMinusExpression *>(ast))
557 return isUselessExpressionStatement_impl(e->expression);
558 if (
const auto *e = cast<
const UnaryPlusExpression *>(ast))
559 return isUselessExpressionStatement_impl(e->expression);
560 if (
const auto *e = cast<
const ConditionalExpression *>(ast))
561 return isUselessExpressionStatement_impl(e->ok) && isUselessExpressionStatement_impl(e->ko);
563 if (
const BinaryExpression *binary = cast<
const BinaryExpression *>(ast)) {
564 switch (binary->op) {
565 case QSOperator::InplaceAnd:
566 case QSOperator::Assign:
567 case QSOperator::InplaceSub:
568 case QSOperator::InplaceDiv:
569 case QSOperator::InplaceExp:
570 case QSOperator::InplaceAdd:
571 case QSOperator::InplaceLeftShift:
572 case QSOperator::InplaceMod:
573 case QSOperator::InplaceMul:
574 case QSOperator::InplaceOr:
575 case QSOperator::InplaceRightShift:
576 case QSOperator::InplaceURightShift:
577 case QSOperator::InplaceXor:
580 return isUselessExpressionStatement_impl(binary->left)
581 && isUselessExpressionStatement_impl(binary->right);
589
590
591
592
595 return isUselessExpressionStatement_impl(ast->expression);
598void LinterVisitor::handleUselessExpressionStatement(
const ExpressionStatement *ast)
601 const auto it = std::find_if(m_ancestryIncludingCurrentNode.crbegin(),
602 m_ancestryIncludingCurrentNode.crend(),
604 return it->kind == Node::Kind_UiPublicMember
605 || it->kind == Node::Kind_FunctionDeclaration
606 || it->kind == Node::Kind_UiScriptBinding;
609 if (it == m_ancestryIncludingCurrentNode.crend())
613 const auto isLastExprStat = [](
const ExpressionStatement *statement,
const Statement *base) {
614 const auto lasts = possibleLastStatements(base);
615 return lasts.contains(statement);
618 if (
const auto *usb = cast<UiScriptBinding *>(*it); usb && usb->qualifiedId) {
619 if (usb->qualifiedId->toString() ==
"id"_L1)
621 if (usb->qualifiedId->next)
623 if (m_savedBindingOuterScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope)
626 QQmlJSScope::Ptr object = m_currentScope;
627 while (object && object->scopeType() != QQmlSA::ScopeType::QMLScope)
628 object = object->parentScope();
633 if (m_propertyBindings.contains(object)) {
634 for (
const auto &entry : m_propertyBindings[object]) {
635 if (entry.data == usb->qualifiedId->toString()) {
636 if (isLastExprStat(ast, usb->statement))
645 const auto *upm = cast<
const UiPublicMember *>(*it);
646 if (upm && upm->type == AST::UiPublicMember::Property && upm->statement) {
647 if (isLastExprStat(ast, upm->statement))
651 if (isUselessExpressionStatement(ast)) {
652 m_logger->log(
"Expression statement has no obvious effect."_L1,
653 qmlConfusingExpressionStatement,
654 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
658bool LinterVisitor::
visit(ExpressionStatement *ast)
660 QQmlJSImportVisitor::visit(ast);
661 handleUselessExpressionStatement(ast);
665bool LinterVisitor::
safeInsertJSIdentifier(QQmlJSScope::Ptr &scope,
const QString &name,
const QQmlJSScope::JavaScriptIdentifier &identifier)
667 if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
668 identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
671 Q_ASSERT(!scope->parentScope().isNull());
672 auto parentScopeType = scope->parentScope()->scopeType();
673 bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
674 || parentScopeType == QQmlSA::ScopeType::SignalHandlerFunctionScope;
675 if (!inTopLevelBindingBlockScope) {
676 m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
677 u"Replace it with const or let to silence the warning\n"_s,
678 qmlBlockScopeVarDeclaration, identifier.location);
680 }
else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
684 return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
688 const QString &name,
const QQmlJS::AST::Statement *statement,
689 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
691 if (statement && statement->kind == (
int)AST::Node::Kind::Kind_Block) {
692 const auto *block =
static_cast<
const AST::Block *>(statement);
693 if (!block->statements && associatedPropertyDefinition) {
694 m_logger->log(
"Unintentional empty block, use ({}) for empty object literal"_L1,
695 qmlUnintentionalEmptyBlock,
696 combine(block->lbraceToken, block->rbraceToken));
700 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
704 const UiPublicMember *associatedPropertyDefinition)
706 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
709 if (!associatedPropertyDefinition->isReadonly())
712 const auto &prop = m_currentScope->property(binding.propertyName());
713 const auto log = [&](
const QString &preferredType) {
714 m_logger->log(
"Prefer more specific type %1 over var"_L1.arg(preferredType),
715 qmlPreferNonVarProperties, prop.sourceLocation());
718 if (prop.typeName() !=
"QVariant"_L1)
721 switch (binding.bindingType()) {
722 case QQmlSA::BindingType::BoolLiteral: {
726 case QQmlSA::BindingType::NumberLiteral: {
727 double v = binding.numberValue();
728 auto loc = binding.sourceLocation();
729 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
730 if (literal.contains(u'.') ||
double(
int(v)) != v)
731 log(
"real or double"_L1);
736 case QQmlSA::BindingType::StringLiteral: {
746bool LinterVisitor::
visit(UiProgram *ast)
748 const bool result = QQmlJSImportVisitor::visit(ast);
750 m_renamedComponents.setScopeToName(&m_rootScopeImports.names());
755void LinterVisitor::checkUnusedImports()
758 if (m_hasMaybeUnresolvedTypes || m_unresolvedTypes.begin() != m_unresolvedTypes.end())
761 auto unusedImports = m_importLocations;
762 for (
const QString &type : std::as_const(m_usedTypes)) {
763 const auto &importLocations = m_importTypeLocationMap.values(type);
764 for (
const auto &importLocation : importLocations)
765 unusedImports.remove(importLocation);
768 if (unusedImports.isEmpty())
772 const auto &imports = m_importStaticModuleLocationMap.values();
773 for (
const QQmlJS::SourceLocation &import : imports)
774 unusedImports.remove(import);
776 for (
const auto &import : unusedImports) {
777 m_logger->log(QString::fromLatin1(
"Unused import"), qmlUnusedImports, import);
783 QQmlJSImportVisitor::endVisit(ast);
784 checkFileSelections();
785 checkUnusedImports();
786 checkSignalHandlerNameClashes();
797 const QQmlJS::SourceLocation &location,
798 QQmlJSLogger *logger)
801 if (!base->hasMethod(name))
804 static constexpr QLatin1String warningMessage =
805 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
806 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
807 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
808 logger->log(warningMessage.arg(isSignal ?
"Signal"_L1 :
"Method"_L1, name,
809 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
810 qmlShadow, location);
814 const QQmlJS::SourceLocation &location,
815 OverrideInformations overrideFlags, QQmlJSLogger *logger)
818 const bool hasOverride = overrideFlags.testFlag(
WithOverride);
819 if (!base->hasProperty(name)) {
823 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
825 qmlPropertyOverride, location);
829 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
830 const auto shadowedProperty = owner->ownProperty(name);
831 if (shadowedProperty.isFinal()) {
834 ?
"Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
835 :
"Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
836 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
837 qmlPropertyOverride, location);
841 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
842 if (hasOverride || overrideFlags.testFlag(
WithFinal))
846 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
847 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
848 qmlPropertyOverride, location);
854 "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
855 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
856 qmlPropertyOverride, location);
859 logger->log(
"Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
860 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
861 qmlPropertyOverride, location);
865 QLatin1String type,
const QQmlJS::SourceLocation &location,
866 OverrideInformations overrideFlags, QQmlJSLogger *logger)
868 static constexpr QLatin1String duplicateMessage =
869 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
870 if (
const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
871 const bool isSignal = methods.front().methodType() == QQmlSA::MethodType::Signal;
872 const bool alreadyWarnedByIrBuilder =
873 (type == s_signal && isSignal) || (type == s_method && !isSignal);
874 if (!alreadyWarnedByIrBuilder) {
875 logger->log(duplicateMessage.arg(type, name, isSignal ? s_signal : s_method),
876 qmlDuplicatedName, location);
880 if (type != s_property && scope->hasOwnProperty(name))
881 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
883 const QQmlJSScope::ConstPtr base = scope->baseType();
887 warnForMethodShadowingInBase(base, name, location, logger);
888 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
891static bool hasSignal(
const QQmlJSScope::ConstPtr &scope,
const QString &name)
893 const auto methods = scope->methods(name);
894 return std::any_of(methods.cbegin(), methods.cend(), [](
const QQmlJSMetaMethod &method) {
895 return method.methodType() == QQmlSA::MethodType::Signal;
900
901
902
903
904
905
906
907void LinterVisitor::checkSignalHandlerNameClashes()
909 static constexpr QLatin1String clashMessage =
910 "Property \"%1\" is interpreted as a signal handler for %2 \"%3\", "
911 "use a different name."_L1;
913 for (
const auto &[scope, name, location] : std::as_const(m_handlerLikeProperties)) {
914 QString changedPropertyName;
916 if (
const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(name)) {
917 changedPropertyName = *propertyName;
918 signalName = *QQmlSignalNames::changedHandlerNameToSignalName(name);
920 signalName = QQmlSignalNames::handlerNameToSignalName(name).value_or(QString());
923 if (signalName.isEmpty())
926 if (hasSignal(scope, signalName)) {
927 m_logger->log(clashMessage.arg(name, s_signal, signalName), qmlSignalHandlerNameClash,
929 }
else if (!changedPropertyName.isEmpty() && scope->hasProperty(changedPropertyName)) {
930 m_logger->log(clashMessage.arg(name, s_property, changedPropertyName),
931 qmlSignalHandlerNameClash, location);
932 }
else if (scope->hasOwnProperty(signalName)) {
933 m_logger->log(clashMessage.arg(name, s_property, signalName), qmlSignalHandlerNameClash,
939void LinterVisitor::handleRenamedType(UiQualifiedId *qualifiedId)
941 m_renamedComponents.handleRenamedType(
942 m_rootScopeImports.type(qualifiedId->name.toString()).scope, qualifiedId->name,
943 qualifiedId->identifierToken, m_logger);
946bool LinterVisitor::
visit(Type *type)
948 const bool result = QQmlJSImportVisitor::visit(type);
950 handleRenamedType(type->typeId);
955void LinterVisitor::handleRecursivelyInstantiatedType(UiQualifiedId *qualifiedId)
958 if (qualifiedId->next)
961 auto logWarning = [&qualifiedId,
this]() {
962 m_logger->log(
"Type \"%1\" can't be instantiated recursively"_L1.arg(qualifiedId->name),
963 qmlTypeInstantiatedRecursively, qualifiedId->identifierToken);
966 const QString name = qualifiedId->name.toString();
967 if (m_rootScopeImports.names().contains(m_exportedRootScope, name))
971 if (
const auto inlineComponentName = std::get_if<InlineComponentNameType>(&m_currentRootName);
972 inlineComponentName && name == *inlineComponentName) {
977bool LinterVisitor::
visit(QQmlJS::AST::UiPragma *pragma)
979 const bool result = QQmlJSImportVisitor::visit(pragma);
982 if (pragma->name == u"Singleton")
983 m_rootIsSingleton =
true;
988void LinterVisitor::checkSingletonRoot()
990 const bool hasQmldirSingletonEntry = m_exportedRootScope->isSingleton();
991 const bool hasSingletonPragma = m_rootIsSingleton;
993 if (hasQmldirSingletonEntry == hasSingletonPragma)
996 if (hasQmldirSingletonEntry && !hasSingletonPragma) {
997 m_logger->log(
"Type %1 declared as singleton in qmldir but missing pragma Singleton"_L1.arg(
998 m_exportedRootScope->internalName()),
999 qmlImport, QQmlJS::SourceLocation());
1002 Q_ASSERT(!hasQmldirSingletonEntry && hasSingletonPragma);
1003 m_logger->log(
"Type %1 not declared as singleton in qmldir but using pragma Singleton"_L1.arg(
1004 m_exportedRootScope->internalName()),
1005 qmlImport, QQmlJS::SourceLocation());
1008bool LinterVisitor::
visit(QQmlJS::AST::UiObjectDefinition *objectDefinition)
1010 handleRenamedType(objectDefinition->qualifiedTypeNameId);
1011 handleRecursivelyInstantiatedType(objectDefinition->qualifiedTypeNameId);
1012 if (!rootScopeIsValid() && !objectDefinition->qualifiedTypeNameId->name.front().isLower())
1013 checkSingletonRoot();
1015 return QQmlJSImportVisitor::visit(objectDefinition);
1018bool LinterVisitor::
visit(UiPublicMember *publicMember)
1020 switch (publicMember->type) {
1021 case UiPublicMember::Signal: {
1022 const QString signalName = publicMember->name.toString();
1023 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
1027 case QQmlJS::AST::UiPublicMember::Property: {
1028 const QString propertyName = publicMember->name.toString();
1029 OverrideInformations flags;
1030 flags.setFlag(
WithOverride, publicMember->isOverride());
1031 flags.setFlag(
WithFinal, publicMember->isFinal());
1032 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
1034 if (QQmlSignalNames::isHandlerName(propertyName)) {
1035 m_handlerLikeProperties.append(
1036 { m_currentScope, propertyName, publicMember->identifierToken });
1038 handleRenamedType(publicMember->memberType);
1040 const QString typeName = publicMember->memberType->toString();
1041 if (typeName !=
"alias"_L1) {
1042 if (m_rootScopeImports.hasType(typeName)
1043 && !m_rootScopeImports.type(typeName).scope.isNull()) {
1044 if (m_importTypeLocationMap.contains(typeName))
1045 m_usedTypes.insert(typeName);
1051 return QQmlJSImportVisitor::visit(publicMember);
1054bool LinterVisitor::
visit(FunctionExpression *fexpr)
1056 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
1057 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
1060 return QQmlJSImportVisitor::visit(fexpr);
1063bool LinterVisitor::
visit(FunctionDeclaration *fdecl)
1065 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
1066 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
1069 return QQmlJSImportVisitor::visit(fdecl);
1073
1074
1076 const QQmlJSScope::ConstPtr &scope2)
1078 for (
const auto &[propertyName, prop] : scope1->properties().asKeyValueRange())
1079 if (!scope2->hasProperty(propertyName))
1081 for (
const auto &[methodName, method] : scope1->methods().asKeyValueRange())
1082 if (!scope2->hasMethod(methodName))
1088void LinterVisitor::checkFileSelections()
1090 const QQmlJS::FileSelectorInfo info =
1091 m_rootScopeImports.contextualTypes().fileSelectorInfoFor(m_exportedRootScope);
1093 if (info.fileSelectedTypes.isEmpty() || info.mainType.isNull())
1096 const QString name = m_rootScopeImports.name(m_exportedRootScope);
1098 if (info.mainType == m_exportedRootScope) {
1100 for (
const auto &fileSelected : info.fileSelectedTypes) {
1101 if (compatibilityHeuristicForFileSelector(m_exportedRootScope,
1102 fileSelected.type.scope)) {
1104 "Type %1 is ambiguous due to file selector usage, ignoring %2."_L1.arg(
1105 name, fileSelected.type.scope->filePath()),
1106 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1109 m_logger->log(
"Type %1 has a potentially incompatible file-selected variant %2."_L1.arg(
1110 name, fileSelected.type.scope->filePath()),
1111 qmlImport, m_exportedRootScope->sourceLocation());
1117 if (compatibilityHeuristicForFileSelector(info.mainType, m_exportedRootScope)) {
1119 "File-selected type %1 is ambiguous due to file selector usage, this file will be ignored in favour of %2."_L1
1120 .arg(name, info.mainType->filePath()),
1121 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1124 m_logger->log(
"File-selected type %1 is potentially incompatible with %2."_L1.arg(
1125 name, info.mainType->filePath()),
1126 qmlImport, m_exportedRootScope->sourceLocation());
1132 if (m_importTypeLocationMap.contains(name)
1133 && m_importTypeLocationMap.values(name).contains(loc)) {
1137 m_importTypeLocationMap.insert(name, loc);
1142 if (!hadWarnings && loc.isValid())
1143 m_importLocations.insert(loc);
1147 const QQmlJS::SourceLocation &loc,
1151 if (isDependency && m_importStaticModuleLocationMap.contains(name))
1153 m_importStaticModuleLocationMap[name] = loc;
1156bool LinterVisitor::
visit(QQmlJS::AST::IdentifierExpression *idexp)
1158 const QString name = idexp->name.toString();
1159 if (m_importTypeLocationMap.contains(name)) {
1160 m_usedTypes.insert(name);
1162 m_hasMaybeUnresolvedTypes = m_hasMaybeUnresolvedTypes || name.front().isUpper();
1168void LinterVisitor::
endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember)
1172 const QString name = fieldMember->name.toString();
1173 if (m_importTypeLocationMap.contains(name)) {
1174 const QQmlJSImportedScope type = m_rootScopeImports.type(name);
1175 if (type.scope.isNull()) {
1176 if (m_rootScopeImports.hasType(name))
1177 m_usedTypes.insert(name);
1178 }
else if (!type.scope->attachedTypeName().isEmpty()) {
1179 m_usedTypes.insert(name);
1182 m_hasMaybeUnresolvedTypes = m_hasMaybeUnresolvedTypes || name.front().isUpper();
1188 for (
const auto &scope : std::as_const(m_objectDefinitionScopes))
1189 checkGroupedAndAttachedScope(scope);
1192void LinterVisitor::checkGroupedAndAttachedScope(
const QQmlJSScope::ConstPtr &scope)
1196 if (checkCustomParser(scope))
1199 if (!checkTypeResolved(scope))
1202 auto children = scope->childScopes();
1203 while (!children.isEmpty()) {
1204 auto childScope = children.takeFirst();
1205 const auto type = childScope->scopeType();
1207 case QQmlSA::ScopeType::GroupedPropertyScope:
1208 case QQmlSA::ScopeType::AttachedPropertyScope:
1209 if (!childScope->baseType() && !m_unresolvedTypes.hasSeen(childScope)) {
1212 m_logger->log(QStringLiteral(
"unknown %1 property scope %2.")
1213 .arg(type == QQmlSA::ScopeType::GroupedPropertyScope
1214 ? QStringLiteral(
"grouped")
1215 : QStringLiteral(
"attached"),
1216 childScope->internalName()),
1217 qmlUnqualified, childScope->sourceLocation());
1219 children.append(childScope->childScopes());
void postVisit(QQmlJS::AST::Node *) override
void checkGroupedAndAttachedScopes() override
void addStaticImportWithLocation(const QString &name, const QQmlJS::SourceLocation &loc, bool isDependency) override
void handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding, const AST::UiPublicMember *associatedPropertyDefinition) override
LinterVisitor(QQmlJSImporter *importer, QQmlJSLogger *logger, const QString &implicitImportDirectory, const QStringList &qmldirFiles=QStringList(), QQmlJS::Engine *engine=nullptr)
BindingExpressionParseResult parseBindingExpression(const QString &name, const QQmlJS::AST::Statement *statement, const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition=nullptr) override
void addImportWithLocation(const QString &name, const QQmlJS::SourceLocation &loc, bool hadWarnings) override
void leaveEnvironment() override
bool safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier) override
bool visit(QQmlJS::AST::StringLiteral *) override
QQmlJS::AST::Node * astParentOfVisitedNode() const
void endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember) override
bool preVisit(QQmlJS::AST::Node *) override
static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name, const QQmlJS::SourceLocation &location, QQmlJSLogger *logger)
static bool isUselessExpressionStatement_impl(const ExpressionNode *ast)
static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name, QLatin1String type, const QQmlJS::SourceLocation &location, OverrideInformations overrideFlags, QQmlJSLogger *logger)
static bool hasSignal(const QQmlJSScope::ConstPtr &scope, const QString &name)
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.