// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "jsoncheck.h" #include #include #include #include #include #include using namespace QmlJS; using namespace QmlJS::AST; using namespace QmlJS::StaticAnalysis; using namespace Utils; JsonCheck::JsonCheck(Document::Ptr doc) : m_doc(doc) , m_schema(nullptr) { QTC_CHECK(m_doc->ast()); } JsonCheck::~JsonCheck() {} QList JsonCheck::operator ()(JsonSchema *schema) { QTC_ASSERT(schema, return QList()); m_schema = schema; m_analysis.push(AnalysisData()); processSchema(m_doc->ast()); const AnalysisData &analysis = m_analysis.pop(); return analysis.m_messages; } bool JsonCheck::preVisit(Node *ast) { if (!m_firstLoc.isValid()) { if (ExpressionNode *expr = ast->expressionCast()) m_firstLoc = expr->firstSourceLocation(); } m_analysis.push(AnalysisData()); return true; } void JsonCheck::postVisit(Node *) { const AnalysisData &previous = m_analysis.pop(); if (previous.m_messages.isEmpty()) analysis()->m_hasMatch = true; else analysis()->m_messages.append(previous.m_messages); analysis()->m_ranking += previous.m_ranking; } bool JsonCheck::visit(AST::TemplateLiteral *ast) { Node::accept(ast->expression, this); return true; } bool JsonCheck::visit(ObjectPattern *ast) { if (!proceedCheck(JsonValue::Object, ast->lbraceToken)) return false; analysis()->boostRanking(); const QStringList &properties = m_schema->properties(); if (properties.isEmpty()) return false; QSet propertiesFound; for (PatternPropertyList *it = ast->properties; it; it = it->next) { PatternProperty *assignment = AST::cast(it->property); StringLiteralPropertyName *literalName = cast(assignment->name); if (literalName) { const QString &propertyName = literalName->id.toString(); if (m_schema->hasPropertySchema(propertyName)) { analysis()->boostRanking(); propertiesFound.insert(propertyName); // Sec. 5.2: "... each property definition's value MUST be a schema..." m_schema->enterNestedPropertySchema(propertyName); processSchema(assignment->initializer); m_schema->leaveNestedSchema(); } else { analysis()->m_messages.append(Message(ErrInvalidPropertyName, literalName->firstSourceLocation(), propertyName, QString(), false)); } } else { analysis()->m_messages.append(Message(ErrStringValueExpected, assignment->name->firstSourceLocation(), QString(), QString(), false)); } } QStringList missing; for (const QString &property : properties) { if (!propertiesFound.contains(property)) { m_schema->enterNestedPropertySchema(property); if (m_schema->required()) missing.append(property); m_schema->leaveNestedSchema(); } } if (!missing.isEmpty()) { analysis()->m_messages.append(Message(ErrMissingRequiredProperty, ast->firstSourceLocation(), missing.join(QLatin1String(", ")), QString(), false)); } else { analysis()->boostRanking(); } return false; } bool JsonCheck::visit(ArrayPattern *ast) { if (!proceedCheck(JsonValue::Array, ast->firstSourceLocation())) return false; analysis()->boostRanking(); if (m_schema->hasItemSchema()) { // Sec. 5.5: "When this attribute value is a schema... all the items in the array MUST // be valid according to the schema." m_schema->enterNestedItemSchema(); for (PatternElementList *element = ast->elements; element; element = element->next) processSchema(element->element->initializer); m_schema->leaveNestedSchema(); } else if (m_schema->hasItemArraySchema()) { // Sec. 5.5: "When this attribute value is an array of schemas... each position in the // instance array MUST conform to the schema in the corresponding position for this array." int current = 0; const int arraySize = m_schema->itemArraySchemaSize(); for (PatternElementList *element = ast->elements; element; element = element->next, ++current) { if (current < arraySize) { if (m_schema->maybeEnterNestedArraySchema(current)) { processSchema(element->element->initializer); m_schema->leaveNestedSchema(); } else { Node::accept(element->element->initializer, this); } } else { // TODO: Handle additionalItems. } } if (current < arraySize || (current > arraySize && !m_schema->hasAdditionalItems())) { analysis()->m_messages.append(Message(ErrInvalidArrayValueLength, ast->firstSourceLocation(), QString::number(arraySize), QString(), false)); } } return false; } bool JsonCheck::visit(NullExpression *ast) { if (proceedCheck(JsonValue::Null, ast->firstSourceLocation())) return false; analysis()->boostRanking(); return false; } bool JsonCheck::visit(TrueLiteral *ast) { if (!proceedCheck(JsonValue::Boolean, ast->firstSourceLocation())) return false; analysis()->boostRanking(); return false; } bool JsonCheck::visit(FalseLiteral *ast) { if (!proceedCheck(JsonValue::Boolean, ast->firstSourceLocation())) return false; analysis()->boostRanking(); return false; } bool JsonCheck::visit(NumericLiteral *ast) { double dummy; if (std::abs(std::modf(ast->value, &dummy)) > 0.000000001) { if (!proceedCheck(JsonValue::Double, ast->firstSourceLocation())) return false; } else if (!proceedCheck(JsonValue::Int, ast->firstSourceLocation())) { return false; } analysis()->boostRanking(); if (m_schema->hasMinimum()) { double minValue = m_schema->minimum(); if (ast->value < minValue) { analysis()->m_messages.append(Message(ErrLargerNumberValueExpected, ast->firstSourceLocation(), QString::number(minValue), QString(), false)); } else if (m_schema->hasExclusiveMinimum() && std::abs(ast->value - minValue) > 0.000000001) { analysis()->m_messages.append(Message(ErrMinimumNumberValueIsExclusive, ast->firstSourceLocation(), QString(), QString(), false)); } else { analysis()->boostRanking(); } } if (m_schema->hasMaximum()) { double maxValue = m_schema->maximum(); if (ast->value > maxValue) { analysis()->m_messages.append(Message(ErrSmallerNumberValueExpected, ast->firstSourceLocation(), QString::number(maxValue), QString(), false)); } else if (m_schema->hasExclusiveMaximum()) { analysis()->m_messages.append(Message(ErrMaximumNumberValueIsExclusive, ast->firstSourceLocation(), QString(), QString(), false)); } else { analysis()->boostRanking(); } } return false; } bool JsonCheck::visit(StringLiteral *ast) { if (!proceedCheck(JsonValue::String, ast->firstSourceLocation())) return false; analysis()->boostRanking(); const QStringView literal = ast->value; const QString &pattern = m_schema->pattern(); if (!pattern.isEmpty()) { const QRegularExpression regExp(pattern); if (regExp.match(literal.toString()).hasMatch()) { analysis()->m_messages.append(Message(ErrInvalidStringValuePattern, ast->firstSourceLocation(), QString(), QString(), false)); return false; } analysis()->boostRanking(3); // Treat string patterns with higher weight. } int expectedLength = m_schema->minimumLength(); if (expectedLength != -1) { if (literal.length() < expectedLength) { analysis()->m_messages.append(Message(ErrLongerStringValueExpected, ast->firstSourceLocation(), QString::number(expectedLength), QString(), false)); } else { analysis()->boostRanking(); } } expectedLength = m_schema->maximumLength(); if (expectedLength != -1) { if (literal.length() > expectedLength) { analysis()->m_messages.append(Message(ErrShorterStringValueExpected, ast->firstSourceLocation(), QString::number(expectedLength), QString(), false)); } else { analysis()->boostRanking(); } } return false; } void JsonCheck::throwRecursionDepthError() { analysis()->m_messages.append(Message(ErrHitMaximumRecursion, SourceLocation(), QString(), QString(), false)); } static QString formatExpectedTypes(QStringList all) { all.removeDuplicates(); return all.join(QLatin1String(", or ")); } void JsonCheck::processSchema(Node *ast) { if (m_schema->hasTypeSchema()) { m_schema->enterNestedTypeSchema(); processSchema(ast); m_schema->leaveNestedSchema(); } else if (m_schema->hasUnionSchema()) { // Sec. 5.1: "... value is valid if it is of the same type as one of the simple // type definitions, or valid by one of the schemas, in the array." int bestRank = 0; QList bestErrorGuess; int current = 0; const int unionSize = m_schema->unionSchemaSize(); m_analysis.push(AnalysisData()); for (; current < unionSize; ++current) { if (m_schema->maybeEnterNestedUnionSchema(current)) { processSchema(ast); m_schema->leaveNestedSchema(); } else { Node::accept(ast, this); } if (analysis()->m_hasMatch) break; if (analysis()->m_ranking >= bestRank) { bestRank = analysis()->m_ranking; bestErrorGuess = analysis()->m_messages; } analysis()->m_ranking = 0; analysis()->m_messages.clear(); } m_analysis.pop(); if (current == unionSize) { // When we don't have a match for a union typed schema, we try to "guess" which // particular item from the union the user tried to represent. The one with the best // ranking wins. if (bestRank > 0) { analysis()->m_messages.append(bestErrorGuess); } else { analysis()->m_messages.append(Message(ErrDifferentValueExpected, ast->firstSourceLocation(), formatExpectedTypes(m_schema->validTypes()), QString(), false)); } } } else { Node::accept(ast, this); } } bool JsonCheck::proceedCheck(JsonValue::Kind kind, const SourceLocation &location) { if (!m_firstLoc.isValid()) return false; if (!m_schema->isTypeConstrained()) return false; if (!m_schema->acceptsType(JsonValue::kindToString(kind))) { analysis()->m_messages.append(Message(ErrDifferentValueExpected, location, formatExpectedTypes(m_schema->validTypes()), QString(), false)); return false; } return true; } JsonCheck::AnalysisData *JsonCheck::analysis() { return &m_analysis.top(); }