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-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);
|
|
|
|
|
|
|
|
if (m_currentScope->inlineComponentName()) {
|
|
|
|
m_logger->log(u"Enums declared inside of inline component are ignored."_s, qmlSyntax,
|
|
|
|
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, qmlSyntax,
|
|
|
|
member->memberToken);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (seen.contains(key))
|
|
|
|
handleDuplicateEnums(uied->members, key, member->memberToken);
|
|
|
|
else
|
|
|
|
seen[member->member] = member;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2025-04-29 07:24:05 +00:00
|
|
|
} // namespace QQmlJS
|
|
|
|
|
|
|
|
QT_END_NAMESPACE
|