qtdeclarative/src/qmlcompiler/qqmljslintervisitor.cpp

403 lines
14 KiB
C++
Raw Normal View History

// 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
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(
const QQmlJSScope::Ptr &target, QQmlJSImporter *importer, QQmlJSLogger *logger,
const QString &implicitImportDirectory, const QStringList &qmldirFiles,
QQmlJS::Engine *engine)
: QQmlJSImportVisitor(target, importer, logger, implicitImportDirectory, qmldirFiles)
, m_engine(engine)
{
}
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;
}
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);
}
}
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, qmlSyntax,
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, qmlSyntax,
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;
}
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;
}
/*!
\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;
}
} // namespace QQmlJS
QT_END_NAMESPACE