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;
301 if (!key.front().isUpper()) {
302 m_logger->log(u"Enum keys should start with an uppercase."_s, qmlEnumKeyCase,
303 member->memberToken);
306 if (seen.contains(key))
307 handleDuplicateEnums(uied->members, key, member->memberToken);
309 seen[member->member] = member;
311 if (uied->name == key) {
312 m_logger->log(
"Enum entry should be named differently than the enum itself to avoid "
313 "confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
326 switch (statement->kind) {
327 case Node::Kind_Block: {
328 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
330 case Node::Kind_BreakStatement:
332 case Node::Kind_CaseBlock: {
333 const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
334 if (caseBlock->defaultClause)
335 return allCodePathsReturnInsideCase(caseBlock->defaultClause);
336 return allCodePathsReturnInsideCase(caseBlock->clauses);
338 case Node::Kind_CaseClause:
339 return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
340 case Node::Kind_CaseClauses: {
341 for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
342 caseClauses = caseClauses->next) {
343 if (!allCodePathsReturnInsideCase(caseClauses->clause))
348 case Node::Kind_ContinueStatement:
352 case Node::Kind_DefaultClause:
353 return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
354 case Node::Kind_IfStatement: {
355 const auto *ifStatement = cast<IfStatement *>(statement);
356 return allCodePathsReturnInsideCase(ifStatement->ok)
357 && allCodePathsReturnInsideCase(ifStatement->ko);
359 case Node::Kind_LabelledStatement:
360 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
361 case Node::Kind_ReturnStatement:
363 case Node::Kind_StatementList: {
364 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
365 if (allCodePathsReturnInsideCase(list->statement))
370 case Node::Kind_SwitchStatement:
371 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
372 case Node::Kind_ThrowStatement:
374 case Node::Kind_TryStatement: {
375 auto *tryStatement = cast<TryStatement *>(statement);
376 if (allCodePathsReturnInsideCase(tryStatement->statement))
378 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
380 case Node::Kind_WithStatement:
381 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
388void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
389 SourceLocation nextLoc)
391 if (!statements || !nextLoc.isValid())
394 if (allCodePathsReturnInsideCase(statements))
397 quint32 afterLastStatement = 0;
398 for (StatementList *it = statements; it; it = it->next) {
400 afterLastStatement = it->statement->lastSourceLocation().end();
404 const auto &comments = m_engine->comments();
405 auto it = std::find_if(comments.cbegin(), comments.cend(),
406 [&](
auto c) {
return afterLastStatement < c.offset; });
407 auto end = std::find_if(it, comments.cend(),
408 [&](
auto c) {
return c.offset >= nextLoc.offset; });
410 for (; it != end; ++it) {
411 const QString &commentText = m_engine->code().mid(it->offset, it->length);
412 if (commentText.contains(
"fall through"_L1, Qt::CaseInsensitive)
413 || commentText.contains(
"fall-through"_L1, Qt::CaseInsensitive)
414 || commentText.contains(
"fallthrough"_L1, Qt::CaseInsensitive)) {
420 "Non-empty case block potentially falls through to the next case or default statement. "
421 "Add \"// fallthrough\" at the end of the block to silence this warning."_L1,
422 qmlUnterminatedCase, errorLoc);
425bool LinterVisitor::
visit(QQmlJS::AST::CaseBlock *block)
427 QQmlJSImportVisitor::visit(block);
429 std::vector<std::pair<SourceLocation, StatementList *>> clauses;
430 for (CaseClauses *it = block->clauses; it; it = it->next)
431 clauses.push_back({ it->clause->caseToken, it->clause->statements });
432 if (block->defaultClause)
433 clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
434 for (CaseClauses *it = block->moreClauses; it; it = it->next)
435 clauses.push_back({ it->clause->caseToken, it->clause->statements });
438 for (size_t i = 0; i < clauses.size() - 1; ++i) {
439 const SourceLocation nextToken = clauses[i + 1].first;
440 checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
448 QList<
const Statement *> lasts;
450 for (
const auto *clause = ast->clauses; clause; clause = clause->next)
451 lasts << possibleLastStatements(clause->clause->statements);
452 if (ast->defaultClause)
453 lasts << possibleLastStatements(ast->defaultClause->statements);
454 for (
const auto *clause = ast->moreClauses; clause; clause = clause->next)
455 lasts << possibleLastStatements(clause->clause->statements);
462 if (
const auto *s = cast<
const Block *>(ast))
463 return possibleLastStatements(s->statements) << s;
464 if (
const auto *s = cast<
const BreakStatement *>(ast))
466 if (
const auto *s = cast<
const ContinueStatement *>(ast))
468 if (
const auto *s = cast<
const DebuggerStatement *>(ast))
470 if (
const auto *s = cast<
const DoWhileStatement *>(ast))
471 return possibleLastStatements(s->statement) << s;
472 if (
const auto *s = cast<
const EmptyStatement *>(ast))
474 if (
const auto *s = cast<
const ExportDeclaration *>(ast))
476 if (
const auto *s = cast<
const ExpressionStatement *>(ast))
478 if (
const auto *s = cast<
const ForEachStatement *>(ast))
479 return possibleLastStatements(s->statement) << s;
480 if (
const auto *s = cast<
const ForStatement *>(ast))
481 return possibleLastStatements(s->statement) << s;
482 if (
const auto *s = cast<
const IfStatement *>(ast)) {
483 auto lasts = possibleLastStatements(s->ok);
485 lasts << possibleLastStatements(s->ko);
488 if (
const auto *s = cast<
const ImportDeclaration *>(ast))
490 if (
const auto *s = cast<
const LabelledStatement *>(ast))
491 return possibleLastStatements(s->statement) << ast;
492 if (
const auto *s = cast<
const ReturnStatement *>(ast))
494 if (
const auto *s = cast<
const SwitchStatement *>(ast))
495 return possibleLastStatements(s->block) << s;
496 if (
const auto *s = cast<
const ThrowStatement *>(ast))
498 if (
const auto *s = cast<
const TryStatement *>(ast))
500 if (
const auto *s = cast<
const VariableStatement *>(ast))
502 if (
const auto *s = cast<
const WhileStatement *>(ast))
503 return possibleLastStatements(s->statement) << s;
504 if (
const auto *s = cast<
const WithStatement *>(ast))
505 return possibleLastStatements(s->statement) << s;
507 Q_UNREACHABLE_RETURN({});
514 for (; ast->next; ast = ast->next) { }
515 const auto *statement = ast->statement;
518 if (cast<
const FunctionDeclaration *>(statement))
521 return possibleLastStatements(
static_cast<
const Statement *>(statement));
527 case Node::Kind_CallExpression:
528 case Node::Kind_DeleteExpression:
529 case Node::Kind_NewExpression:
530 case Node::Kind_PreDecrementExpression:
531 case Node::Kind_PreIncrementExpression:
532 case Node::Kind_PostDecrementExpression:
533 case Node::Kind_PostIncrementExpression:
534 case Node::Kind_YieldExpression:
535 case Node::Kind_FunctionExpression:
537 case Node::Kind_NumericLiteral:
538 case Node::Kind_StringLiteral:
539 case Node::Kind_FalseLiteral:
540 case Node::Kind_TrueLiteral:
541 case Node::Kind_NullExpression:
542 case Node::Kind_Undefined:
543 case Node::Kind_RegExpLiteral:
544 case Node::Kind_SuperLiteral:
545 case Node::Kind_ThisExpression:
546 case Node::Kind_FieldMemberExpression:
547 case Node::Kind_IdentifierExpression:
548 case Node::Kind_TypeOfExpression:
554 if (
const auto *e = cast<
const NestedExpression *>(ast))
555 return isUselessExpressionStatement_impl(e->expression);
556 if (
const auto *e = cast<
const NotExpression *>(ast))
557 return isUselessExpressionStatement_impl(e->expression);
558 if (
const auto *e = cast<
const TildeExpression *>(ast))
559 return isUselessExpressionStatement_impl(e->expression);
560 if (
const auto *e = cast<
const UnaryMinusExpression *>(ast))
561 return isUselessExpressionStatement_impl(e->expression);
562 if (
const auto *e = cast<
const UnaryPlusExpression *>(ast))
563 return isUselessExpressionStatement_impl(e->expression);
564 if (
const auto *e = cast<
const ConditionalExpression *>(ast))
565 return isUselessExpressionStatement_impl(e->ok) && isUselessExpressionStatement_impl(e->ko);
567 if (
const BinaryExpression *binary = cast<
const BinaryExpression *>(ast)) {
568 switch (binary->op) {
569 case QSOperator::InplaceAnd:
570 case QSOperator::Assign:
571 case QSOperator::InplaceSub:
572 case QSOperator::InplaceDiv:
573 case QSOperator::InplaceExp:
574 case QSOperator::InplaceAdd:
575 case QSOperator::InplaceLeftShift:
576 case QSOperator::InplaceMod:
577 case QSOperator::InplaceMul:
578 case QSOperator::InplaceOr:
579 case QSOperator::InplaceRightShift:
580 case QSOperator::InplaceURightShift:
581 case QSOperator::InplaceXor:
584 return isUselessExpressionStatement_impl(binary->left)
585 && isUselessExpressionStatement_impl(binary->right);
593
594
595
596
599 return isUselessExpressionStatement_impl(ast->expression);
602void LinterVisitor::handleUselessExpressionStatement(
const ExpressionStatement *ast)
605 const auto it = std::find_if(m_ancestryIncludingCurrentNode.crbegin(),
606 m_ancestryIncludingCurrentNode.crend(),
608 return it->kind == Node::Kind_UiPublicMember
609 || it->kind == Node::Kind_FunctionDeclaration
610 || it->kind == Node::Kind_UiScriptBinding;
613 if (it == m_ancestryIncludingCurrentNode.crend())
617 const auto isLastExprStat = [](
const ExpressionStatement *statement,
const Statement *base) {
618 const auto lasts = possibleLastStatements(base);
619 return lasts.contains(statement);
622 if (
const auto *usb = cast<UiScriptBinding *>(*it); usb && usb->qualifiedId) {
623 if (usb->qualifiedId->toString() ==
"id"_L1)
625 if (usb->qualifiedId->next)
627 if (m_savedBindingOuterScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope)
630 QQmlJSScope::Ptr object = m_currentScope;
631 while (object && object->scopeType() != QQmlSA::ScopeType::QMLScope)
632 object = object->parentScope();
637 if (m_propertyBindings.contains(object)) {
638 for (
const auto &entry : m_propertyBindings[object]) {
639 if (entry.data == usb->qualifiedId->toString()) {
640 if (isLastExprStat(ast, usb->statement))
649 const auto *upm = cast<
const UiPublicMember *>(*it);
650 if (upm && upm->type == AST::UiPublicMember::Property && upm->statement) {
651 if (isLastExprStat(ast, upm->statement))
655 if (isUselessExpressionStatement(ast)) {
656 m_logger->log(
"Expression statement has no obvious effect."_L1,
657 qmlConfusingExpressionStatement,
658 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
662bool LinterVisitor::
visit(ExpressionStatement *ast)
664 QQmlJSImportVisitor::visit(ast);
665 handleUselessExpressionStatement(ast);
669bool LinterVisitor::
safeInsertJSIdentifier(QQmlJSScope::Ptr &scope,
const QString &name,
const QQmlJSScope::JavaScriptIdentifier &identifier)
671 if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
672 identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
675 Q_ASSERT(!scope->parentScope().isNull());
676 auto parentScopeType = scope->parentScope()->scopeType();
677 bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
678 || parentScopeType == QQmlSA::ScopeType::SignalHandlerFunctionScope;
679 if (!inTopLevelBindingBlockScope) {
680 m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
681 u"Replace it with const or let to silence the warning\n"_s,
682 qmlBlockScopeVarDeclaration, identifier.location);
684 }
else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
685 const QQmlJSScope *scopePtr = scope.get();
686 std::pair<
const QQmlJSScope*, QString> misplaced { scopePtr, name };
687 if (misplacedJSIdentifiers.contains(misplaced))
689 misplacedJSIdentifiers.insert(misplaced);
690 m_logger->log(u"JavaScript declarations are not allowed in QML elements"_s, qmlSyntax,
691 identifier.location);
694 return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
698 const QString &name,
const QQmlJS::AST::Statement *statement,
699 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
701 if (statement && statement->kind == (
int)AST::Node::Kind::Kind_Block) {
702 const auto *block =
static_cast<
const AST::Block *>(statement);
703 if (!block->statements && associatedPropertyDefinition) {
704 m_logger->log(
"Unintentional empty block, use ({}) for empty object literal"_L1,
705 qmlUnintentionalEmptyBlock,
706 combine(block->lbraceToken, block->rbraceToken));
710 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
714 const UiPublicMember *associatedPropertyDefinition)
716 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
719 if (!associatedPropertyDefinition->isReadonly())
722 const auto &prop = m_currentScope->property(binding.propertyName());
723 const auto log = [&](
const QString &preferredType) {
724 m_logger->log(
"Prefer more specific type %1 over var"_L1.arg(preferredType),
725 qmlPreferNonVarProperties, prop.sourceLocation());
728 if (prop.typeName() !=
"QVariant"_L1)
731 switch (binding.bindingType()) {
732 case QQmlSA::BindingType::BoolLiteral: {
736 case QQmlSA::BindingType::NumberLiteral: {
737 double v = binding.numberValue();
738 auto loc = binding.sourceLocation();
739 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
740 if (literal.contains(u'.') ||
double(
int(v)) != v)
741 log(
"real or double"_L1);
746 case QQmlSA::BindingType::StringLiteral: {
756bool LinterVisitor::
visit(UiProgram *ast)
758 const bool result = QQmlJSImportVisitor::visit(ast);
760 m_renamedComponents.setScopeToName(&m_rootScopeImports.names());
765void LinterVisitor::checkUnusedImports()
767 auto unusedImports = m_importLocations;
768 for (
const QString &type : std::as_const(m_usedTypes)) {
769 const auto &importLocations = m_importTypeLocationMap.values(type);
770 for (
const auto &importLocation : importLocations)
771 unusedImports.remove(importLocation);
774 if (unusedImports.isEmpty())
778 const auto &imports = m_importStaticModuleLocationMap.values();
779 for (
const QQmlJS::SourceLocation &import : imports)
780 unusedImports.remove(import);
782 for (
const auto &import : unusedImports) {
783 m_logger->log(QString::fromLatin1(
"Unused import"), qmlUnusedImports, import);
789 QQmlJSImportVisitor::endVisit(ast);
790 checkFileSelections();
791 checkUnusedImports();
792 checkSignalHandlerNameClashes();
803 const QQmlJS::SourceLocation &location,
804 QQmlJSLogger *logger)
807 if (!base->hasMethod(name))
810 static constexpr QLatin1String warningMessage =
811 "%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
812 const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
813 const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
814 logger->log(warningMessage.arg(isSignal ?
"Signal"_L1 :
"Method"_L1, name,
815 QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
816 qmlShadow, location);
820 const QQmlJS::SourceLocation &location,
821 OverrideInformations overrideFlags, QQmlJSLogger *logger)
824 const bool hasOverride = overrideFlags.testFlag(
WithOverride);
825 if (!base->hasProperty(name)) {
829 "Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
831 qmlPropertyOverride, location);
835 const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
836 const auto shadowedProperty = owner->ownProperty(name);
837 if (shadowedProperty.isFinal()) {
840 ?
"Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
841 :
"Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
842 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
843 qmlPropertyOverride, location);
847 if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
848 if (hasOverride || overrideFlags.testFlag(
WithFinal))
852 "Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
853 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
854 qmlPropertyOverride, location);
860 "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
861 .arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
862 qmlPropertyOverride, location);
865 logger->log(
"Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
866 name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
867 qmlPropertyOverride, location);
871 QLatin1String type,
const QQmlJS::SourceLocation &location,
872 OverrideInformations overrideFlags, QQmlJSLogger *logger)
874 static constexpr QLatin1String duplicateMessage =
875 "Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
876 if (
const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
877 logger->log(duplicateMessage.arg(type, name,
878 methods.front().methodType() == QQmlSA::MethodType::Signal
881 qmlDuplicatedName, location);
883 if (scope->hasOwnProperty(name))
884 logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
886 const QQmlJSScope::ConstPtr base = scope->baseType();
890 warnForMethodShadowingInBase(base, name, location, logger);
891 warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
894static bool hasSignal(
const QQmlJSScope::ConstPtr &scope,
const QString &name)
896 const auto methods = scope->methods(name);
897 return std::any_of(methods.cbegin(), methods.cend(), [](
const QQmlJSMetaMethod &method) {
898 return method.methodType() == QQmlSA::MethodType::Signal;
903
904
905
906
907
908
909
910void LinterVisitor::checkSignalHandlerNameClashes()
912 static constexpr QLatin1String clashMessage =
913 "Property \"%1\" is interpreted as a signal handler for %2 \"%3\", "
914 "use a different name."_L1;
916 for (
const auto &[scope, name, location] : std::as_const(m_handlerLikeProperties)) {
917 QString changedPropertyName;
919 if (
const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(name)) {
920 changedPropertyName = *propertyName;
921 signalName = *QQmlSignalNames::changedHandlerNameToSignalName(name);
923 signalName = QQmlSignalNames::handlerNameToSignalName(name).value_or(QString());
926 if (signalName.isEmpty())
929 if (hasSignal(scope, signalName)) {
930 m_logger->log(clashMessage.arg(name, s_signal, signalName), qmlSignalHandlerNameClash,
932 }
else if (!changedPropertyName.isEmpty() && scope->hasProperty(changedPropertyName)) {
933 m_logger->log(clashMessage.arg(name, s_property, changedPropertyName),
934 qmlSignalHandlerNameClash, location);
935 }
else if (scope->hasOwnProperty(signalName)) {
936 m_logger->log(clashMessage.arg(name, s_property, signalName), qmlSignalHandlerNameClash,
942void LinterVisitor::handleRenamedType(UiQualifiedId *qualifiedId)
944 m_renamedComponents.handleRenamedType(
945 m_rootScopeImports.type(qualifiedId->name.toString()).scope, qualifiedId->name,
946 qualifiedId->identifierToken, m_logger);
949bool LinterVisitor::
visit(Type *type)
951 const bool result = QQmlJSImportVisitor::visit(type);
953 handleRenamedType(type->typeId);
958void LinterVisitor::handleRecursivelyInstantiatedType(UiQualifiedId *qualifiedId)
961 if (qualifiedId->next)
964 auto logWarning = [&qualifiedId,
this]() {
965 m_logger->log(
"Type \"%1\" can't be instantiated recursively"_L1.arg(qualifiedId->name),
966 qmlTypeInstantiatedRecursively, qualifiedId->identifierToken);
969 const QString name = qualifiedId->name.toString();
970 if (m_rootScopeImports.names().contains(m_exportedRootScope, name))
974 if (
const auto inlineComponentName = std::get_if<InlineComponentNameType>(&m_currentRootName);
975 inlineComponentName && name == *inlineComponentName) {
980bool LinterVisitor::
visit(QQmlJS::AST::UiPragma *pragma)
982 const bool result = QQmlJSImportVisitor::visit(pragma);
985 if (pragma->name == u"Singleton")
986 m_rootIsSingleton =
true;
991void LinterVisitor::checkSingletonRoot()
993 const bool hasQmldirSingletonEntry = m_exportedRootScope->isSingleton();
994 const bool hasSingletonPragma = m_rootIsSingleton;
996 if (hasQmldirSingletonEntry == hasSingletonPragma)
999 if (hasQmldirSingletonEntry && !hasSingletonPragma) {
1000 m_logger->log(
"Type %1 declared as singleton in qmldir but missing pragma Singleton"_L1.arg(
1001 m_exportedRootScope->internalName()),
1002 qmlImport, QQmlJS::SourceLocation());
1005 Q_ASSERT(!hasQmldirSingletonEntry && hasSingletonPragma);
1006 m_logger->log(
"Type %1 not declared as singleton in qmldir but using pragma Singleton"_L1.arg(
1007 m_exportedRootScope->internalName()),
1008 qmlImport, QQmlJS::SourceLocation());
1011bool LinterVisitor::
visit(QQmlJS::AST::UiObjectDefinition *objectDefinition)
1013 handleRenamedType(objectDefinition->qualifiedTypeNameId);
1014 handleRecursivelyInstantiatedType(objectDefinition->qualifiedTypeNameId);
1015 if (!rootScopeIsValid() && !objectDefinition->qualifiedTypeNameId->name.front().isLower())
1016 checkSingletonRoot();
1018 return QQmlJSImportVisitor::visit(objectDefinition);
1021bool LinterVisitor::
visit(UiPublicMember *publicMember)
1023 switch (publicMember->type) {
1024 case UiPublicMember::Signal: {
1025 const QString signalName = publicMember->name.toString();
1026 warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
1030 case QQmlJS::AST::UiPublicMember::Property: {
1031 const QString propertyName = publicMember->name.toString();
1032 OverrideInformations flags;
1033 flags.setFlag(
WithOverride, publicMember->isOverride());
1034 flags.setFlag(
WithFinal, publicMember->isFinal());
1035 warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
1037 if (QQmlSignalNames::isHandlerName(propertyName)) {
1038 m_handlerLikeProperties.append(
1039 { m_currentScope, propertyName, publicMember->identifierToken });
1041 handleRenamedType(publicMember->memberType);
1043 const QString typeName = publicMember->memberType->toString();
1044 if (typeName !=
"alias"_L1) {
1045 if (m_rootScopeImports.hasType(typeName)
1046 && !m_rootScopeImports.type(typeName).scope.isNull()) {
1047 if (m_importTypeLocationMap.contains(typeName))
1048 m_usedTypes.insert(typeName);
1054 return QQmlJSImportVisitor::visit(publicMember);
1057bool LinterVisitor::
visit(FunctionExpression *fexpr)
1059 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
1060 warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
1063 return QQmlJSImportVisitor::visit(fexpr);
1066bool LinterVisitor::
visit(FunctionDeclaration *fdecl)
1068 if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
1069 warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
1072 return QQmlJSImportVisitor::visit(fdecl);
1076
1077
1079 const QQmlJSScope::ConstPtr &scope2)
1081 for (
const auto &[propertyName, prop] : scope1->properties().asKeyValueRange())
1082 if (!scope2->hasProperty(propertyName))
1084 for (
const auto &[methodName, method] : scope1->methods().asKeyValueRange())
1085 if (!scope2->hasMethod(methodName))
1091void LinterVisitor::checkFileSelections()
1093 const QQmlJS::FileSelectorInfo info =
1094 m_rootScopeImports.contextualTypes().fileSelectorInfoFor(m_exportedRootScope);
1096 if (info.fileSelectedTypes.isEmpty() || info.mainType.isNull())
1099 const QString name = m_rootScopeImports.name(m_exportedRootScope);
1101 if (info.mainType == m_exportedRootScope) {
1103 for (
const auto &fileSelected : info.fileSelectedTypes) {
1104 if (compatibilityHeuristicForFileSelector(m_exportedRootScope,
1105 fileSelected.type.scope)) {
1107 "Type %1 is ambiguous due to file selector usage, ignoring %2."_L1.arg(
1108 name, fileSelected.type.scope->filePath()),
1109 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1112 m_logger->log(
"Type %1 has a potentially incompatible file-selected variant %2."_L1.arg(
1113 name, fileSelected.type.scope->filePath()),
1114 qmlImport, m_exportedRootScope->sourceLocation());
1120 if (compatibilityHeuristicForFileSelector(info.mainType, m_exportedRootScope)) {
1122 "File-selected type %1 is ambiguous due to file selector usage, this file will be ignored in favour of %2."_L1
1123 .arg(name, info.mainType->filePath()),
1124 qmlImportFileSelector, m_exportedRootScope->sourceLocation());
1127 m_logger->log(
"File-selected type %1 is potentially incompatible with %2."_L1.arg(
1128 name, info.mainType->filePath()),
1129 qmlImport, m_exportedRootScope->sourceLocation());
1135 if (m_importTypeLocationMap.contains(name)
1136 && m_importTypeLocationMap.values(name).contains(loc)) {
1140 m_importTypeLocationMap.insert(name, loc);
1145 if (!hadWarnings && loc.isValid())
1146 m_importLocations.insert(loc);
1150 const QQmlJS::SourceLocation &loc,
1154 if (isDependency && m_importStaticModuleLocationMap.contains(name))
1156 m_importStaticModuleLocationMap[name] = loc;
1159bool LinterVisitor::
visit(QQmlJS::AST::IdentifierExpression *idexp)
1161 const QString name = idexp->name.toString();
1162 if (m_importTypeLocationMap.contains(name)) {
1163 m_usedTypes.insert(name);
1169void LinterVisitor::
endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember)
1173 const QString name = fieldMember->name.toString();
1174 if (m_importTypeLocationMap.contains(name)) {
1175 const QQmlJSImportedScope type = m_rootScopeImports.type(name);
1176 if (type.scope.isNull()) {
1177 if (m_rootScopeImports.hasType(name))
1178 m_usedTypes.insert(name);
1179 }
else if (!type.scope->attachedTypeName().isEmpty()) {
1180 m_usedTypes.insert(name);
1187 for (
const auto &scope : std::as_const(m_objectDefinitionScopes))
1188 checkGroupedAndAttachedScope(scope);
1191void LinterVisitor::checkGroupedAndAttachedScope(
const QQmlJSScope::ConstPtr &scope)
1195 if (checkCustomParser(scope))
1198 if (!checkTypeResolved(scope))
1201 auto children = scope->childScopes();
1202 while (!children.isEmpty()) {
1203 auto childScope = children.takeFirst();
1204 const auto type = childScope->scopeType();
1206 case QQmlSA::ScopeType::GroupedPropertyScope:
1207 case QQmlSA::ScopeType::AttachedPropertyScope:
1208 if (!childScope->baseType() && !m_unresolvedTypes.hasSeen(childScope)) {
1211 m_logger->log(QStringLiteral(
"unknown %1 property scope %2.")
1212 .arg(type == QQmlSA::ScopeType::GroupedPropertyScope
1213 ? QStringLiteral(
"grouped")
1214 : QStringLiteral(
"attached"),
1215 childScope->internalName()),
1216 qmlUnqualified, childScope->sourceLocation());
1218 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.