9using namespace Qt::StringLiterals;
10using namespace QQmlJS::AST;
14
15
16
17
18
19
22 QQmlJSImporter *importer, QQmlJSLogger *logger,
23 const QString &implicitImportDirectory,
const QStringList &qmldirFiles,
24 QQmlJS::Engine *engine)
32 const auto leaveEnv = qScopeGuard([
this] { QQmlJSImportVisitor::leaveEnvironment(); });
34 if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
37 if (
auto base = m_currentScope->baseType()) {
38 if (base->internalName() == u"QQmlComponent"_s) {
39 const auto nChildren = std::count_if(
40 m_currentScope->childScopesBegin(), m_currentScope->childScopesEnd(),
41 [](
const QQmlJSScope::ConstPtr &scope) {
42 return scope->scopeType() == QQmlSA::ScopeType::QMLScope;
45 m_logger->log(
"Components must have exactly one child"_L1,
46 qmlComponentChildrenCount, m_currentScope->sourceLocation());
52bool LinterVisitor::
visit(StringLiteral *sl)
54 QQmlJSImportVisitor::visit(sl);
55 const QString s = m_logger->code().mid(sl->literalToken.begin(), sl->literalToken.length);
57 if (s.contains(QLatin1Char(
'\r')) || s.contains(QLatin1Char(
'\n')) || s.contains(QChar(0x2028u))
58 || s.contains(QChar(0x2029u))) {
59 QString templateString;
62 const QChar stringQuote = s[0];
63 for (qsizetype i = 1; i < s.size() - 1; i++) {
71 templateString.chop(1);
76 templateString += u'\\';
77 if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{')
78 templateString += u'\\';
84 QQmlJSFixSuggestion suggestion = {
"Use a template literal instead."_L1, sl->literalToken,
85 u"`" % templateString % u"`" };
86 suggestion.setAutoApplicable();
87 m_logger->log(QStringLiteral(
"String contains unescaped line terminator which is "
89 qmlMultilineStrings, sl->literalToken,
true,
true, suggestion);
96 m_ancestryIncludingCurrentNode.push_back(n);
102 Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n);
103 m_ancestryIncludingCurrentNode.pop_back();
108 if (m_ancestryIncludingCurrentNode.size() < 2)
110 return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2];
113bool LinterVisitor::
visit(CommaExpression *expression)
115 QQmlJSImportVisitor::visit(expression);
116 if (!expression->left || !expression->right)
120 if (cast<ForStatement *>(astParentOfVisitedNode()))
123 m_logger->log(
"Do not use comma expressions."_L1, qmlComma, expression->commaToken);
129 static constexpr std::array literals{
"Boolean"_L1,
"Function"_L1,
"JSON"_L1,
130 "Math"_L1,
"Number"_L1,
"String"_L1 };
132 const IdentifierExpression *identifier = cast<IdentifierExpression *>(expression->base);
136 if (std::find(literals.cbegin(), literals.cend(), identifier->name) != literals.cend()) {
137 logger->log(
"Do not use '%1' as a constructor."_L1.arg(identifier->name),
138 qmlLiteralConstructor, identifier->identifierToken);
142bool LinterVisitor::
visit(NewMemberExpression *expression)
144 QQmlJSImportVisitor::visit(expression);
145 warnAboutLiteralConstructors(expression, m_logger);
149bool LinterVisitor::
visit(VoidExpression *ast)
151 QQmlJSImportVisitor::visit(ast);
152 m_logger->log(
"Do not use void expressions."_L1, qmlVoid, ast->voidToken);
158 Q_ASSERT(exp->op == QSOperator::Add);
160 SourceLocation location = exp->operatorToken;
163 if (
auto increment = cast<PostIncrementExpression *>(exp->left))
164 location = combine(increment->incrementToken, location);
166 if (
auto unary = cast<UnaryPlusExpression *>(exp->right))
167 location = combine(location, unary->plusToken);
169 if (
auto increment = cast<PreIncrementExpression *>(exp->right))
170 location = combine(location, increment->incrementToken);
172 if (location == exp->operatorToken)
173 return SourceLocation{};
180 Q_ASSERT(exp->op == QSOperator::Sub);
182 SourceLocation location = exp->operatorToken;
185 if (
auto decrement = cast<PostDecrementExpression *>(exp->left))
186 location = combine(decrement->decrementToken, location);
188 if (
auto unary = cast<UnaryMinusExpression *>(exp->right))
189 location = combine(location, unary->minusToken);
191 if (
auto decrement = cast<PreDecrementExpression *>(exp->right))
192 location = combine(location, decrement->decrementToken);
194 if (location == exp->operatorToken)
195 return SourceLocation{};
200bool LinterVisitor::
visit(BinaryExpression *exp)
202 QQmlJSImportVisitor::visit(exp);
204 case QSOperator::Add:
205 if (SourceLocation loc = confusingPluses(exp); loc.isValid())
206 m_logger->log(
"Confusing pluses."_L1, qmlConfusingPluses, loc);
208 case QSOperator::Sub:
209 if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
210 m_logger->log(
"Confusing minuses."_L1, qmlConfusingMinuses, loc);
219bool LinterVisitor::
visit(QQmlJS::AST::UiImport *import)
221 QQmlJSImportVisitor::visit(import);
223 const auto locAndName = [](
const UiImport *i) {
225 return std::make_pair(i->fileNameToken, i->fileName.toString());
227 QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
228 if (i->importIdToken.isValid())
229 l = combine(l, i->importIdToken);
231 l = combine(l, i->version->minorToken);
233 l = combine(l, i->importUri->lastSourceLocation());
235 return std::make_pair(l, i->importUri->toString());
238 SeenImport i(import);
239 if (
const auto it = m_seenImports.constFind(i); it != m_seenImports.constEnd()) {
240 const auto locAndNameImport = locAndName(import);
241 const auto locAndNameSeen = locAndName(it->uiImport);
242 m_logger->log(
"Duplicate import '%1'"_L1.arg(locAndNameImport.second),
243 qmlDuplicateImport, locAndNameImport.first);
244 m_logger->log(
"Note: previous import '%1' here"_L1.arg(locAndNameSeen.second),
245 qmlDuplicateImport, locAndNameSeen.first,
true,
true, {}, {},
246 locAndName(import).first.startLine);
249 m_seenImports.insert(i);
253void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
254 const QQmlJS::SourceLocation &location)
256 m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
258 for (
const auto *member = members; member; member = member->next) {
259 if (member->member.toString() == key) {
260 m_logger->log(u"Note: previous declaration of '%1' here"_s.arg(key),
261 qmlDuplicateEnumEntries, member->memberToken);
267bool LinterVisitor::
visit(QQmlJS::AST::UiEnumDeclaration *uied)
269 QQmlJSImportVisitor::visit(uied);
271 if (m_currentScope->isInlineComponent()) {
272 m_logger->log(u"Enums declared inside of inline component are ignored."_s, qmlSyntax,
273 uied->firstSourceLocation());
274 }
else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No
275 && !m_currentScope->isFileRootComponent()) {
276 m_logger->log(u"Enum declared outside the root element. It won't be accessible."_s,
277 qmlNonRootEnums, uied->firstSourceLocation());
280 QHash<QStringView,
const QQmlJS::AST::UiEnumMemberList *> seen;
281 for (
const auto *member = uied->members; member; member = member->next) {
282 QStringView key = member->member;
283 if (!key.front().isUpper()) {
284 m_logger->log(u"Enum keys should start with an uppercase."_s, qmlSyntax,
285 member->memberToken);
288 if (seen.contains(key))
289 handleDuplicateEnums(uied->members, key, member->memberToken);
291 seen[member->member] = member;
293 if (uied->name == key) {
294 m_logger->log(
"Enum entry should be named differently than the enum itself to avoid "
295 "confusion."_L1, qmlEnumEntryMatchesEnum, member->firstSourceLocation());
308 switch (statement->kind) {
309 case Node::Kind_Block: {
310 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
312 case Node::Kind_BreakStatement:
314 case Node::Kind_CaseBlock: {
315 const CaseBlock *caseBlock = cast<CaseBlock *>(statement);
316 if (caseBlock->defaultClause)
317 return allCodePathsReturnInsideCase(caseBlock->defaultClause);
318 return allCodePathsReturnInsideCase(caseBlock->clauses);
320 case Node::Kind_CaseClause:
321 return allCodePathsReturnInsideCase(cast<CaseClause *>(statement)->statements);
322 case Node::Kind_CaseClauses: {
323 for (CaseClauses *caseClauses = cast<CaseClauses *>(statement); caseClauses;
324 caseClauses = caseClauses->next) {
325 if (!allCodePathsReturnInsideCase(caseClauses->clause))
330 case Node::Kind_ContinueStatement:
334 case Node::Kind_DefaultClause:
335 return allCodePathsReturnInsideCase(cast<DefaultClause *>(statement)->statements);
336 case Node::Kind_IfStatement: {
337 const auto *ifStatement = cast<IfStatement *>(statement);
338 return allCodePathsReturnInsideCase(ifStatement->ok)
339 && allCodePathsReturnInsideCase(ifStatement->ko);
341 case Node::Kind_LabelledStatement:
342 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
343 case Node::Kind_ReturnStatement:
345 case Node::Kind_StatementList: {
346 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
347 if (allCodePathsReturnInsideCase(list->statement))
352 case Node::Kind_SwitchStatement:
353 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
354 case Node::Kind_ThrowStatement:
356 case Node::Kind_TryStatement: {
357 auto *tryStatement = cast<TryStatement *>(statement);
358 if (allCodePathsReturnInsideCase(tryStatement->statement))
360 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
362 case Node::Kind_WithStatement:
363 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
370void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
371 SourceLocation nextLoc)
373 if (!statements || !nextLoc.isValid())
376 if (allCodePathsReturnInsideCase(statements))
379 quint32 afterLastStatement = 0;
380 for (StatementList *it = statements; it; it = it->next) {
382 afterLastStatement = it->statement->lastSourceLocation().end();
386 const auto &comments = m_engine->comments();
387 auto it = std::find_if(comments.cbegin(), comments.cend(),
388 [&](
auto c) {
return afterLastStatement < c.offset; });
389 auto end = std::find_if(it, comments.cend(),
390 [&](
auto c) {
return c.offset >= nextLoc.offset; });
392 for (; it != end; ++it) {
393 const QString &commentText = m_engine->code().mid(it->offset, it->length);
394 if (commentText.contains(
"fall through"_L1)
395 || commentText.contains(
"fall-through"_L1)
396 || commentText.contains(
"fallthrough"_L1)) {
401 m_logger->log(
"Unterminated non-empty case block"_L1, qmlUnterminatedCase, errorLoc);
404bool LinterVisitor::
visit(QQmlJS::AST::CaseBlock *block)
406 QQmlJSImportVisitor::visit(block);
408 std::vector<std::pair<SourceLocation, StatementList *>> clauses;
409 for (CaseClauses *it = block->clauses; it; it = it->next)
410 clauses.push_back({ it->clause->caseToken, it->clause->statements });
411 if (block->defaultClause)
412 clauses.push_back({ block->defaultClause->defaultToken, block->defaultClause->statements });
413 for (CaseClauses *it = block->moreClauses; it; it = it->next)
414 clauses.push_back({ it->clause->caseToken, it->clause->statements });
417 for (size_t i = 0; i < clauses.size() - 1; ++i) {
418 const SourceLocation nextToken = clauses[i + 1].first;
419 checkCaseFallthrough(clauses[i].second, clauses[i].first, nextToken);
425
426
427
428
432 case Node::Kind_CallExpression:
433 case Node::Kind_DeleteExpression:
434 case Node::Kind_NewExpression:
435 case Node::Kind_PreDecrementExpression:
436 case Node::Kind_PreIncrementExpression:
437 case Node::Kind_PostDecrementExpression:
438 case Node::Kind_PostIncrementExpression:
439 case Node::Kind_YieldExpression:
440 case Node::Kind_FunctionExpression:
445 BinaryExpression *binary = cast<BinaryExpression *>(ast);
449 switch (binary->op) {
450 case QSOperator::InplaceAnd:
451 case QSOperator::Assign:
452 case QSOperator::InplaceSub:
453 case QSOperator::InplaceDiv:
454 case QSOperator::InplaceExp:
455 case QSOperator::InplaceAdd:
456 case QSOperator::InplaceLeftShift:
457 case QSOperator::InplaceMod:
458 case QSOperator::InplaceMul:
459 case QSOperator::InplaceOr:
460 case QSOperator::InplaceRightShift:
461 case QSOperator::InplaceURightShift:
462 case QSOperator::InplaceXor:
467 Q_UNREACHABLE_RETURN(
true);
472 return parent->kind != Node::Kind_UiScriptBinding && parent->kind != Node::Kind_UiPublicMember;
475bool LinterVisitor::
visit(ExpressionStatement *ast)
477 QQmlJSImportVisitor::visit(ast);
479 if (canHaveUselessExpressionStatement(astParentOfVisitedNode())
480 && isUselessExpressionStatement(ast->expression)) {
481 m_logger->log(
"Expression statement has no obvious effect."_L1,
482 qmlConfusingExpressionStatement,
483 combine(ast->firstSourceLocation(), ast->lastSourceLocation()));
490 const QString &name,
const QQmlJS::AST::Statement *statement,
491 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
493 if (statement && statement->kind == (
int)AST::Node::Kind::Kind_Block) {
494 const auto *block =
static_cast<
const AST::Block *>(statement);
495 if (!block->statements && associatedPropertyDefinition) {
496 m_logger->log(
"Unintentional empty block, use ({}) for empty object literal"_L1,
497 qmlUnintentionalEmptyBlock,
498 combine(block->lbraceToken, block->rbraceToken));
502 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
506 const UiPublicMember *associatedPropertyDefinition)
508 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
511 if (!associatedPropertyDefinition->isReadonly())
514 const auto &prop = m_currentScope->property(binding.propertyName());
515 const auto log = [&](
const QString &preferredType) {
516 m_logger->log(
"Prefer more specific type %1 over var"_L1.arg(preferredType),
517 qmlPreferNonVarProperties, prop.sourceLocation());
520 if (prop.typeName() !=
"QVariant"_L1)
523 switch (binding.bindingType()) {
524 case QQmlSA::BindingType::BoolLiteral: {
528 case QQmlSA::BindingType::NumberLiteral: {
529 double v = binding.numberValue();
530 auto loc = binding.sourceLocation();
531 QStringView literal = QStringView(m_engine->code()).mid(loc.offset, loc.length);
532 if (literal.contains(u'.') ||
double(
int(v)) != v)
533 log(
"real or double"_L1);
538 case QQmlSA::BindingType::StringLiteral: {
void postVisit(QQmlJS::AST::Node *) 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 leaveEnvironment() override
bool visit(QQmlJS::AST::StringLiteral *) override
QQmlJS::AST::Node * astParentOfVisitedNode() const
bool preVisit(QQmlJS::AST::Node *) override
static bool isUselessExpressionStatement(ExpressionNode *ast)
static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger)
static SourceLocation confusingPluses(BinaryExpression *exp)
static bool allCodePathsReturnInsideCase(Node *statement)
static SourceLocation confusingMinuses(BinaryExpression *exp)
static bool canHaveUselessExpressionStatement(Node *parent)