2025-04-29 07:24:05 +00:00
|
|
|
// Copyright (C) 2025 The Qt Company Ltd.
|
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
|
|
|
|
#include "qqmljslintervisitor_p.h"
|
|
|
|
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
|
2025-04-29 07:38:41 +00:00
|
|
|
using namespace Qt::StringLiterals;
|
|
|
|
using namespace QQmlJS::AST;
|
|
|
|
|
2025-04-29 07:24:05 +00:00
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2025-05-15 08:46:53 +00:00
|
|
|
LinterVisitor::LinterVisitor(
|
2025-05-23 14:47:10 +00:00
|
|
|
QQmlJSImporter *importer, QQmlJSLogger *logger,
|
2025-05-15 08:46:53 +00:00
|
|
|
const QString &implicitImportDirectory, const QStringList &qmldirFiles,
|
|
|
|
QQmlJS::Engine *engine)
|
2025-05-23 14:47:10 +00:00
|
|
|
: QQmlJSImportVisitor(importer, logger, implicitImportDirectory, qmldirFiles)
|
2025-05-15 08:46:53 +00:00
|
|
|
, m_engine(engine)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2025-05-20 08:25:13 +00:00
|
|
|
void LinterVisitor::leaveEnvironment()
|
|
|
|
{
|
|
|
|
const auto leaveEnv = qScopeGuard([this] { QQmlJSImportVisitor::leaveEnvironment(); });
|
|
|
|
|
2025-07-04 12:42:12 +00:00
|
|
|
if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
|
|
|
|
return;
|
|
|
|
|
2025-05-20 08:25:13 +00:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-29 07:38:41 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
QQmlJSFixSuggestion suggestion = { "Use a template literal instead."_L1, sl->literalToken,
|
|
|
|
u"`" % templateString % u"`" };
|
|
|
|
suggestion.setAutoApplicable();
|
|
|
|
m_logger->log(QStringLiteral("String contains unescaped line terminator which is "
|
|
|
|
"deprecated."),
|
|
|
|
qmlMultilineStrings, sl->literalToken, true, true, suggestion);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-04-25 10:24:11 +00:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
|
2025-04-25 10:24:11 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2025-04-25 11:20:52 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinterVisitor::visit(NewMemberExpression *expression)
|
|
|
|
{
|
|
|
|
QQmlJSImportVisitor::visit(expression);
|
|
|
|
warnAboutLiteralConstructors(expression, m_logger);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-04-25 11:37:45 +00:00
|
|
|
bool LinterVisitor::visit(VoidExpression *ast)
|
|
|
|
{
|
|
|
|
QQmlJSImportVisitor::visit(ast);
|
|
|
|
m_logger->log("Do not use void expressions."_L1, qmlVoid, ast->voidToken);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-04-25 12:27:27 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2025-05-05 08:56:40 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2025-05-05 09:39:07 +00:00
|
|
|
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);
|
|
|
|
|
2025-05-06 13:50:12 +00:00
|
|
|
if (m_currentScope->isInlineComponent()) {
|
2025-05-05 09:39:07 +00:00
|
|
|
m_logger->log(u"Enums declared inside of inline component are ignored."_s, qmlSyntax,
|
|
|
|
uied->firstSourceLocation());
|
2025-05-06 13:50:12 +00:00
|
|
|
} 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());
|
2025-05-05 09:39:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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, qmlSyntax,
|
|
|
|
member->memberToken);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (seen.contains(key))
|
|
|
|
handleDuplicateEnums(uied->members, key, member->memberToken);
|
|
|
|
else
|
|
|
|
seen[member->member] = member;
|
2025-05-06 13:49:25 +00:00
|
|
|
|
|
|
|
if (uied->name == key) {
|
|
|
|
m_logger->log("Enum entry should be named differently than the enum itself to avoid "
|
|
|
|
"confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
|
|
|
|
}
|
2025-05-05 09:39:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-05-15 08:46:53 +00:00
|
|
|
void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
|
|
|
|
SourceLocation nextLoc)
|
|
|
|
{
|
|
|
|
if (!statements || !nextLoc.isValid())
|
|
|
|
return;
|
|
|
|
|
|
|
|
quint32 afterLastStatement = 0;
|
|
|
|
for (StatementList *it = statements; it; it = it->next) {
|
|
|
|
if (!it->next) {
|
|
|
|
Node::Kind kind = (Node::Kind) it->statement->kind;
|
|
|
|
if (kind == Node::Kind_BreakStatement || kind == Node::Kind_ReturnStatement
|
|
|
|
|| kind == Node::Kind_ThrowStatement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
|| commentText.contains("fall-through"_L1)
|
|
|
|
|| commentText.contains("fallthrough"_L1)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
m_logger->log("Unterminated non-empty case block"_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;
|
|
|
|
}
|
|
|
|
|
2025-04-25 14:21:58 +00:00
|
|
|
/*!
|
|
|
|
\internal
|
|
|
|
|
|
|
|
This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
|
|
|
|
*/
|
|
|
|
static bool isUselessExpressionStatement(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;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
};
|
|
|
|
BinaryExpression *binary = cast<BinaryExpression *>(ast);
|
|
|
|
if (!binary)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
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 true;
|
|
|
|
}
|
|
|
|
Q_UNREACHABLE_RETURN(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool canHaveUselessExpressionStatement(Node *parent)
|
|
|
|
{
|
|
|
|
return parent->kind != Node::Kind_UiScriptBinding && parent->kind != Node::Kind_UiPublicMember;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LinterVisitor::visit(ExpressionStatement *ast)
|
|
|
|
{
|
|
|
|
QQmlJSImportVisitor::visit(ast);
|
|
|
|
|
|
|
|
if (canHaveUselessExpressionStatement(astParentOfVisitedNode())
|
|
|
|
&& isUselessExpressionStatement(ast->expression)) {
|
|
|
|
m_logger->log("Expression statement has no obvious effect."_L1,
|
|
|
|
qmlConfusingExpressionStatement,
|
|
|
|
combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-05-28 13:00:38 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2025-05-20 11:45:56 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-29 07:24:05 +00:00
|
|
|
} // namespace QQmlJS
|
|
|
|
|
|
|
|
QT_END_NAMESPACE
|