Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmljslintervisitor.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3// Qt-Security score:significant
4
6
8
9using namespace Qt::StringLiterals;
10using namespace QQmlJS::AST;
11
12namespace QQmlJS {
13/*!
14 \internal
15 \class QQmlJS::LinterVisitor
16 Extends QQmlJSImportVisitor with extra warnings that are required for linting but unrelated to
17 QQmlJSImportVisitor actual task that is constructing QQmlJSScopes. One example of such warnings
18 are purely syntactic checks, or style-checks warnings that don't make sense during compilation.
19 */
20
21LinterVisitor::LinterVisitor(
22 QQmlJSImporter *importer, QQmlJSLogger *logger,
23 const QString &implicitImportDirectory, const QStringList &qmldirFiles,
24 QQmlJS::Engine *engine)
27{
28}
29
30void LinterVisitor::leaveEnvironment()
31{
32 const auto leaveEnv = qScopeGuard([this] { QQmlJSImportVisitor::leaveEnvironment(); });
33
34 if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope)
35 return;
36
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;
43 });
44 if (nChildren != 1) {
45 m_logger->log("Components must have exactly one child"_L1,
46 qmlComponentChildrenCount, m_currentScope->sourceLocation());
47 }
48 }
49 }
50}
51
52bool LinterVisitor::visit(StringLiteral *sl)
53{
54 QQmlJSImportVisitor::visit(sl);
55 const QString s = m_logger->code().mid(sl->literalToken.begin(), sl->literalToken.length);
56
57 if (s.contains(QLatin1Char('\r')) || s.contains(QLatin1Char('\n')) || s.contains(QChar(0x2028u))
58 || s.contains(QChar(0x2029u))) {
59 QString templateString;
60
61 bool escaped = false;
62 const QChar stringQuote = s[0];
63 for (qsizetype i = 1; i < s.size() - 1; i++) {
64 const QChar c = s[i];
65
66 if (c == u'\\') {
67 escaped = !escaped;
68 } else if (escaped) {
69 // If we encounter an escaped quote, unescape it since we use backticks here
70 if (c == stringQuote)
71 templateString.chop(1);
72
73 escaped = false;
74 } else {
75 if (c == u'`')
76 templateString += u'\\';
77 if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{')
78 templateString += u'\\';
79 }
80
81 templateString += c;
82 }
83
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 "
88 "deprecated."),
89 qmlMultilineStrings, sl->literalToken, true, true, suggestion);
90 }
91 return true;
92}
93
94bool LinterVisitor::preVisit(Node *n)
95{
96 m_ancestryIncludingCurrentNode.push_back(n);
97 return true;
98}
99
100void LinterVisitor::postVisit(Node *n)
101{
102 Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n);
103 m_ancestryIncludingCurrentNode.pop_back();
104}
105
106Node *LinterVisitor::astParentOfVisitedNode() const
107{
108 if (m_ancestryIncludingCurrentNode.size() < 2)
109 return nullptr;
110 return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2];
111}
112
113bool LinterVisitor::visit(CommaExpression *expression)
114{
115 QQmlJSImportVisitor::visit(expression);
116 if (!expression->left || !expression->right)
117 return true;
118
119 // don't warn about commas in "for" statements
120 if (cast<ForStatement *>(astParentOfVisitedNode()))
121 return true;
122
123 m_logger->log("Do not use comma expressions."_L1, qmlComma, expression->commaToken);
124 return true;
125}
126
127static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger)
128{
129 static constexpr std::array literals{ "Boolean"_L1, "Function"_L1, "JSON"_L1,
130 "Math"_L1, "Number"_L1, "String"_L1 };
131
132 const IdentifierExpression *identifier = cast<IdentifierExpression *>(expression->base);
133 if (!identifier)
134 return;
135
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);
139 }
140}
141
142bool LinterVisitor::visit(NewMemberExpression *expression)
143{
144 QQmlJSImportVisitor::visit(expression);
145 warnAboutLiteralConstructors(expression, m_logger);
146 return true;
147}
148
149bool LinterVisitor::visit(VoidExpression *ast)
150{
151 QQmlJSImportVisitor::visit(ast);
152 m_logger->log("Do not use void expressions."_L1, qmlVoid, ast->voidToken);
153 return true;
154}
155
156static SourceLocation confusingPluses(BinaryExpression *exp)
157{
158 Q_ASSERT(exp->op == QSOperator::Add);
159
160 SourceLocation location = exp->operatorToken;
161
162 // a++ + b
163 if (auto increment = cast<PostIncrementExpression *>(exp->left))
164 location = combine(increment->incrementToken, location);
165 // a + +b
166 if (auto unary = cast<UnaryPlusExpression *>(exp->right))
167 location = combine(location, unary->plusToken);
168 // a + ++b
169 if (auto increment = cast<PreIncrementExpression *>(exp->right))
170 location = combine(location, increment->incrementToken);
171
172 if (location == exp->operatorToken)
173 return SourceLocation{};
174
175 return location;
176}
177
178static SourceLocation confusingMinuses(BinaryExpression *exp)
179{
180 Q_ASSERT(exp->op == QSOperator::Sub);
181
182 SourceLocation location = exp->operatorToken;
183
184 // a-- - b
185 if (auto decrement = cast<PostDecrementExpression *>(exp->left))
186 location = combine(decrement->decrementToken, location);
187 // a - -b
188 if (auto unary = cast<UnaryMinusExpression *>(exp->right))
189 location = combine(location, unary->minusToken);
190 // a - --b
191 if (auto decrement = cast<PreDecrementExpression *>(exp->right))
192 location = combine(location, decrement->decrementToken);
193
194 if (location == exp->operatorToken)
195 return SourceLocation{};
196
197 return location;
198}
199
200bool LinterVisitor::visit(BinaryExpression *exp)
201{
202 QQmlJSImportVisitor::visit(exp);
203 switch (exp->op) {
204 case QSOperator::Add:
205 if (SourceLocation loc = confusingPluses(exp); loc.isValid())
206 m_logger->log("Confusing pluses."_L1, qmlConfusingPluses, loc);
207 break;
208 case QSOperator::Sub:
209 if (SourceLocation loc = confusingMinuses(exp); loc.isValid())
210 m_logger->log("Confusing minuses."_L1, qmlConfusingMinuses, loc);
211 break;
212 default:
213 break;
214 }
215
216 return true;
217}
218
219bool LinterVisitor::visit(QQmlJS::AST::UiImport *import)
220{
221 QQmlJSImportVisitor::visit(import);
222
223 const auto locAndName = [](const UiImport *i) {
224 if (!i->importUri)
225 return std::make_pair(i->fileNameToken, i->fileName.toString());
226
227 QQmlJS::SourceLocation l = i->importUri->firstSourceLocation();
228 if (i->importIdToken.isValid())
229 l = combine(l, i->importIdToken);
230 else if (i->version)
231 l = combine(l, i->version->minorToken);
232 else
233 l = combine(l, i->importUri->lastSourceLocation());
234
235 return std::make_pair(l, i->importUri->toString());
236 };
237
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);
247 }
248
249 m_seenImports.insert(i);
250 return true;
251}
252
253void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key,
254 const QQmlJS::SourceLocation &location)
255{
256 m_logger->log(u"Enum key '%1' has already been declared"_s.arg(key), qmlDuplicateEnumEntries,
257 location);
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);
262 return;
263 }
264 }
265}
266
267bool LinterVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied)
268{
269 QQmlJSImportVisitor::visit(uied);
270
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());
278 }
279
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);
286 }
287
288 if (seen.contains(key))
289 handleDuplicateEnums(uied->members, key, member->memberToken);
290 else
291 seen[member->member] = member;
292
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());
296 }
297 }
298
299 return true;
300}
301
302static bool allCodePathsReturnInsideCase(Node *statement)
303{
304 using namespace AST;
305 if (!statement)
306 return false;
307
308 switch (statement->kind) {
309 case Node::Kind_Block: {
310 return allCodePathsReturnInsideCase(cast<Block *>(statement)->statements);
311 }
312 case Node::Kind_BreakStatement:
313 return true;
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);
319 }
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))
326 return false;
327 }
328 return true;
329 }
330 case Node::Kind_ContinueStatement:
331 // allCodePathsReturn() doesn't recurse into loops, so any encountered `continue` should
332 // belong to a loop outside the switch statement.
333 return true;
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);
340 }
341 case Node::Kind_LabelledStatement:
342 return allCodePathsReturnInsideCase(cast<LabelledStatement *>(statement)->statement);
343 case Node::Kind_ReturnStatement:
344 return true;
345 case Node::Kind_StatementList: {
346 for (StatementList *list = cast<StatementList *>(statement); list; list = list->next) {
347 if (allCodePathsReturnInsideCase(list->statement))
348 return true;
349 }
350 return false;
351 }
352 case Node::Kind_SwitchStatement:
353 return allCodePathsReturnInsideCase(cast<SwitchStatement *>(statement)->block);
354 case Node::Kind_ThrowStatement:
355 return true;
356 case Node::Kind_TryStatement: {
357 auto *tryStatement = cast<TryStatement *>(statement);
358 if (allCodePathsReturnInsideCase(tryStatement->statement))
359 return true;
360 return allCodePathsReturnInsideCase(tryStatement->finallyExpression->statement);
361 }
362 case Node::Kind_WithStatement:
363 return allCodePathsReturnInsideCase(cast<WithStatement *>(statement)->statement);
364 default:
365 break;
366 }
367 return false;
368}
369
370void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc,
371 SourceLocation nextLoc)
372{
373 if (!statements || !nextLoc.isValid())
374 return;
375
376 if (allCodePathsReturnInsideCase(statements))
377 return;
378
379 quint32 afterLastStatement = 0;
380 for (StatementList *it = statements; it; it = it->next) {
381 if (!it->next) {
382 afterLastStatement = it->statement->lastSourceLocation().end();
383 }
384 }
385
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; });
391
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)) {
397 return;
398 }
399 }
400
401 m_logger->log("Unterminated non-empty case block"_L1, qmlUnterminatedCase, errorLoc);
402}
403
404bool LinterVisitor::visit(QQmlJS::AST::CaseBlock *block)
405{
406 QQmlJSImportVisitor::visit(block);
407
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 });
415
416 // check all but the last clause for fallthrough
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);
420 }
421 return true;
422}
423
424/*!
425\internal
426
427This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar.
428*/
429static bool isUselessExpressionStatement(ExpressionNode *ast)
430{
431 switch (ast->kind) {
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:
441 return false;
442 default:
443 break;
444 };
445 BinaryExpression *binary = cast<BinaryExpression *>(ast);
446 if (!binary)
447 return false;
448
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:
463 return false;
464 default:
465 return true;
466 }
467 Q_UNREACHABLE_RETURN(true);
468}
469
470static bool canHaveUselessExpressionStatement(Node *parent)
471{
472 return parent->kind != Node::Kind_UiScriptBinding && parent->kind != Node::Kind_UiPublicMember;
473}
474
475bool LinterVisitor::visit(ExpressionStatement *ast)
476{
477 QQmlJSImportVisitor::visit(ast);
478
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()));
484 }
485
486 return true;
487}
488
490 const QString &name, const QQmlJS::AST::Statement *statement,
491 const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition)
492{
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));
499 }
500 }
501
502 return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition);
503}
504
505void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding,
506 const UiPublicMember *associatedPropertyDefinition)
507{
508 if (!m_currentScope->hasOwnProperty(binding.propertyName()))
509 return;
510
511 if (!associatedPropertyDefinition->isReadonly())
512 return;
513
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());
518 };
519
520 if (prop.typeName() != "QVariant"_L1)
521 return;
522
523 switch (binding.bindingType()) {
524 case QQmlSA::BindingType::BoolLiteral: {
525 log("bool"_L1);
526 break;
527 }
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);
534 else
535 log("int"_L1);
536 break;
537 }
538 case QQmlSA::BindingType::StringLiteral: {
539 log("string"_L1);
540 break;
541 }
542 default: {
543 break;
544 }
545 }
546}
547
548} // namespace QQmlJS
549
550QT_END_NAMESPACE
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
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)