| 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 | |
| 4 | #include "qqmljslintervisitor_p.h" |
| 5 | |
| 6 | QT_BEGIN_NAMESPACE |
| 7 | |
| 8 | using namespace Qt::StringLiterals; |
| 9 | using namespace QQmlJS::AST; |
| 10 | |
| 11 | namespace QQmlJS { |
| 12 | /*! |
| 13 | \internal |
| 14 | \class QQmlJS::LinterVisitor |
| 15 | Extends QQmlJSImportVisitor with extra warnings that are required for linting but unrelated to |
| 16 | QQmlJSImportVisitor actual task that is constructing QQmlJSScopes. One example of such warnings |
| 17 | are purely syntactic checks, or style-checks warnings that don't make sense during compilation. |
| 18 | */ |
| 19 | |
| 20 | LinterVisitor::LinterVisitor( |
| 21 | const QQmlJSScope::Ptr &target, QQmlJSImporter *importer, QQmlJSLogger *logger, |
| 22 | const QString &implicitImportDirectory, const QStringList &qmldirFiles, |
| 23 | QQmlJS::Engine *engine) |
| 24 | : QQmlJSImportVisitor(target, importer, logger, implicitImportDirectory, qmldirFiles) |
| 25 | , m_engine(engine) |
| 26 | { |
| 27 | } |
| 28 | |
| 29 | void LinterVisitor::leaveEnvironment() |
| 30 | { |
| 31 | const auto leaveEnv = qScopeGuard(f: [this] { QQmlJSImportVisitor::leaveEnvironment(); }); |
| 32 | |
| 33 | if (m_currentScope->scopeType() != QQmlSA::ScopeType::QMLScope) |
| 34 | return; |
| 35 | |
| 36 | if (auto base = m_currentScope->baseType()) { |
| 37 | if (base->internalName() == u"QQmlComponent"_s ) { |
| 38 | const auto nChildren = std::count_if( |
| 39 | first: m_currentScope->childScopesBegin(), last: m_currentScope->childScopesEnd(), |
| 40 | pred: [](const QQmlJSScope::ConstPtr &scope) { |
| 41 | return scope->scopeType() == QQmlSA::ScopeType::QMLScope; |
| 42 | }); |
| 43 | if (nChildren != 1) { |
| 44 | m_logger->log(message: "Components must have exactly one child"_L1 , |
| 45 | id: qmlComponentChildrenCount, srcLocation: m_currentScope->sourceLocation()); |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | bool LinterVisitor::visit(StringLiteral *sl) |
| 52 | { |
| 53 | QQmlJSImportVisitor::visit(sl); |
| 54 | const QString s = m_logger->code().mid(position: sl->literalToken.begin(), n: sl->literalToken.length); |
| 55 | |
| 56 | if (s.contains(c: QLatin1Char('\r')) || s.contains(c: QLatin1Char('\n')) || s.contains(c: QChar(0x2028u)) |
| 57 | || s.contains(c: QChar(0x2029u))) { |
| 58 | QString templateString; |
| 59 | |
| 60 | bool escaped = false; |
| 61 | const QChar stringQuote = s[0]; |
| 62 | for (qsizetype i = 1; i < s.size() - 1; i++) { |
| 63 | const QChar c = s[i]; |
| 64 | |
| 65 | if (c == u'\\') { |
| 66 | escaped = !escaped; |
| 67 | } else if (escaped) { |
| 68 | // If we encounter an escaped quote, unescape it since we use backticks here |
| 69 | if (c == stringQuote) |
| 70 | templateString.chop(n: 1); |
| 71 | |
| 72 | escaped = false; |
| 73 | } else { |
| 74 | if (c == u'`') |
| 75 | templateString += u'\\'; |
| 76 | if (c == u'$' && i + 1 < s.size() - 1 && s[i + 1] == u'{') |
| 77 | templateString += u'\\'; |
| 78 | } |
| 79 | |
| 80 | templateString += c; |
| 81 | } |
| 82 | |
| 83 | QQmlJSFixSuggestion suggestion = { "Use a template literal instead."_L1 , sl->literalToken, |
| 84 | u"`" % templateString % u"`" }; |
| 85 | suggestion.setAutoApplicable(); |
| 86 | m_logger->log(QStringLiteral("String contains unescaped line terminator which is " |
| 87 | "deprecated." ), |
| 88 | id: qmlMultilineStrings, srcLocation: sl->literalToken, showContext: true, showFileName: true, suggestion); |
| 89 | } |
| 90 | return true; |
| 91 | } |
| 92 | |
| 93 | bool LinterVisitor::preVisit(Node *n) |
| 94 | { |
| 95 | m_ancestryIncludingCurrentNode.push_back(x: n); |
| 96 | return true; |
| 97 | } |
| 98 | |
| 99 | void LinterVisitor::postVisit(Node *n) |
| 100 | { |
| 101 | Q_ASSERT(m_ancestryIncludingCurrentNode.back() == n); |
| 102 | m_ancestryIncludingCurrentNode.pop_back(); |
| 103 | } |
| 104 | |
| 105 | Node *LinterVisitor::astParentOfVisitedNode() const |
| 106 | { |
| 107 | if (m_ancestryIncludingCurrentNode.size() < 2) |
| 108 | return nullptr; |
| 109 | return m_ancestryIncludingCurrentNode[m_ancestryIncludingCurrentNode.size() - 2]; |
| 110 | } |
| 111 | |
| 112 | bool LinterVisitor::visit(CommaExpression *expression) |
| 113 | { |
| 114 | QQmlJSImportVisitor::visit(expression); |
| 115 | if (!expression->left || !expression->right) |
| 116 | return true; |
| 117 | |
| 118 | // don't warn about commas in "for" statements |
| 119 | if (cast<ForStatement *>(ast: astParentOfVisitedNode())) |
| 120 | return true; |
| 121 | |
| 122 | m_logger->log(message: "Do not use comma expressions."_L1 , id: qmlComma, srcLocation: expression->commaToken); |
| 123 | return true; |
| 124 | } |
| 125 | |
| 126 | static void warnAboutLiteralConstructors(NewMemberExpression *expression, QQmlJSLogger *logger) |
| 127 | { |
| 128 | static constexpr std::array literals{ "Boolean"_L1 , "Function"_L1 , "JSON"_L1 , |
| 129 | "Math"_L1 , "Number"_L1 , "String"_L1 }; |
| 130 | |
| 131 | const IdentifierExpression *identifier = cast<IdentifierExpression *>(ast: expression->base); |
| 132 | if (!identifier) |
| 133 | return; |
| 134 | |
| 135 | if (std::find(first: literals.cbegin(), last: literals.cend(), val: identifier->name) != literals.cend()) { |
| 136 | logger->log(message: "Do not use '%1' as a constructor."_L1 .arg(args: identifier->name), |
| 137 | id: qmlLiteralConstructor, srcLocation: identifier->identifierToken); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | bool LinterVisitor::visit(NewMemberExpression *expression) |
| 142 | { |
| 143 | QQmlJSImportVisitor::visit(expression); |
| 144 | warnAboutLiteralConstructors(expression, logger: m_logger); |
| 145 | return true; |
| 146 | } |
| 147 | |
| 148 | bool LinterVisitor::visit(VoidExpression *ast) |
| 149 | { |
| 150 | QQmlJSImportVisitor::visit(ast); |
| 151 | m_logger->log(message: "Do not use void expressions."_L1 , id: qmlVoid, srcLocation: ast->voidToken); |
| 152 | return true; |
| 153 | } |
| 154 | |
| 155 | static SourceLocation confusingPluses(BinaryExpression *exp) |
| 156 | { |
| 157 | Q_ASSERT(exp->op == QSOperator::Add); |
| 158 | |
| 159 | SourceLocation location = exp->operatorToken; |
| 160 | |
| 161 | // a++ + b |
| 162 | if (auto increment = cast<PostIncrementExpression *>(ast: exp->left)) |
| 163 | location = combine(l1: increment->incrementToken, l2: location); |
| 164 | // a + +b |
| 165 | if (auto unary = cast<UnaryPlusExpression *>(ast: exp->right)) |
| 166 | location = combine(l1: location, l2: unary->plusToken); |
| 167 | // a + ++b |
| 168 | if (auto increment = cast<PreIncrementExpression *>(ast: exp->right)) |
| 169 | location = combine(l1: location, l2: increment->incrementToken); |
| 170 | |
| 171 | if (location == exp->operatorToken) |
| 172 | return SourceLocation{}; |
| 173 | |
| 174 | return location; |
| 175 | } |
| 176 | |
| 177 | static SourceLocation confusingMinuses(BinaryExpression *exp) |
| 178 | { |
| 179 | Q_ASSERT(exp->op == QSOperator::Sub); |
| 180 | |
| 181 | SourceLocation location = exp->operatorToken; |
| 182 | |
| 183 | // a-- - b |
| 184 | if (auto decrement = cast<PostDecrementExpression *>(ast: exp->left)) |
| 185 | location = combine(l1: decrement->decrementToken, l2: location); |
| 186 | // a - -b |
| 187 | if (auto unary = cast<UnaryMinusExpression *>(ast: exp->right)) |
| 188 | location = combine(l1: location, l2: unary->minusToken); |
| 189 | // a - --b |
| 190 | if (auto decrement = cast<PreDecrementExpression *>(ast: exp->right)) |
| 191 | location = combine(l1: location, l2: decrement->decrementToken); |
| 192 | |
| 193 | if (location == exp->operatorToken) |
| 194 | return SourceLocation{}; |
| 195 | |
| 196 | return location; |
| 197 | } |
| 198 | |
| 199 | bool LinterVisitor::visit(BinaryExpression *exp) |
| 200 | { |
| 201 | QQmlJSImportVisitor::visit(exp); |
| 202 | switch (exp->op) { |
| 203 | case QSOperator::Add: |
| 204 | if (SourceLocation loc = confusingPluses(exp); loc.isValid()) |
| 205 | m_logger->log(message: "Confusing pluses."_L1 , id: qmlConfusingPluses, srcLocation: loc); |
| 206 | break; |
| 207 | case QSOperator::Sub: |
| 208 | if (SourceLocation loc = confusingMinuses(exp); loc.isValid()) |
| 209 | m_logger->log(message: "Confusing minuses."_L1 , id: qmlConfusingMinuses, srcLocation: loc); |
| 210 | break; |
| 211 | default: |
| 212 | break; |
| 213 | } |
| 214 | |
| 215 | return true; |
| 216 | } |
| 217 | |
| 218 | bool LinterVisitor::visit(QQmlJS::AST::UiImport *import) |
| 219 | { |
| 220 | QQmlJSImportVisitor::visit(import); |
| 221 | |
| 222 | const auto locAndName = [](const UiImport *i) { |
| 223 | if (!i->importUri) |
| 224 | return std::make_pair(x: i->fileNameToken, y: i->fileName.toString()); |
| 225 | |
| 226 | QQmlJS::SourceLocation l = i->importUri->firstSourceLocation(); |
| 227 | if (i->importIdToken.isValid()) |
| 228 | l = combine(l1: l, l2: i->importIdToken); |
| 229 | else if (i->version) |
| 230 | l = combine(l1: l, l2: i->version->minorToken); |
| 231 | else |
| 232 | l = combine(l1: l, l2: i->importUri->lastSourceLocation()); |
| 233 | |
| 234 | return std::make_pair(x&: l, y: i->importUri->toString()); |
| 235 | }; |
| 236 | |
| 237 | SeenImport i(import); |
| 238 | if (const auto it = m_seenImports.constFind(value: i); it != m_seenImports.constEnd()) { |
| 239 | const auto locAndNameImport = locAndName(import); |
| 240 | const auto locAndNameSeen = locAndName(it->uiImport); |
| 241 | m_logger->log(message: "Duplicate import '%1'"_L1 .arg(args: locAndNameImport.second), |
| 242 | id: qmlDuplicateImport, srcLocation: locAndNameImport.first); |
| 243 | m_logger->log(message: "Note: previous import '%1' here"_L1 .arg(args: locAndNameSeen.second), |
| 244 | id: qmlDuplicateImport, srcLocation: locAndNameSeen.first, showContext: true, showFileName: true, suggestion: {}, overrideFileName: {}, |
| 245 | customLineForDisabling: locAndName(import).first.startLine); |
| 246 | } |
| 247 | |
| 248 | m_seenImports.insert(value: i); |
| 249 | return true; |
| 250 | } |
| 251 | |
| 252 | void LinterVisitor::handleDuplicateEnums(UiEnumMemberList *members, QStringView key, |
| 253 | const QQmlJS::SourceLocation &location) |
| 254 | { |
| 255 | m_logger->log(message: u"Enum key '%1' has already been declared"_s .arg(a: key), id: qmlDuplicateEnumEntries, |
| 256 | srcLocation: location); |
| 257 | for (const auto *member = members; member; member = member->next) { |
| 258 | if (member->member.toString() == key) { |
| 259 | m_logger->log(message: u"Note: previous declaration of '%1' here"_s .arg(a: key), |
| 260 | id: qmlDuplicateEnumEntries, srcLocation: member->memberToken); |
| 261 | return; |
| 262 | } |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | bool LinterVisitor::visit(QQmlJS::AST::UiEnumDeclaration *uied) |
| 267 | { |
| 268 | QQmlJSImportVisitor::visit(uied); |
| 269 | |
| 270 | if (m_currentScope->isInlineComponent()) { |
| 271 | m_logger->log(message: u"Enums declared inside of inline component are ignored."_s , id: qmlSyntax, |
| 272 | srcLocation: uied->firstSourceLocation()); |
| 273 | } else if (m_currentScope->componentRootStatus() == QQmlJSScope::IsComponentRoot::No |
| 274 | && !m_currentScope->isFileRootComponent()) { |
| 275 | m_logger->log(message: u"Enum declared outside the root element. It won't be accessible."_s , |
| 276 | id: qmlNonRootEnums, srcLocation: uied->firstSourceLocation()); |
| 277 | } |
| 278 | |
| 279 | QHash<QStringView, const QQmlJS::AST::UiEnumMemberList *> seen; |
| 280 | for (const auto *member = uied->members; member; member = member->next) { |
| 281 | QStringView key = member->member; |
| 282 | if (!key.front().isUpper()) { |
| 283 | m_logger->log(message: u"Enum keys should start with an uppercase."_s , id: qmlSyntax, |
| 284 | srcLocation: member->memberToken); |
| 285 | } |
| 286 | |
| 287 | if (seen.contains(key)) |
| 288 | handleDuplicateEnums(members: uied->members, key, location: member->memberToken); |
| 289 | else |
| 290 | seen[member->member] = member; |
| 291 | |
| 292 | if (uied->name == key) { |
| 293 | m_logger->log(message: "Enum entry should be named differently than the enum itself to avoid " |
| 294 | "confusion."_L1 , id: qmlEnumEntryMatchesEnum, srcLocation: member->firstSourceLocation()); |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | return true; |
| 299 | } |
| 300 | |
| 301 | static bool allCodePathsReturnInsideCase(Node *statement) |
| 302 | { |
| 303 | using namespace AST; |
| 304 | if (!statement) |
| 305 | return false; |
| 306 | |
| 307 | switch (statement->kind) { |
| 308 | case Node::Kind_Block: { |
| 309 | return allCodePathsReturnInsideCase(statement: cast<Block *>(ast: statement)->statements); |
| 310 | } |
| 311 | case Node::Kind_BreakStatement: |
| 312 | return true; |
| 313 | case Node::Kind_CaseBlock: { |
| 314 | const CaseBlock *caseBlock = cast<CaseBlock *>(ast: statement); |
| 315 | if (caseBlock->defaultClause) |
| 316 | return allCodePathsReturnInsideCase(statement: caseBlock->defaultClause); |
| 317 | return allCodePathsReturnInsideCase(statement: caseBlock->clauses); |
| 318 | } |
| 319 | case Node::Kind_CaseClause: |
| 320 | return allCodePathsReturnInsideCase(statement: cast<CaseClause *>(ast: statement)->statements); |
| 321 | case Node::Kind_CaseClauses: { |
| 322 | for (CaseClauses *caseClauses = cast<CaseClauses *>(ast: statement); caseClauses; |
| 323 | caseClauses = caseClauses->next) { |
| 324 | if (!allCodePathsReturnInsideCase(statement: caseClauses->clause)) |
| 325 | return false; |
| 326 | } |
| 327 | return true; |
| 328 | } |
| 329 | case Node::Kind_ContinueStatement: |
| 330 | // allCodePathsReturn() doesn't recurse into loops, so any encountered `continue` should |
| 331 | // belong to a loop outside the switch statement. |
| 332 | return true; |
| 333 | case Node::Kind_DefaultClause: |
| 334 | return allCodePathsReturnInsideCase(statement: cast<DefaultClause *>(ast: statement)->statements); |
| 335 | case Node::Kind_IfStatement: { |
| 336 | const auto *ifStatement = cast<IfStatement *>(ast: statement); |
| 337 | return allCodePathsReturnInsideCase(statement: ifStatement->ok) |
| 338 | && allCodePathsReturnInsideCase(statement: ifStatement->ko); |
| 339 | } |
| 340 | case Node::Kind_LabelledStatement: |
| 341 | return allCodePathsReturnInsideCase(statement: cast<LabelledStatement *>(ast: statement)->statement); |
| 342 | case Node::Kind_ReturnStatement: |
| 343 | return true; |
| 344 | case Node::Kind_StatementList: { |
| 345 | for (StatementList *list = cast<StatementList *>(ast: statement); list; list = list->next) { |
| 346 | if (allCodePathsReturnInsideCase(statement: list->statement)) |
| 347 | return true; |
| 348 | } |
| 349 | return false; |
| 350 | } |
| 351 | case Node::Kind_SwitchStatement: |
| 352 | return allCodePathsReturnInsideCase(statement: cast<SwitchStatement *>(ast: statement)->block); |
| 353 | case Node::Kind_ThrowStatement: |
| 354 | return true; |
| 355 | case Node::Kind_TryStatement: { |
| 356 | auto *tryStatement = cast<TryStatement *>(ast: statement); |
| 357 | if (allCodePathsReturnInsideCase(statement: tryStatement->statement)) |
| 358 | return true; |
| 359 | return allCodePathsReturnInsideCase(statement: tryStatement->finallyExpression->statement); |
| 360 | } |
| 361 | case Node::Kind_WithStatement: |
| 362 | return allCodePathsReturnInsideCase(statement: cast<WithStatement *>(ast: statement)->statement); |
| 363 | default: |
| 364 | break; |
| 365 | } |
| 366 | return false; |
| 367 | } |
| 368 | |
| 369 | void LinterVisitor::checkCaseFallthrough(StatementList *statements, SourceLocation errorLoc, |
| 370 | SourceLocation nextLoc) |
| 371 | { |
| 372 | if (!statements || !nextLoc.isValid()) |
| 373 | return; |
| 374 | |
| 375 | if (allCodePathsReturnInsideCase(statement: statements)) |
| 376 | return; |
| 377 | |
| 378 | quint32 afterLastStatement = 0; |
| 379 | for (StatementList *it = statements; it; it = it->next) { |
| 380 | if (!it->next) { |
| 381 | afterLastStatement = it->statement->lastSourceLocation().end(); |
| 382 | } |
| 383 | } |
| 384 | |
| 385 | const auto & = m_engine->comments(); |
| 386 | auto it = std::find_if(first: comments.cbegin(), last: comments.cend(), |
| 387 | pred: [&](auto c) { return afterLastStatement < c.offset; }); |
| 388 | auto end = std::find_if(first: it, last: comments.cend(), |
| 389 | pred: [&](auto c) { return c.offset >= nextLoc.offset; }); |
| 390 | |
| 391 | for (; it != end; ++it) { |
| 392 | const QString & = m_engine->code().mid(position: it->offset, n: it->length); |
| 393 | if (commentText.contains(s: "fall through"_L1 ) |
| 394 | || commentText.contains(s: "fall-through"_L1 ) |
| 395 | || commentText.contains(s: "fallthrough"_L1 )) { |
| 396 | return; |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | m_logger->log(message: "Unterminated non-empty case block"_L1 , id: qmlUnterminatedCase, srcLocation: errorLoc); |
| 401 | } |
| 402 | |
| 403 | bool LinterVisitor::visit(QQmlJS::AST::CaseBlock *block) |
| 404 | { |
| 405 | QQmlJSImportVisitor::visit(ast: block); |
| 406 | |
| 407 | std::vector<std::pair<SourceLocation, StatementList *>> clauses; |
| 408 | for (CaseClauses *it = block->clauses; it; it = it->next) |
| 409 | clauses.push_back(x: { it->clause->caseToken, it->clause->statements }); |
| 410 | if (block->defaultClause) |
| 411 | clauses.push_back(x: { block->defaultClause->defaultToken, block->defaultClause->statements }); |
| 412 | for (CaseClauses *it = block->moreClauses; it; it = it->next) |
| 413 | clauses.push_back(x: { it->clause->caseToken, it->clause->statements }); |
| 414 | |
| 415 | // check all but the last clause for fallthrough |
| 416 | for (size_t i = 0; i < clauses.size() - 1; ++i) { |
| 417 | const SourceLocation nextToken = clauses[i + 1].first; |
| 418 | checkCaseFallthrough(statements: clauses[i].second, errorLoc: clauses[i].first, nextLoc: nextToken); |
| 419 | } |
| 420 | return true; |
| 421 | } |
| 422 | |
| 423 | /*! |
| 424 | \internal |
| 425 | |
| 426 | This assumes that there is no custom coercion enabled via \c Symbol.toPrimitive or similar. |
| 427 | */ |
| 428 | static bool isUselessExpressionStatement(ExpressionNode *ast) |
| 429 | { |
| 430 | switch (ast->kind) { |
| 431 | case Node::Kind_CallExpression: |
| 432 | case Node::Kind_DeleteExpression: |
| 433 | case Node::Kind_NewExpression: |
| 434 | case Node::Kind_PreDecrementExpression: |
| 435 | case Node::Kind_PreIncrementExpression: |
| 436 | case Node::Kind_PostDecrementExpression: |
| 437 | case Node::Kind_PostIncrementExpression: |
| 438 | case Node::Kind_YieldExpression: |
| 439 | case Node::Kind_FunctionExpression: |
| 440 | return false; |
| 441 | default: |
| 442 | break; |
| 443 | }; |
| 444 | BinaryExpression *binary = cast<BinaryExpression *>(ast); |
| 445 | if (!binary) |
| 446 | return false; |
| 447 | |
| 448 | switch (binary->op) { |
| 449 | case QSOperator::InplaceAnd: |
| 450 | case QSOperator::Assign: |
| 451 | case QSOperator::InplaceSub: |
| 452 | case QSOperator::InplaceDiv: |
| 453 | case QSOperator::InplaceExp: |
| 454 | case QSOperator::InplaceAdd: |
| 455 | case QSOperator::InplaceLeftShift: |
| 456 | case QSOperator::InplaceMod: |
| 457 | case QSOperator::InplaceMul: |
| 458 | case QSOperator::InplaceOr: |
| 459 | case QSOperator::InplaceRightShift: |
| 460 | case QSOperator::InplaceURightShift: |
| 461 | case QSOperator::InplaceXor: |
| 462 | return false; |
| 463 | default: |
| 464 | return true; |
| 465 | } |
| 466 | Q_UNREACHABLE_RETURN(true); |
| 467 | } |
| 468 | |
| 469 | static bool canHaveUselessExpressionStatement(Node *parent) |
| 470 | { |
| 471 | return parent->kind != Node::Kind_UiScriptBinding && parent->kind != Node::Kind_UiPublicMember; |
| 472 | } |
| 473 | |
| 474 | bool LinterVisitor::visit(ExpressionStatement *ast) |
| 475 | { |
| 476 | QQmlJSImportVisitor::visit(ast); |
| 477 | |
| 478 | if (canHaveUselessExpressionStatement(parent: astParentOfVisitedNode()) |
| 479 | && isUselessExpressionStatement(ast: ast->expression)) { |
| 480 | m_logger->log(message: "Expression statement has no obvious effect."_L1 , |
| 481 | id: qmlConfusingExpressionStatement, |
| 482 | srcLocation: combine(l1: ast->firstSourceLocation(), l2: ast->lastSourceLocation())); |
| 483 | } |
| 484 | |
| 485 | return true; |
| 486 | } |
| 487 | |
| 488 | QQmlJSImportVisitor::BindingExpressionParseResult LinterVisitor::parseBindingExpression( |
| 489 | const QString &name, const QQmlJS::AST::Statement *statement, |
| 490 | const QQmlJS::AST::UiPublicMember *associatedPropertyDefinition) |
| 491 | { |
| 492 | if (statement && statement->kind == (int)AST::Node::Kind::Kind_Block) { |
| 493 | const auto *block = static_cast<const AST::Block *>(statement); |
| 494 | if (!block->statements && associatedPropertyDefinition) { |
| 495 | m_logger->log(message: "Unintentional empty block, use ({}) for empty object literal"_L1 , |
| 496 | id: qmlUnintentionalEmptyBlock, |
| 497 | srcLocation: combine(l1: block->lbraceToken, l2: block->rbraceToken)); |
| 498 | } |
| 499 | } |
| 500 | |
| 501 | return QQmlJSImportVisitor::parseBindingExpression(name, statement, associatedPropertyDefinition); |
| 502 | } |
| 503 | |
| 504 | void LinterVisitor::handleLiteralBinding(const QQmlJSMetaPropertyBinding &binding, |
| 505 | const UiPublicMember *associatedPropertyDefinition) |
| 506 | { |
| 507 | if (!m_currentScope->hasOwnProperty(name: binding.propertyName())) |
| 508 | return; |
| 509 | |
| 510 | if (!associatedPropertyDefinition->isReadonly()) |
| 511 | return; |
| 512 | |
| 513 | const auto &prop = m_currentScope->property(name: binding.propertyName()); |
| 514 | const auto log = [&](const QString &preferredType) { |
| 515 | m_logger->log(message: "Prefer more specific type %1 over var"_L1 .arg(args: preferredType), |
| 516 | id: qmlPreferNonVarProperties, srcLocation: prop.sourceLocation()); |
| 517 | }; |
| 518 | |
| 519 | if (prop.typeName() != "QVariant"_L1 ) |
| 520 | return; |
| 521 | |
| 522 | switch (binding.bindingType()) { |
| 523 | case QQmlSA::BindingType::BoolLiteral: { |
| 524 | log("bool"_L1 ); |
| 525 | break; |
| 526 | } |
| 527 | case QQmlSA::BindingType::NumberLiteral: { |
| 528 | double v = binding.numberValue(); |
| 529 | auto loc = binding.sourceLocation(); |
| 530 | QStringView literal = QStringView(m_engine->code()).mid(pos: loc.offset, n: loc.length); |
| 531 | if (literal.contains(c: u'.') || double(int(v)) != v) |
| 532 | log("real or double"_L1 ); |
| 533 | else |
| 534 | log("int"_L1 ); |
| 535 | break; |
| 536 | } |
| 537 | case QQmlSA::BindingType::StringLiteral: { |
| 538 | log("string"_L1 ); |
| 539 | break; |
| 540 | } |
| 541 | default: { |
| 542 | break; |
| 543 | } |
| 544 | } |
| 545 | } |
| 546 | |
| 547 | } // namespace QQmlJS |
| 548 | |
| 549 | QT_END_NAMESPACE |
| 550 | |