// 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 #include #include using namespace QmlJS::AST; using namespace QmlJS::StaticAnalysis; using namespace Utils; namespace QmlJS { 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 {}); 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(); } JsonMemoryPool::~JsonMemoryPool() { for (char *obj : std::as_const(_objs)) { reinterpret_cast(obj)->~JsonValue(); delete[] obj; } } JsonValue::JsonValue(Kind kind) : m_kind(kind) {} JsonValue::~JsonValue() = default; JsonValue *JsonValue::create(const QString &s, JsonMemoryPool *pool) { const QJsonDocument document = QJsonDocument::fromJson(s.toUtf8()); if (document.isNull()) return nullptr; return build(document.toVariant(), pool); } void *JsonValue::operator new(size_t size, JsonMemoryPool *pool) { return pool->allocate(size); } void JsonValue::operator delete(void *) { } void JsonValue::operator delete(void *, JsonMemoryPool *) { } QString JsonValue::kindToString(JsonValue::Kind kind) { if (kind == String) return QLatin1String("string"); if (kind == Double) return QLatin1String("number"); if (kind == Int) return QLatin1String("integer"); if (kind == Object) return QLatin1String("object"); if (kind == Array) return QLatin1String("array"); if (kind == Boolean) return QLatin1String("boolean"); if (kind == Null) return QLatin1String("null"); return QLatin1String("unknown"); } JsonValue *JsonValue::build(const QVariant &variant, JsonMemoryPool *pool) { switch (variant.typeId()) { case QMetaType::QVariantList: { auto newValue = new (pool) JsonArrayValue; const QList list = variant.toList(); for (const QVariant &element : list) newValue->addElement(build(element, pool)); return newValue; } case QMetaType::QVariantMap: { auto newValue = new (pool) JsonObjectValue; const QVariantMap variantMap = variant.toMap(); for (QVariantMap::const_iterator it = variantMap.begin(); it != variantMap.end(); ++it) newValue->addMember(it.key(), build(it.value(), pool)); return newValue; } case QMetaType::QString: return new (pool) JsonStringValue(variant.toString()); case QMetaType::Int: return new (pool) JsonIntValue(variant.toInt()); case QMetaType::Double: return new (pool) JsonDoubleValue(variant.toDouble()); case QMetaType::Bool: return new (pool) JsonBooleanValue(variant.toBool()); case QMetaType::UnknownType: return new (pool) JsonNullValue; default: break; } return nullptr; } /////////////////////////////////////////////////////////////////////////////// QString JsonSchema::kType() { return QStringLiteral("type"); } QString JsonSchema::kProperties() { return QStringLiteral("properties"); } QString JsonSchema::kPatternProperties() { return QStringLiteral("patternProperties"); } QString JsonSchema::kAdditionalProperties() { return QStringLiteral("additionalProperties"); } QString JsonSchema::kItems() { return QStringLiteral("items"); } QString JsonSchema::kAdditionalItems() { return QStringLiteral("additionalItems"); } QString JsonSchema::kRequired() { return QStringLiteral("required"); } QString JsonSchema::kDependencies() { return QStringLiteral("dependencies"); } QString JsonSchema::kMinimum() { return QStringLiteral("minimum"); } QString JsonSchema::kMaximum() { return QStringLiteral("maximum"); } QString JsonSchema::kExclusiveMinimum() { return QStringLiteral("exclusiveMinimum"); } QString JsonSchema::kExclusiveMaximum() { return QStringLiteral("exclusiveMaximum"); } QString JsonSchema::kMinItems() { return QStringLiteral("minItems"); } QString JsonSchema::kMaxItems() { return QStringLiteral("maxItems"); } QString JsonSchema::kUniqueItems() { return QStringLiteral("uniqueItems"); } QString JsonSchema::kPattern() { return QStringLiteral("pattern"); } QString JsonSchema::kMinLength() { return QStringLiteral("minLength"); } QString JsonSchema::kMaxLength() { return QStringLiteral("maxLength"); } QString JsonSchema::kTitle() { return QStringLiteral("title"); } QString JsonSchema::kDescription() { return QStringLiteral("description"); } QString JsonSchema::kExtends() { return QStringLiteral("extends"); } QString JsonSchema::kRef() { return QStringLiteral("$ref"); } JsonSchema::JsonSchema(JsonObjectValue *rootObject, const JsonSchemaManager *manager) : m_manager(manager) { enter(rootObject); } bool JsonSchema::isTypeConstrained() const { // Simple types if (JsonStringValue *sv = getStringValue(kType(), currentValue())) return isCheckableType(sv->value()); // Union types if (JsonArrayValue *av = getArrayValue(kType(), currentValue())) { QTC_ASSERT(currentIndex() != -1, return false); QTC_ASSERT(av->elements().at(currentIndex())->kind() == JsonValue::String, return false); JsonStringValue *sv = av->elements().at(currentIndex())->toString(); return isCheckableType(sv->value()); } return false; } bool JsonSchema::acceptsType(const QString &type) const { // Simple types if (JsonStringValue *sv = getStringValue(kType(), currentValue())) return typeMatches(sv->value(), type); // Union types if (JsonArrayValue *av = getArrayValue(kType(), currentValue())) { QTC_ASSERT(currentIndex() != -1, return false); QTC_ASSERT(av->elements().at(currentIndex())->kind() == JsonValue::String, return false); JsonStringValue *sv = av->elements().at(currentIndex())->toString(); return typeMatches(sv->value(), type); } return false; } QStringList JsonSchema::validTypes(JsonObjectValue *v) { QStringList all; if (JsonStringValue *sv = getStringValue(kType(), v)) all.append(sv->value()); if (JsonObjectValue *ov = getObjectValue(kType(), v)) return validTypes(ov); if (JsonArrayValue *av = getArrayValue(kType(), v)) { const QList elements = av->elements(); for (JsonValue *v : elements) { if (JsonStringValue *sv = v->toString()) all.append(sv->value()); else if (JsonObjectValue *ov = v->toObject()) all.append(validTypes(ov)); } } return all; } bool JsonSchema::typeMatches(const QString &expected, const QString &actual) { if (expected == QLatin1String("number") && actual == QLatin1String("integer")) return true; return expected == actual; } bool JsonSchema::isCheckableType(const QString &s) { return s == QLatin1String("string") || s == QLatin1String("number") || s == QLatin1String("integer") || s == QLatin1String("boolean") || s == QLatin1String("object") || s == QLatin1String("array") || s == QLatin1String("null"); } QStringList JsonSchema::validTypes() const { return validTypes(currentValue()); } bool JsonSchema::hasTypeSchema() const { return getObjectValue(kType(), currentValue()); } void JsonSchema::enterNestedTypeSchema() { QTC_ASSERT(hasTypeSchema(), return); enter(getObjectValue(kType(), currentValue())); } QStringList JsonSchema::properties(JsonObjectValue *v) const { using Members = QHash; QStringList all; if (JsonObjectValue *ov = getObjectValue(kProperties(), v)) { const Members members = ov->members(); const Members::ConstIterator cend = members.constEnd(); for (Members::ConstIterator it = members.constBegin(); it != cend; ++it) if (hasPropertySchema(it.key())) all.append(it.key()); } if (JsonObjectValue *base = resolveBase(v)) all.append(properties(base)); return all; } QStringList JsonSchema::properties() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Object)), return {}); return properties(currentValue()); } JsonObjectValue *JsonSchema::propertySchema(const QString &property, JsonObjectValue *v) const { if (JsonObjectValue *ov = getObjectValue(kProperties(), v)) { JsonValue *member = ov->member(property); if (member && member->kind() == JsonValue::Object) return member->toObject(); } if (JsonObjectValue *base = resolveBase(v)) return propertySchema(property, base); return nullptr; } bool JsonSchema::hasPropertySchema(const QString &property) const { return propertySchema(property, currentValue()); } void JsonSchema::enterNestedPropertySchema(const QString &property) { QTC_ASSERT(hasPropertySchema(property), return); JsonObjectValue *schema = propertySchema(property, currentValue()); enter(schema); } /*! * An array schema is allowed to have its \e items specification in the form of * another schema * or in the form of an array of schemas [Sec. 5.5]. This functions checks whether this is case * in which the items are a schema. * * Returns whether or not the items from the array are a schema. */ bool JsonSchema::hasItemSchema() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Array)), return false); return getObjectValue(kItems(), currentValue()); } void JsonSchema::enterNestedItemSchema() { QTC_ASSERT(hasItemSchema(), return); enter(getObjectValue(kItems(), currentValue())); } /*! * An array schema is allowed to have its \e items specification in the form of another schema * or in the form of an array of schemas [Sec. 5.5]. This functions checks whether this is case * in which the items are an array of schemas. * * Returns whether or not the items from the array are a an array of schemas. */ bool JsonSchema::hasItemArraySchema() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Array)), return false); return getArrayValue(kItems(), currentValue()); } int JsonSchema::itemArraySchemaSize() const { QTC_ASSERT(hasItemArraySchema(), return false); return getArrayValue(kItems(), currentValue())->size(); } /*! * When evaluating the items of an array it might be necessary to \e enter a * particular schema, * since this API assumes that there's always a valid schema in context (the one the user is * interested on). This shall only happen if the item at the supplied array index is of type * object, which is then assumed to be a schema. * * The function also marks the context as being inside an array evaluation. * * Returns whether it was necessary to enter a schema for the supplied * array \a index, false if index is out of bounds. */ bool JsonSchema::maybeEnterNestedArraySchema(int index) { QTC_ASSERT(itemArraySchemaSize(), return false); QTC_ASSERT(index >= 0 && index < itemArraySchemaSize(), return false); JsonValue *v = getArrayValue(kItems(), currentValue())->elements().at(index); return maybeEnter(v, Array, index); } /*! * The type of a schema can be specified in the form of a union type, which is basically an * array of allowed types for the particular instance [Sec. 5.1]. This function checks whether * the current schema is one of such. * * Returns whether or not the current schema specifies a union type. */ bool JsonSchema::hasUnionSchema() const { return getArrayValue(kType(), currentValue()); } int JsonSchema::unionSchemaSize() const { return getArrayValue(kType(), currentValue())->size(); } /*! * When evaluating union types it might be necessary to enter a particular * schema, since this * API assumes that there's always a valid schema in context (the one the user is interested on). * This shall only happen if the item at the supplied union \a index, which is then assumed to be * a schema. * * The function also marks the context as being inside an union evaluation. * * Returns whether or not it was necessary to enter a schema for the * supplied union index. */ bool JsonSchema::maybeEnterNestedUnionSchema(int index) { QTC_ASSERT(unionSchemaSize(), return false); QTC_ASSERT(index >= 0 && index < unionSchemaSize(), return false); JsonValue *v = getArrayValue(kType(), currentValue())->elements().at(index); return maybeEnter(v, Union, index); } void JsonSchema::leaveNestedSchema() { QTC_ASSERT(!m_schemas.isEmpty(), return); leave(); } bool JsonSchema::required() const { if (JsonBooleanValue *bv = getBooleanValue(kRequired(), currentValue())) return bv->value(); return false; } bool JsonSchema::hasMinimum() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Int)), return false); return getDoubleValue(kMinimum(), currentValue()); } double JsonSchema::minimum() const { QTC_ASSERT(hasMinimum(), return 0); return getDoubleValue(kMinimum(), currentValue())->value(); } bool JsonSchema::hasExclusiveMinimum() { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Int)), return false); if (JsonBooleanValue *bv = getBooleanValue(kExclusiveMinimum(), currentValue())) return bv->value(); return false; } bool JsonSchema::hasMaximum() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Int)), return false); return getDoubleValue(kMaximum(), currentValue()); } double JsonSchema::maximum() const { QTC_ASSERT(hasMaximum(), return 0); return getDoubleValue(kMaximum(), currentValue())->value(); } bool JsonSchema::hasExclusiveMaximum() { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Int)), return false); if (JsonBooleanValue *bv = getBooleanValue(kExclusiveMaximum(), currentValue())) return bv->value(); return false; } QString JsonSchema::pattern() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::String)), return QString()); if (JsonStringValue *sv = getStringValue(kPattern(), currentValue())) return sv->value(); return QString(); } int JsonSchema::minimumLength() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::String)), return -1); if (JsonDoubleValue *dv = getDoubleValue(kMinLength(), currentValue())) return dv->value(); return -1; } int JsonSchema::maximumLength() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::String)), return -1); if (JsonDoubleValue *dv = getDoubleValue(kMaxLength(), currentValue())) return dv->value(); return -1; } bool JsonSchema::hasAdditionalItems() const { QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Array)), return false); return currentValue()->member(kAdditionalItems()); } bool JsonSchema::maybeSchemaName(const QString &s) { if (s.isEmpty() || s == QLatin1String("any")) return false; return !isCheckableType(s); } JsonObjectValue *JsonSchema::rootValue() const { QTC_ASSERT(!m_schemas.isEmpty(), return nullptr); return m_schemas.first().m_value; } JsonObjectValue *JsonSchema::currentValue() const { QTC_ASSERT(!m_schemas.isEmpty(), return nullptr); return m_schemas.last().m_value; } int JsonSchema::currentIndex() const { QTC_ASSERT(!m_schemas.isEmpty(), return 0); return m_schemas.last().m_index; } void JsonSchema::evaluate(EvaluationMode eval, int index) { QTC_ASSERT(!m_schemas.isEmpty(), return); m_schemas.last().m_eval = eval; m_schemas.last().m_index = index; } void JsonSchema::enter(JsonObjectValue *ov, EvaluationMode eval, int index) { Context context; context.m_eval = eval; context.m_index = index; context.m_value = resolveReference(ov); m_schemas.push_back(context); } bool JsonSchema::maybeEnter(JsonValue *v, EvaluationMode eval, int index) { evaluate(eval, index); if (v->kind() == JsonValue::Object) { enter(v->toObject()); return true; } if (v->kind() == JsonValue::String) { const QString &s = v->toString()->value(); if (maybeSchemaName(s)) { JsonSchema *schema = m_manager->schemaByName(s); if (schema) { enter(schema->rootValue()); return true; } } } return false; } void JsonSchema::leave() { QTC_ASSERT(!m_schemas.isEmpty(), return); m_schemas.pop_back(); } JsonObjectValue *JsonSchema::resolveReference(JsonObjectValue *ov) const { if (JsonStringValue *sv = getStringValue(kRef(), ov)) { JsonSchema *referenced = m_manager->schemaByName(sv->value()); if (referenced) return referenced->rootValue(); } return ov; } JsonObjectValue *JsonSchema::resolveBase(JsonObjectValue *ov) const { if (JsonValue *v = ov->member(kExtends())) { if (v->kind() == JsonValue::String) { JsonSchema *schema = m_manager->schemaByName(v->toString()->value()); if (schema) return schema->rootValue(); } else if (v->kind() == JsonValue::Object) { return resolveReference(v->toObject()); } } return nullptr; } JsonStringValue *JsonSchema::getStringValue(const QString &name, JsonObjectValue *value) { JsonValue *v = value->member(name); if (!v) return nullptr; return v->toString(); } JsonObjectValue *JsonSchema::getObjectValue(const QString &name, JsonObjectValue *value) { JsonValue *v = value->member(name); if (!v) return nullptr; return v->toObject(); } JsonBooleanValue *JsonSchema::getBooleanValue(const QString &name, JsonObjectValue *value) { JsonValue *v = value->member(name); if (!v) return nullptr; return v->toBoolean(); } JsonArrayValue *JsonSchema::getArrayValue(const QString &name, JsonObjectValue *value) { JsonValue *v = value->member(name); if (!v) return nullptr; return v->toArray(); } JsonDoubleValue *JsonSchema::getDoubleValue(const QString &name, JsonObjectValue *value) { JsonValue *v = value->member(name); if (!v) return nullptr; return v->toDouble(); } /////////////////////////////////////////////////////////////////////////////// JsonSchemaManager::JsonSchemaManager(const FilePaths &searchPaths) : m_searchPaths(searchPaths) { for (const FilePath &path : searchPaths) { if (!path.isReadableDir()) continue; const FilePaths entries = path.dirEntries(QStringList{QLatin1String("*.json")}); for (const FilePath &entry : entries) m_schemas.insert(entry.baseName(), JsonSchemaData(entry.absoluteFilePath())); } } JsonSchemaManager::~JsonSchemaManager() { for (const JsonSchemaData &schemaData : std::as_const(m_schemas)) delete schemaData.m_schema; } /*! * Tries to find a JSON schema to validate \a fileName against. According * to the specification, how the schema/instance association is done is implementation defined. * Currently we use a quite naive approach which is simply based on file names. Specifically, * if one opens a foo.json file we'll look for a schema named foo.json. We should probably * investigate alternative settings later. * * Returns a valid schema or 0. */ JsonSchema *JsonSchemaManager::schemaForFile(const FilePath &filePath) const { return schemaByName(filePath.baseName()); } JsonSchema *JsonSchemaManager::schemaByName(const QString &baseName) const { QHash::iterator it = m_schemas.find(baseName); if (it == m_schemas.end()) { for (const FilePath &path : m_searchPaths) { const FilePath candidate = path.pathAppended(baseName + ".json"); if (candidate.exists()) { m_schemas.insert(baseName, candidate.absoluteFilePath()); break; } } } it = m_schemas.find(baseName); if (it == m_schemas.end()) return nullptr; JsonSchemaData *schemaData = &it.value(); if (!schemaData->m_schema) { // Schemas are built on-demand. const FilePath currentSchema = schemaData->m_absoluteFilePath; if (schemaData->m_lastParseAttempt.isNull() || schemaData->m_lastParseAttempt < currentSchema.lastModified()) { schemaData->m_schema = parseSchema(currentSchema.absoluteFilePath()); } } return schemaData->m_schema; } JsonSchema *JsonSchemaManager::parseSchema(const FilePath &schemaFileName) const { if (Result contents = schemaFileName.fileContents()) { JsonValue *json = JsonValue::create(QString::fromUtf8(*contents), &m_pool); if (json && json->kind() == JsonValue::Object) return new JsonSchema(json->toObject(), this); } return nullptr; } } // QmlJs