Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// Qt-Security score:significant
#include "qqmljslintervisitor_p.h"
#include <private/qqmljsutils_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using namespace QQmlJS::AST;
namespace QQmlJS {
/*!
\internal
\class QQmlJS::LinterVisitor
Extends QQmlJSImportVisitor with extra warnings that are required for linting but unrelated to
QQmlJSImportVisitor actual task that is constructing QQmlJSScopes. One example of such warnings
are purely syntactic checks, or style-checks warnings that don't make sense during compilation.
*/
LinterVisitor::LinterVisitor(
QQmlJSImporter *importer, QQmlJSLogger *logger,
const QString &implicitImportDirectory, const QStringList &qmldirFiles,
QQmlJS::Engine *engine)
: QQmlJSImportVisitor(importer, logger, implicitImportDirectory, qmldirFiles)
, m_engine(engine)
{
}
void LinterVisitor::leaveEnvironment()
{
const auto leaveEnv = qScopeGuard([this] { QQmlJSImportVisitor::leaveEnvironment(); });
if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
return;
if (auto base = m_currentScope->baseType()) {
if (base->internalName() == u"QQmlComponent"_s) {
const auto nChildren = std::count_if(
m_currentScope->childScopesBegin(), m_currentScope->childScopesEnd(),
[](const QQmlJSScope::ConstPtr &scope) {
return scope->scopeType() == QQmlSA::ScopeType::QMLScope;
});
if (nChildren != 1) {
m_logger->log("Components must have exactly one child"_L1,
qmlComponentChildrenCount, m_currentScope->sourceLocation());
}
}
}
}
bool LinterVisitor::visit(StringLiteral *sl)
{
QQmlJSImportVisitor::visit(sl);
const QString s = m_logger->code().mid(sl->literalToken.begin(), sl->literalToken.length);
if (s.contains(QLatin1Char('\r')) || s.contains(QLatin1Char('\n')) || s.contains(QChar(0x2028u))
|| s.contains(QChar(0x2029u))) {
QString templateString;
bool escaped = false;
const QChar stringQuote = s[0];
for (qsizetype i = 1; i < s.size() - 1; i++) {
const QChar c = s[i];
if (c == u'\\') {
escaped = !escaped;
} else if (escaped) {
// If we encounter an escaped quote, unescape it since we use backticks here
if (c == stringQuote)
templateString.chop(1);
escaped = false;
} else {
if (c == u'`')
templateString += u'\\';
if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{')
templateString += u'\\';
}
templateString += c;
}
QQmlJSDocumentEdit documentEdit{
m_logger->filePath(), sl->literalToken, u"`" % templateString % u"`"
};
QQmlJSFixSuggestion suggestion = { "Use a template literal instead."_L1, sl->literalToken,
documentEdit };
suggestion.setAutoApplicable();
m_logger->log(QStringLiteral("String contains unescaped line terminator which is "
"deprecated."),
qmlMultilineStrings, sl->literalToken, true, true, suggestion);
}
return true;
}
bool LinterVisitor::preVisit(Node *n)
{
m_ancestryIncludingCurrentNode.push_back(n);
return true;
}
void LinterVisitor::postVisit(Node *n)
{
Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n);
m_ancestryIncludingCurrentNode.pop_back();
}
Node *LinterVisitor::astParentOfVisitedNode() const
{
if (m_ancestryIncludingCurrentNode.size() < 2)
return nullptr;
return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2];
}
bool LinterVisitor::visit(CommaExpression *expression)
{
QQmlJSImportVisitor::visit(expression);
if (!expression->left || !expression->right)
return true;
// don't warn about commas in "for" statements
if (cast<ForStatement *>(astParentOfVisitedNode()))
return true;
m_logger->log("Do not use comma expressions."_L1, qmlComma, expression->commaToken);
return true;
}
static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger)
{
static constexpr std::array literals{ "Boolean"_L1, "Function"_L1, "JSON"_L1,
"Math"_L1, "Number"_L1, "String"_L1 };
const IdentifierExpression *identifier = cast<IdentifierExpression *>(expression->base);
if (!identifier)
return;
if (std::find(literals.cbegin(), literals.cend(), identifier->name) != literals.cend()) {
logger->log("Do not use '%1' as a constructor."_L1.arg(identifier->name),
qmlLiteralConstructor, identifier->identifierToken);
}
if (identifier->name == "Array"_L1 && expression->arguments && expression->arguments->next) {
const auto fullRange = combine(expression->newToken, expression->rparenToken);
const QList<QQmlJSDocumentEdit> edits = {
{ logger->filePath(), combine(expression->newToken, expression->lparenToken), "["_L1 },
{ logger->filePath(), expression->rparenToken, "]"_L1 },
};
QQmlJSFixSuggestion fix("Replace with array literal"_L1, fullRange, edits);
fix.setAutoApplicable(true);
logger->log("Array has confusing semantics, use an array literal ([]) instead."_L1,
qmlLiteralConstructor, identifier->identifierToken, true, true, fix);
}
}
bool LinterVisitor::visit(NewMemberExpression *expression)
{
QQmlJSImportVisitor::visit(expression);
warnAboutLiteralConstructors(expression, m_logger);
return true;
}
bool LinterVisitor::visit(VoidExpression *ast)
{
QQmlJSImportVisitor::visit(ast);
m_logger->log("Do not use void expressions."_L1, qmlVoid, ast->voidToken);
return true;
}
static SourceLocation confusingPluses(BinaryExpression *exp)
{
Q_ASSERT(exp->op == QSOperator::Add);
SourceLocation location = exp->operatorToken;
// a++ + b
if (auto increment = cast<PostIncrementExpression *>(exp->left))
location = combine(increment->incrementToken, location);
// a + +b
if (auto unary = cast<UnaryPlusExpression *>(exp->right))
location = combine(location, unary->plusToken);
// a + ++b
if (auto increment = cast<PreIncrementExpression *>(exp->right))
location = combine(location, increment->incrementToken);
if (location == exp->operatorToken)
return SourceLocation{};
return location;
}
static SourceLocation confusingMinuses(BinaryExpression *exp)
{
Q_ASSERT(exp->op == QSOperator::Sub);
SourceLocation location = exp->operatorToken;
// a-- - b
if (auto decrement = cast<PostDecrementExpression *>(exp->left))
location = combine(decrement->decrementToken, location);
// a - -b
if (auto unary = cast<UnaryMinusExpression *>(exp->right))
location = combine(location, unary->minusToken);
// a - --b
if (auto decrement = cast<PreDecrementExpression *>(exp->right))
location = combine(location, decrement->decrementToken);
if (location == exp->operatorToken)
return SourceLocation{};
return location;
}
bool LinterVisitor::visit(BinaryExpression *exp)
{
QQmlJSImportVisitor::visit(exp);
switch (exp->op) {
case QSOperator::Add:
if (SourceLocation loc = confusingPluses(exp); loc.isValid())
m_logger->log("Confusing pluses."_L1, qmlConfusingPluses, loc);
break;
case QSOperator::Sub:
if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
m_logger->log("Confusing minuses."_L1, qmlConfusingMinuses, loc);
break;
default:
break;
}
return true;
}
bool LinterVisitor::visit(QQmlJS::AST::UiImport *import)
{
QQmlJSImportVisitor::visit(import);
const auto locAndName = [](const UiImport *i) {
if (!i->importUri)
return std::make_pair(i->fileNameToken, i->fileName.toString());
QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
if (i->importIdToken.isValid())
l = combine(l, i->importIdToken);
else if (i->version)
l = combine(l, i->version->minorToken);
else
l = combine(l, i->importUri->lastSourceLocation());
return std::make_pair(l, i->importUri->toString());
};
SeenImport i(import);
if (const auto it = m_seenImports.constFind(i); it != m_seenImports.constEnd()) {
const auto locAndNameImport = locAndName(import);
const auto locAndNameSeen = locAndName(it->uiImport);
m_logger->log("Duplicate import '%1'"_L1.arg(locAndNameImport.second),
qmlDuplicateImport, locAndNameImport.first);
m_logger->log("Note: previous import '%1' here"_L1.arg(locAndNameSeen.second),
qmlDuplicateImport, locAndNameSeen.first, true, true, {},
locAndName(import).first.startLine);
}
m_seenImports.insert(i);
return true;
}
void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
const QQmlJS::SourceLocation &location)
{
m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
location);
for (const auto *member = members; member; member = member->next) {
if (member->member.toString() == key) {
m_logger->log(u"Note: previous declaration of '%1' here"_s.arg(key),
qmlDuplicateEnumEntries, member->memberToken);
return;
}
}
}
bool LinterVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied)
{
QQmlJSImportVisitor::visit(uied);
if (m_currentScope->isInlineComponent()) {
m_logger->log(u"Enums declared inside of inline component are ignored."_s,
qmlInlineComponentEnums, uied->firstSourceLocation());
} else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No
&& !m_currentScope->isFileRootComponent()) {
m_logger->log(u"Enum declared outside the root element. It won't be accessible."_s,
qmlNonRootEnums, uied->firstSourceLocation());
}
QHash<QStringView, const QQmlJS::AST::UiEnumMemberList *> seen;
for (const auto *member = uied->members; member; member = member->next) {
QStringView key = member->member;
if (!key.front().isUpper()) {
m_logger->log(u"Enum keys should start with an uppercase."_s, qmlEnumKeyCase,
member->memberToken);
}
if (seen.contains(key))
handleDuplicateEnums(uied->members, key, member->memberToken);
else
seen[member->member] = member;
if (uied->name == key) {
m_logger->log("Enum entry should be named differently than the enum itself to avoid "
"confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
}
}
return true;
}
static bool allCodePathsReturnInsideCase(Node *statement)
{
using namespace AST;
if (!statement)
return false;
switch (statement->kind) {
case Node::Kind_Block: {
return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
}
case Node::Kind_BreakStatement:
return true;
case Node::Kind_CaseBlock: {
const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
if (caseBlock->defaultClause)
return allCodePathsReturnInsideCase(caseBlock->defaultClause);
return allCodePathsReturnInsideCase(caseBlock->clauses);
}
case Node::Kind_CaseClause:
return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
case Node::Kind_CaseClauses: {
for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
caseClauses = caseClauses->next) {
if (!allCodePathsReturnInsideCase(caseClauses->clause))
return false;
}
return true;
}
case Node::Kind_ContinueStatement:
// allCodePathsReturn() doesn't recurse into loops, so any encountered `continue` should
// belong to a loop outside the switch statement.
return true;
case Node::Kind_DefaultClause:
return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
case Node::Kind_IfStatement: {
const auto *ifStatement = cast<IfStatement *>(statement);
return allCodePathsReturnInsideCase(ifStatement->ok)
&& allCodePathsReturnInsideCase(ifStatement->ko);
}
case Node::Kind_LabelledStatement:
return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
case Node::Kind_ReturnStatement:
return true;
case Node::Kind_StatementList: {
for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
if (allCodePathsReturnInsideCase(list->statement))
return true;
}
return false;
}
case Node::Kind_SwitchStatement:
return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
case Node::Kind_ThrowStatement:
return true;
case Node::Kind_TryStatement: {
auto *tryStatement = cast<TryStatement *>(statement);
if (allCodePathsReturnInsideCase(tryStatement->statement))
return true;
return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
}
case Node::Kind_WithStatement:
return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
default:
break;
}
return false;
}
void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
SourceLocation nextLoc)
{
if (!statements || !nextLoc.isValid())
return;
if (allCodePathsReturnInsideCase(statements))
return;
quint32 afterLastStatement = 0;
for (StatementList *it = statements; it; it = it->next) {
if (!it->next) {
afterLastStatement = it->statement->lastSourceLocation().end();
}
}
const auto &comments = m_engine->comments();
auto it = std::find_if(comments.cbegin(), comments.cend(),
[&](auto c) { return afterLastStatement < c.offset; });
auto end = std::find_if(it, comments.cend(),
[&](auto c) { return c.offset >= nextLoc.offset; });
for (; it != end; ++it) {
const QString &commentText = m_engine->code().mid(it->offset, it->length);
if (commentText.contains("fall through"_L1, Qt::CaseInsensitive)
|| commentText.contains("fall-through"_L1, Qt::CaseInsensitive)
|| commentText.contains("fallthrough"_L1, Qt::CaseInsensitive)) {
return;
}
}
m_logger->log(
"Non-empty case block potentially falls through to the next case or default statement. "
"Add \"// fallthrough\" at the end of the block to silence this warning."_L1,
qmlUnterminatedCase, errorLoc);
}
bool LinterVisitor::visit(QQmlJS::AST::CaseBlock *block)
{
QQmlJSImportVisitor::visit(block);
std::vector<std::pair<SourceLocation, StatementList *>> clauses;
for (CaseClauses *it = block->clauses; it; it = it->next)
clauses.push_back({ it->clause->caseToken, it->clause->statements });
if (block->defaultClause)
clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
for (CaseClauses *it = block->moreClauses; it; it = it->next)
clauses.push_back({ it->clause->caseToken, it->clause->statements });
// check all but the last clause for fallthrough
for (size_t i = 0; i < clauses.size() - 1; ++i) {
const SourceLocation nextToken = clauses[i + 1].first;
checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
}
return true;
}
static QList<const Statement *> possibleLastStatements(const StatementList *ast);
static QList<const Statement *> possibleLastStatements(const CaseBlock *ast)
{
QList<const Statement *> lasts;
for (const auto *clause = ast->clauses; clause; clause = clause->next)
lasts << possibleLastStatements(clause->clause->statements);
if (ast->defaultClause)
lasts << possibleLastStatements(ast->defaultClause->statements);
for (const auto *clause = ast->moreClauses; clause; clause = clause->next)
lasts << possibleLastStatements(clause->clause->statements);
return lasts;
}
static QList<const Statement *> possibleLastStatements(const Statement *ast)
{
if (const auto *s = cast<const Block *>(ast))
return possibleLastStatements(s->statements) << s;
if (const auto *s = cast<const BreakStatement *>(ast))
return { s };
if (const auto *s = cast<const ContinueStatement *>(ast))
return { s };
if (const auto *s = cast<const DebuggerStatement *>(ast))
return { s };
if (const auto *s = cast<const DoWhileStatement *>(ast))
return possibleLastStatements(s->statement) << s;
if (const auto *s = cast<const EmptyStatement *>(ast))
return { s };
if (const auto *s = cast<const ExportDeclaration *>(ast))
return { s };
if (const auto *s = cast<const ExpressionStatement *>(ast))
return { s };
if (const auto *s = cast<const ForEachStatement *>(ast))
return possibleLastStatements(s->statement) << s;
if (const auto *s = cast<const ForStatement *>(ast))
return possibleLastStatements(s->statement) << s;
if (const auto *s = cast<const IfStatement *>(ast)) {
auto lasts = possibleLastStatements(s->ok);
if (s->ko)
lasts << possibleLastStatements(s->ko);
return lasts << s;
}
if (const auto *s = cast<const ImportDeclaration *>(ast))
return { s };
if (const auto *s = cast<const LabelledStatement *>(ast))
return possibleLastStatements(s->statement) << ast;
if (const auto *s = cast<const ReturnStatement *>(ast))
return { s };
if (const auto *s = cast<const SwitchStatement *>(ast))
return possibleLastStatements(s->block) << s;
if (const auto *s = cast<const ThrowStatement *>(ast))
return { s };
if (const auto *s = cast<const TryStatement *>(ast))
return { s };
if (const auto *s = cast<const VariableStatement *>(ast))
return { s };
if (const auto *s = cast<const WhileStatement *>(ast))
return possibleLastStatements(s->statement) << s;
if (const auto *s = cast<const WithStatement *>(ast))
return possibleLastStatements(s->statement) << s;
Q_UNREACHABLE_RETURN({});
}
static QList<const Statement *> possibleLastStatements(const StatementList *ast)
{
if (!ast)
return {};
for (; ast->next; ast = ast->next) { }
const auto *statement = ast->statement;
// Can't store FunctionDeclaration as a statement. See StatementList.
if (cast<const FunctionDeclaration *>(statement))
return {};
return possibleLastStatements(static_cast<const Statement *>(statement));
}
static bool isUselessExpressionStatement_impl(const ExpressionNode *ast)
{
switch (ast->kind) {
case Node::Kind_CallExpression:
case Node::Kind_DeleteExpression:
case Node::Kind_NewExpression:
case Node::Kind_PreDecrementExpression:
case Node::Kind_PreIncrementExpression:
case Node::Kind_PostDecrementExpression:
case Node::Kind_PostIncrementExpression:
case Node::Kind_YieldExpression:
case Node::Kind_FunctionExpression:
return false;
case Node::Kind_NumericLiteral:
case Node::Kind_StringLiteral:
case Node::Kind_FalseLiteral:
case Node::Kind_TrueLiteral:
case Node::Kind_NullExpression:
case Node::Kind_Undefined:
case Node::Kind_RegExpLiteral:
case Node::Kind_SuperLiteral:
case Node::Kind_ThisExpression:
case Node::Kind_FieldMemberExpression:
case Node::Kind_IdentifierExpression:
case Node::Kind_TypeOfExpression:
return true;
default:
break;
}
if (const auto *e = cast<const NestedExpression *>(ast))
return isUselessExpressionStatement_impl(e->expression);
if (const auto *e = cast<const NotExpression *>(ast))
return isUselessExpressionStatement_impl(e->expression);
if (const auto *e = cast<const TildeExpression *>(ast))
return isUselessExpressionStatement_impl(e->expression);
if (const auto *e = cast<const UnaryMinusExpression *>(ast))
return isUselessExpressionStatement_impl(e->expression);
if (const auto *e = cast<const UnaryPlusExpression *>(ast))
return isUselessExpressionStatement_impl(e->expression);
if (const auto *e = cast<const ConditionalExpression *>(ast))
return isUselessExpressionStatement_impl(e->ok) && isUselessExpressionStatement_impl(e->ko);
if (const BinaryExpression *binary = cast<const BinaryExpression *>(ast)) {
switch (binary->op) {
case QSOperator::InplaceAnd:
case QSOperator::Assign:
case QSOperator::InplaceSub:
case QSOperator::InplaceDiv:
case QSOperator::InplaceExp:
case QSOperator::InplaceAdd:
case QSOperator::InplaceLeftShift:
case QSOperator::InplaceMod:
case QSOperator::InplaceMul:
case QSOperator::InplaceOr:
case QSOperator::InplaceRightShift:
case QSOperator::InplaceURightShift:
case QSOperator::InplaceXor:
return false;
default:
return isUselessExpressionStatement_impl(binary->left)
&& isUselessExpressionStatement_impl(binary->right);
}
}
return false;
}
/*!
\internal
This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
*/
static bool isUselessExpressionStatement(const ExpressionStatement *ast)
{
return isUselessExpressionStatement_impl(ast->expression);
}
void LinterVisitor::handleUselessExpressionStatement(const ExpressionStatement *ast)
{
// property binding, signal handler, or function declaration
const auto it = std::find_if(m_ancestryIncludingCurrentNode.crbegin(),
m_ancestryIncludingCurrentNode.crend(),
[](auto it) {
return it->kind == Node::Kind_UiPublicMember
|| it->kind == Node::Kind_FunctionDeclaration
|| it->kind == Node::Kind_UiScriptBinding;
});
if (it == m_ancestryIncludingCurrentNode.crend())
return;
// A useless ExpressionStatement in *last position* inside a property binding is not useless
const auto isLastExprStat = [](const ExpressionStatement *statement, const Statement *base) {
const auto lasts = possibleLastStatements(base);
return lasts.contains(statement);
};
if (const auto *usb = cast<UiScriptBinding *>(*it); usb && usb->qualifiedId) {
if (usb->qualifiedId->toString() == "id"_L1)
return;
if (usb->qualifiedId->next)
return; // group/attached property, give up
if (m_savedBindingOuterScope->scopeType() == QQmlSA::ScopeType::GroupedPropertyScope)
return; // grouped property, give up
QQmlJSScope::Ptr object = m_currentScope;
while (object && object->scopeType() != QQmlSA::ScopeType::QMLScope)
object = object->parentScope();
if (!object)
return;
if (m_propertyBindings.contains(object)) {
for (const auto &entry : m_propertyBindings[object]) {
if (entry.data == usb->qualifiedId->toString()) {
if (isLastExprStat(ast, usb->statement))
return;
else
break;
}
}
}
}
const auto *upm = cast<const UiPublicMember *>(*it);
if (upm && upm->type == AST::UiPublicMember::Property && upm->statement) {
if (isLastExprStat(ast, upm->statement))
return;
}
if (isUselessExpressionStatement(ast)) {
m_logger->log("Expression statement has no obvious effect."_L1,
qmlConfusingExpressionStatement,
combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
}
}
bool LinterVisitor::visit(ExpressionStatement *ast)
{
QQmlJSImportVisitor::visit(ast);
handleUselessExpressionStatement(ast);
return true;
}
bool LinterVisitor::safeInsertJSIdentifier(QQmlJSScope::Ptr &scope, const QString &name, const QQmlJSScope::JavaScriptIdentifier &identifier)
{
if (scope->scopeType() == QQmlSA::ScopeType::JSLexicalScope &&
identifier.kind == QQmlJSScope::JavaScriptIdentifier::FunctionScoped) {
// var is generally not great, but we don't want to emit this warning if you
// are in the single, toplevel block of a binding
Q_ASSERT(!scope->parentScope().isNull()); // lexical scope should always have a parent
auto parentScopeType = scope->parentScope()->scopeType();
bool inTopLevelBindingBlockScope = parentScopeType == QQmlSA::ScopeType::BindingFunctionScope
|| parentScopeType == QQmlSA::ScopeType::SignalHandlerFunctionScope;
if (!inTopLevelBindingBlockScope) {
m_logger->log(u"var declaration in block scope is hoisted to function scope\n"_s
u"Replace it with const or let to silence the warning\n"_s,
qmlBlockScopeVarDeclaration, identifier.location);
}
} else if (scope->scopeType() == QQmlSA::ScopeType::QMLScope) {
const QQmlJSScope *scopePtr = scope.get();
std::pair<const QQmlJSScope*, QString> misplaced { scopePtr, name };
if (misplacedJSIdentifiers.contains(misplaced))
return false; // we only want to warn once
misplacedJSIdentifiers.insert(misplaced);
m_logger->log(u"JavaScript declarations are not allowed in QML elements"_s, qmlSyntax,
identifier.location);
return false;
}
return QQmlJSImportVisitor::safeInsertJSIdentifier(scope, name, identifier);
}
QQmlJSImportVisitor::BindingExpressionParseResult LinterVisitor::parseBindingExpression(
const QString &name, const QQmlJS::AST::Statement *statement,
const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
{
if (statement && statement->kind == (int)AST::Node::Kind::Kind_Block) {
const auto *block = static_cast<const AST::Block *>(statement);
if (!block->statements && associatedPropertyDefinition) {
m_logger->log("Unintentional empty block, use ({}) for empty object literal"_L1,
qmlUnintentionalEmptyBlock,
combine(block->lbraceToken, block->rbraceToken));
}
}
return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
}
void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding,
const UiPublicMember *associatedPropertyDefinition)
{
if (!m_currentScope->hasOwnProperty(binding.propertyName()))
return;
if (!associatedPropertyDefinition->isReadonly())
return;
const auto &prop = m_currentScope->property(binding.propertyName());
const auto log = [&](const QString &preferredType) {
m_logger->log("Prefer more specific type %1 over var"_L1.arg(preferredType),
qmlPreferNonVarProperties, prop.sourceLocation());
};
if (prop.typeName() != "QVariant"_L1)
return;
switch (binding.bindingType()) {
case QQmlSA::BindingType::BoolLiteral: {
log("bool"_L1);
break;
}
case QQmlSA::BindingType::NumberLiteral: {
double v = binding.numberValue();
auto loc = binding.sourceLocation();
QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
if (literal.contains(u'.') || double(int(v)) != v)
log("real or double"_L1);
else
log("int"_L1);
break;
}
case QQmlSA::BindingType::StringLiteral: {
log("string"_L1);
break;
}
default: {
break;
}
}
}
bool LinterVisitor::visit(UiProgram *ast)
{
const bool result = QQmlJSImportVisitor::visit(ast);
m_renamedComponents.setScopeToName(&m_rootScopeImports.names());
return result;
}
void LinterVisitor::checkUnusedImports()
{
auto unusedImports = m_importLocations;
for (const QString &type : std::as_const(m_usedTypes)) {
const auto &importLocations = m_importTypeLocationMap.values(type);
for (const auto &importLocation : importLocations)
unusedImports.remove(importLocation);
// If there are no more unused imports left we can abort early
if (unusedImports.isEmpty())
break;
}
const auto &imports = m_importStaticModuleLocationMap.values();
for (const QQmlJS::SourceLocation &import : imports)
unusedImports.remove(import);
for (const auto &import : unusedImports) {
m_logger->log(QString::fromLatin1("Unused import"), qmlUnusedImports, import);
}
}
void LinterVisitor::endVisit(UiProgram *ast)
{
QQmlJSImportVisitor::endVisit(ast);
checkFileSelections();
checkUnusedImports();
checkSignalHandlerNameClashes();
}
static constexpr QLatin1String s_method = "method"_L1;
static constexpr QLatin1String s_signal = "signal"_L1;
static constexpr QLatin1String s_property = "property"_L1;
enum OverrideInformation { WithoutOverride = 0, WithFinal = 1, WithOverride = 2 };
Q_DECLARE_FLAGS(OverrideInformations, OverrideInformation);
static void warnForMethodShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
const QQmlJS::SourceLocation &location,
QQmlJSLogger *logger)
{
Q_ASSERT(base);
if (!base->hasMethod(name))
return;
static constexpr QLatin1String warningMessage =
"%1 \"%2\" already exists in base type \"%3\", use a different name."_L1;
const auto owner = QQmlJSScope::ownerOfMethod(base, name).scope;
const bool isSignal = owner->methods(name).front().methodType() == QQmlJSMetaMethodType::Signal;
logger->log(warningMessage.arg(isSignal ? "Signal"_L1 : "Method"_L1, name,
QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
qmlShadow, location);
}
static void warnForPropertyShadowingInBase(const QQmlJSScope::ConstPtr &base, const QString &name,
const QQmlJS::SourceLocation &location,
OverrideInformations overrideFlags, QQmlJSLogger *logger)
{
Q_ASSERT(base);
const bool hasOverride = overrideFlags.testFlag(WithOverride);
if (!base->hasProperty(name)) {
if (!hasOverride)
return;
logger->log(
"Member \"%1\" does not override anything. Consider removing \"override\"."_L1.arg(
name),
qmlPropertyOverride, location);
return;
}
const auto owner = QQmlJSScope::ownerOfProperty(base, name).scope;
const auto shadowedProperty = owner->ownProperty(name);
if (shadowedProperty.isFinal()) {
logger->log(
(!hasOverride
? "Member \"%1\" shadows final member \"%1\" from base type \"%2\", use a different name."_L1
: "Member \"%1\" overrides final member \"%1\" from base type \"%2\", use a different name and remove the \"override\"."_L1)
.arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
qmlPropertyOverride, location);
return;
}
if (shadowedProperty.isVirtual() || shadowedProperty.isOverride()) {
if (hasOverride || overrideFlags.testFlag(WithFinal))
return;
logger->log(
"Member \"%1\" shadows member \"%1\" from base type \"%2\", use a different name or add a final or override specifier."_L1
.arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
qmlPropertyOverride, location);
return;
}
if (hasOverride) {
logger->log(
"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
.arg(name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
qmlPropertyOverride, location);
return;
}
logger->log("Property \"%2\" already exists in base type \"%3\", use a different name."_L1.arg(
name, QQmlJSUtils::getScopeName(owner, QQmlSA::ScopeType::QMLScope)),
qmlPropertyOverride, location);
}
static void warnForDuplicates(const QQmlJSScope::ConstPtr &scope, const QString &name,
QLatin1String type, const QQmlJS::SourceLocation &location,
OverrideInformations overrideFlags, QQmlJSLogger *logger)
{
static constexpr QLatin1String duplicateMessage =
"Duplicated %1 name \"%2\", \"%2\" is already a %3."_L1;
if (const auto methods = scope->ownMethods(name); !methods.isEmpty()) {
logger->log(duplicateMessage.arg(type, name,
methods.front().methodType() == QQmlSA::MethodType::Signal
? s_signal
: s_method),
qmlDuplicatedName, location);
}
if (scope->hasOwnProperty(name))
logger->log(duplicateMessage.arg(type, name, s_property), qmlDuplicatedName, location);
const QQmlJSScope::ConstPtr base = scope->baseType();
if (!base)
return;
warnForMethodShadowingInBase(base, name, location, logger);
warnForPropertyShadowingInBase(base, name, location, overrideFlags, logger);
}
static bool hasSignal(const QQmlJSScope::ConstPtr &scope, const QString &name)
{
const auto methods = scope->methods(name);
return std::any_of(methods.cbegin(), methods.cend(), [](const QQmlJSMetaMethod &method) {
return method.methodType() == QQmlSA::MethodType::Signal;
});
}
/*!
\internal
Warns about properties whose names are also signal handler names for another member of the same
object. The QML engine gives the signal handler interpretation precedence, so such properties can
never be bound: a literal initializer is an error and an expression initializer is run as a signal
handler instead. This mirrors SignalHandlerResolver, which accepts signals no matter which type
declares them, but plain properties only from the object the binding appears in.
*/
void LinterVisitor::checkSignalHandlerNameClashes()
{
static constexpr QLatin1String clashMessage =
"Property \"%1\" is interpreted as a signal handler for %2 \"%3\", "
"use a different name."_L1;
for (const auto &[scope, name, location] : std::as_const(m_handlerLikeProperties)) {
QString changedPropertyName;
QString signalName;
if (const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(name)) {
changedPropertyName = *propertyName;
signalName = *QQmlSignalNames::changedHandlerNameToSignalName(name);
} else {
signalName = QQmlSignalNames::handlerNameToSignalName(name).value_or(QString());
}
if (signalName.isEmpty())
continue;
if (hasSignal(scope, signalName)) {
m_logger->log(clashMessage.arg(name, s_signal, signalName), qmlSignalHandlerNameClash,
location);
} else if (!changedPropertyName.isEmpty() && scope->hasProperty(changedPropertyName)) {
m_logger->log(clashMessage.arg(name, s_property, changedPropertyName),
qmlSignalHandlerNameClash, location);
} else if (scope->hasOwnProperty(signalName)) {
m_logger->log(clashMessage.arg(name, s_property, signalName), qmlSignalHandlerNameClash,
location);
}
}
}
void LinterVisitor::handleRenamedType(UiQualifiedId *qualifiedId)
{
m_renamedComponents.handleRenamedType(
m_rootScopeImports.type(qualifiedId->name.toString()).scope, qualifiedId->name,
qualifiedId->identifierToken, m_logger);
}
bool LinterVisitor::visit(Type *type)
{
const bool result = QQmlJSImportVisitor::visit(type);
handleRenamedType(type->typeId);
return result;
}
void LinterVisitor::handleRecursivelyInstantiatedType(UiQualifiedId *qualifiedId)
{
// It should be ok to reference inline components or enums inside of the current file
if (qualifiedId->next)
return;
auto logWarning = [&qualifiedId, this]() {
m_logger->log("Type \"%1\" can't be instantiated recursively"_L1.arg(qualifiedId->name),
qmlTypeInstantiatedRecursively, qualifiedId->identifierToken);
};
const QString name = qualifiedId->name.toString();
if (m_rootScopeImports.names().contains(m_exportedRootScope, name))
logWarning();
// note: inline components can't be renamed via qmldir entries
if (const auto inlineComponentName = std::get_if<InlineComponentNameType>(&m_currentRootName);
inlineComponentName && name == *inlineComponentName) {
logWarning();
}
}
bool LinterVisitor::visit(QQmlJS::AST::UiPragma *pragma)
{
const bool result = QQmlJSImportVisitor::visit(pragma);
// The QML Engine ignores this pragma, so __don't__ set m_exportedRootScope's singleton flag with its value.
if (pragma->name == u"Singleton")
m_rootIsSingleton = true;
return result;
}
void LinterVisitor::checkSingletonRoot()
{
const bool hasQmldirSingletonEntry = m_exportedRootScope->isSingleton(); // set by importer
const bool hasSingletonPragma = m_rootIsSingleton;
if (hasQmldirSingletonEntry == hasSingletonPragma)
return;
if (hasQmldirSingletonEntry && !hasSingletonPragma) {
m_logger->log("Type %1 declared as singleton in qmldir but missing pragma Singleton"_L1.arg(
m_exportedRootScope->internalName()),
qmlImport, QQmlJS::SourceLocation());
return;
}
Q_ASSERT(!hasQmldirSingletonEntry && hasSingletonPragma);
m_logger->log("Type %1 not declared as singleton in qmldir but using pragma Singleton"_L1.arg(
m_exportedRootScope->internalName()),
qmlImport, QQmlJS::SourceLocation());
}
bool LinterVisitor::visit(QQmlJS::AST::UiObjectDefinition *objectDefinition)
{
handleRenamedType(objectDefinition->qualifiedTypeNameId);
handleRecursivelyInstantiatedType(objectDefinition->qualifiedTypeNameId);
if (!rootScopeIsValid() && !objectDefinition->qualifiedTypeNameId->name.front().isLower())
checkSingletonRoot();
return QQmlJSImportVisitor::visit(objectDefinition);
}
bool LinterVisitor::visit(UiPublicMember *publicMember)
{
switch (publicMember->type) {
case UiPublicMember::Signal: {
const QString signalName = publicMember->name.toString();
warnForDuplicates(m_currentScope, signalName, s_signal, publicMember->identifierToken,
WithoutOverride, m_logger);
break;
}
case QQmlJS::AST::UiPublicMember::Property: {
const QString propertyName = publicMember->name.toString();
OverrideInformations flags;
flags.setFlag(WithOverride, publicMember->isOverride());
flags.setFlag(WithFinal, publicMember->isFinal());
warnForDuplicates(m_currentScope, propertyName, s_property, publicMember->identifierToken,
flags, m_logger);
if (QQmlSignalNames::isHandlerName(propertyName)) {
m_handlerLikeProperties.append(
{ m_currentScope, propertyName, publicMember->identifierToken });
}
handleRenamedType(publicMember->memberType);
const QString typeName = publicMember->memberType->toString();
if (typeName != "alias"_L1) {
if (m_rootScopeImports.hasType(typeName)
&& !m_rootScopeImports.type(typeName).scope.isNull()) {
if (m_importTypeLocationMap.contains(typeName))
m_usedTypes.insert(typeName);
}
}
break;
}
}
return QQmlJSImportVisitor::visit(publicMember);
}
bool LinterVisitor::visit(FunctionExpression *fexpr)
{
if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
warnForDuplicates(m_currentScope, fexpr->name.toString(), s_method, fexpr->identifierToken,
WithoutOverride, m_logger);
}
return QQmlJSImportVisitor::visit(fexpr);
}
bool LinterVisitor::visit(FunctionDeclaration *fdecl)
{
if (m_currentScope->scopeType() == QQmlSA::ScopeType::QMLScope) {
warnForDuplicates(m_currentScope, fdecl->name.toString(), s_method, fdecl->identifierToken,
WithoutOverride, m_logger);
}
return QQmlJSImportVisitor::visit(fdecl);
}
/* This is a _rough_ heuristic; only meant for qmllint to avoid warnings about common constructs.
We might want to improve it in the future if it causes issues
*/
static bool compatibilityHeuristicForFileSelector(const QQmlJSScope::ConstPtr &scope1,
const QQmlJSScope::ConstPtr &scope2)
{
for (const auto &[propertyName, prop] : scope1->properties().asKeyValueRange())
if (!scope2->hasProperty(propertyName))
return false;
for (const auto &[methodName, method] : scope1->methods().asKeyValueRange())
if (!scope2->hasMethod(methodName))
return false;
return true;
}
// heuristic to check file selected files for "compability" to the unselected file.
void LinterVisitor::checkFileSelections()
{
const QQmlJS::FileSelectorInfo info =
m_rootScopeImports.contextualTypes().fileSelectorInfoFor(m_exportedRootScope);
if (info.fileSelectedTypes.isEmpty() || info.mainType.isNull())
return;
const QString name = m_rootScopeImports.name(m_exportedRootScope);
if (info.mainType == m_exportedRootScope) {
// current has fileselectors -> check all fileselectors for compatiblity
for (const auto &fileSelected : info.fileSelectedTypes) {
if (compatibilityHeuristicForFileSelector(m_exportedRootScope,
fileSelected.type.scope)) {
m_logger->log(
"Type %1 is ambiguous due to file selector usage, ignoring %2."_L1.arg(
name, fileSelected.type.scope->filePath()),
qmlImportFileSelector, m_exportedRootScope->sourceLocation());
continue;
}
m_logger->log("Type %1 has a potentially incompatible file-selected variant %2."_L1.arg(
name, fileSelected.type.scope->filePath()),
qmlImport, m_exportedRootScope->sourceLocation());
}
return;
}
// current is fileselected -> only check against "main" type for compatibility
if (compatibilityHeuristicForFileSelector(info.mainType, m_exportedRootScope)) {
m_logger->log(
"File-selected type %1 is ambiguous due to file selector usage, this file will be ignored in favour of %2."_L1
.arg(name, info.mainType->filePath()),
qmlImportFileSelector, m_exportedRootScope->sourceLocation());
return;
}
m_logger->log("File-selected type %1 is potentially incompatible with %2."_L1.arg(
name, info.mainType->filePath()),
qmlImport, m_exportedRootScope->sourceLocation());
}
void LinterVisitor::addImportWithLocation(const QString &name, const QQmlJS::SourceLocation &loc,
bool hadWarnings)
{
if (m_importTypeLocationMap.contains(name)
&& m_importTypeLocationMap.values(name).contains(loc)) {
return;
}
m_importTypeLocationMap.insert(name, loc);
// If the import had warnings it may be "unused" because we haven't found all of its types.
// If the type's location is not valid it's a builtin.
// We don't need to complain about those being unused.
if (!hadWarnings && loc.isValid())
m_importLocations.insert(loc);
}
void LinterVisitor::addStaticImportWithLocation(const QString &name,
const QQmlJS::SourceLocation &loc,
bool isDependency)
{
// Always prefer a direct import of static module to it being imported as a dependency
if (isDependency && m_importStaticModuleLocationMap.contains(name))
return;
m_importStaticModuleLocationMap[name] = loc;
}
bool LinterVisitor::visit(QQmlJS::AST::IdentifierExpression *idexp)
{
const QString name = idexp->name.toString();
if (m_importTypeLocationMap.contains(name)) {
m_usedTypes.insert(name);
}
return true;
}
void LinterVisitor::endVisit(QQmlJS::AST::FieldMemberExpression *fieldMember)
{
// This is a rather rough approximation of "used type" but the "unused import"
// info message doesn't have to be 100% accurate.
const QString name = fieldMember->name.toString();
if (m_importTypeLocationMap.contains(name)) {
const QQmlJSImportedScope type = m_rootScopeImports.type(name);
if (type.scope.isNull()) {
if (m_rootScopeImports.hasType(name))
m_usedTypes.insert(name);
} else if (!type.scope->attachedTypeName().isEmpty()) {
m_usedTypes.insert(name);
}
}
}
void LinterVisitor::checkGroupedAndAttachedScopes()
{
for (const auto &scope : std::as_const(m_objectDefinitionScopes))
checkGroupedAndAttachedScope(scope);
}
void LinterVisitor::checkGroupedAndAttachedScope(const QQmlJSScope::ConstPtr &scope)
{
// These warnings do not apply for custom parsers and their children and need to be handled on a
// case by case basis
if (checkCustomParser(scope))
return;
if (!checkTypeResolved(scope))
return;
auto children = scope->childScopes();
while (!children.isEmpty()) {
auto childScope = children.takeFirst();
const auto type = childScope->scopeType();
switch (type) {
case QQmlSA::ScopeType::GroupedPropertyScope:
case QQmlSA::ScopeType::AttachedPropertyScope:
if (!childScope->baseType() && !m_unresolvedTypes.hasSeen(childScope)) {
// note: if we already warn about the unknown property scope then we don't have to
// warn again about it being unresolved later
m_logger->log(QStringLiteral("unknown %1 property scope %2.")
.arg(type == QQmlSA::ScopeType::GroupedPropertyScope
? QStringLiteral("grouped")
: QStringLiteral("attached"),
childScope->internalName()),
qmlUnqualified, childScope->sourceLocation());
}
children.append(childScope->childScopes());
break;
default:
break;
}
}
}
} // namespace QQmlJS
QT_END_NAMESPACE