diff --git a/src/libs/qmljs/jsoncheck.cpp b/src/libs/qmljs/jsoncheck.cpp index 366c150c95c..1d7c04e50cf 100644 --- a/src/libs/qmljs/jsoncheck.cpp +++ b/src/libs/qmljs/jsoncheck.cpp @@ -4,19 +4,24 @@ #include "jsoncheck.h" #include + +#include #include #include +#include +#include #include #include #include -using namespace QmlJS; using namespace QmlJS::AST; using namespace QmlJS::StaticAnalysis; using namespace Utils; +namespace QmlJS { + JsonCheck::JsonCheck(Document::Ptr doc) : m_doc(doc) , m_schema(nullptr) @@ -384,3 +389,716 @@ 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 QVariant::List: { + auto newValue = new (pool) JsonArrayValue; + const QList list = variant.toList(); + for (const QVariant &element : list) + newValue->addElement(build(element, pool)); + return newValue; + } + + case QVariant::Map: { + 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 QVariant::String: + return new (pool) JsonStringValue(variant.toString()); + + case QVariant::Int: + return new (pool) JsonIntValue(variant.toInt()); + + case QVariant::Double: + return new (pool) JsonDoubleValue(variant.toDouble()); + + case QVariant::Bool: + return new (pool) JsonBooleanValue(variant.toBool()); + + case QVariant::Invalid: + 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 QStringList &searchPaths) + : m_searchPaths(searchPaths) +{ + for (const QString &path : searchPaths) { + QDir dir(path); + if (!dir.exists()) + continue; + dir.setNameFilters(QStringList(QLatin1String("*.json"))); + const QList entries = dir.entryInfoList(); + for (const QFileInfo &fi : entries) + m_schemas.insert(fi.baseName(), JsonSchemaData(fi.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 QString &fileName) const +{ + QString baseName(QFileInfo(fileName).baseName()); + + return schemaByName(baseName); +} + +JsonSchema *JsonSchemaManager::schemaByName(const QString &baseName) const +{ + QHash::iterator it = m_schemas.find(baseName); + if (it == m_schemas.end()) { + for (const QString &path : m_searchPaths) { + QFileInfo candidate(path + 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. + QFileInfo currentSchema(schemaData->m_absoluteFileName); + Q_ASSERT(currentSchema.exists()); + if (schemaData->m_lastParseAttempt.isNull() + || schemaData->m_lastParseAttempt < currentSchema.lastModified()) { + schemaData->m_schema = parseSchema(currentSchema.absoluteFilePath()); + } + } + + return schemaData->m_schema; +} + +JsonSchema *JsonSchemaManager::parseSchema(const QString &schemaFileName) const +{ + FileReader reader; + if (reader.fetch(FilePath::fromString(schemaFileName), QIODevice::Text)) { + const QString &contents = QString::fromUtf8(reader.data()); + JsonValue *json = JsonValue::create(contents, &m_pool); + if (json && json->kind() == JsonValue::Object) + return new JsonSchema(json->toObject(), this); + } + + return nullptr; +} + +} // QmlJs diff --git a/src/libs/qmljs/jsoncheck.h b/src/libs/qmljs/jsoncheck.h index 6050ff72897..261c349cc30 100644 --- a/src/libs/qmljs/jsoncheck.h +++ b/src/libs/qmljs/jsoncheck.h @@ -9,21 +9,397 @@ #include #include -#include - #include #include - namespace QmlJS { +class JsonStringValue; +class JsonDoubleValue; +class JsonIntValue; +class JsonObjectValue; +class JsonArrayValue; +class JsonBooleanValue; +class JsonNullValue; + +class QMLJS_EXPORT JsonMemoryPool +{ +public: + ~JsonMemoryPool(); + + inline void *allocate(size_t size) + { + auto obj = new char[size]; + _objs.append(obj); + return obj; + } + +private: + QList _objs; +}; + +/*! + * \brief The JsonValue class + */ +class QMLJS_EXPORT JsonValue +{ +public: + enum Kind { + String, + Double, + Int, + Object, + Array, + Boolean, + Null, + Unknown + }; + + virtual ~JsonValue(); + + Kind kind() const { return m_kind; } + static QString kindToString(Kind kind); + + virtual JsonStringValue *toString() { return nullptr; } + virtual JsonDoubleValue *toDouble() { return nullptr; } + virtual JsonIntValue *toInt() { return nullptr; } + virtual JsonObjectValue *toObject() { return nullptr; } + virtual JsonArrayValue *toArray() { return nullptr; } + virtual JsonBooleanValue *toBoolean() { return nullptr; } + virtual JsonNullValue *toNull() { return nullptr; } + + static JsonValue *create(const QString &s, JsonMemoryPool *pool); + void *operator new(size_t size, JsonMemoryPool *pool); + void operator delete(void *); + void operator delete(void *, JsonMemoryPool *); + +protected: + JsonValue(Kind kind); + +private: + static JsonValue *build(const QVariant &varixant, JsonMemoryPool *pool); + + Kind m_kind; +}; + + +/*! + * \brief The JsonStringValue class + */ +class QMLJS_EXPORT JsonStringValue : public JsonValue +{ +public: + JsonStringValue(const QString &value) + : JsonValue(String) + , m_value(value) + {} + + JsonStringValue *toString() override { return this; } + + const QString &value() const { return m_value; } + +private: + QString m_value; +}; + + +/*! + * \brief The JsonDoubleValue class + */ +class QMLJS_EXPORT JsonDoubleValue : public JsonValue +{ +public: + JsonDoubleValue(double value) + : JsonValue(Double) + , m_value(value) + {} + + JsonDoubleValue *toDouble() override { return this; } + + double value() const { return m_value; } + +private: + double m_value; +}; + +/*! + * \brief The JsonIntValue class + */ +class QMLJS_EXPORT JsonIntValue : public JsonValue +{ +public: + JsonIntValue(int value) + : JsonValue(Int) + , m_value(value) + {} + + JsonIntValue *toInt() override { return this; } + + int value() const { return m_value; } + +private: + int m_value; +}; + + +/*! + * \brief The JsonObjectValue class + */ +class QMLJS_EXPORT JsonObjectValue : public JsonValue +{ +public: + JsonObjectValue() + : JsonValue(Object) + {} + + JsonObjectValue *toObject() override { return this; } + + void addMember(const QString &name, JsonValue *value) { m_members.insert(name, value); } + bool hasMember(const QString &name) const { return m_members.contains(name); } + JsonValue *member(const QString &name) const { return m_members.value(name); } + QHash members() const { return m_members; } + bool isEmpty() const { return m_members.isEmpty(); } + +protected: + JsonObjectValue(Kind kind) + : JsonValue(kind) + {} + +private: + QHash m_members; +}; + + +/*! + * \brief The JsonArrayValue class + */ +class QMLJS_EXPORT JsonArrayValue : public JsonValue +{ +public: + JsonArrayValue() + : JsonValue(Array) + {} + + JsonArrayValue *toArray() override { return this; } + + void addElement(JsonValue *value) { m_elements.append(value); } + QList elements() const { return m_elements; } + int size() const { return m_elements.size(); } + +private: + QList m_elements; +}; + + +/*! + * \brief The JsonBooleanValue class + */ +class QMLJS_EXPORT JsonBooleanValue : public JsonValue +{ +public: + JsonBooleanValue(bool value) + : JsonValue(Boolean) + , m_value(value) + {} + + JsonBooleanValue *toBoolean() override { return this; } + + bool value() const { return m_value; } + +private: + bool m_value; +}; + +class QMLJS_EXPORT JsonNullValue : public JsonValue +{ +public: + JsonNullValue() + : JsonValue(Null) + {} + + JsonNullValue *toNull() override { return this; } +}; + +class JsonSchemaManager; + +/*! + * \brief The JsonSchema class + * + * [NOTE: This is an incomplete implementation and a work in progress.] + * + * This class provides an interface for traversing and evaluating a JSON schema, as described + * in the draft http://tools.ietf.org/html/draft-zyp-json-schema-03. + * + * JSON schemas are recursive in concept. This means that a particular attribute from a schema + * might be also another schema. Therefore, the basic working principle of this API is that + * from within some schema, one can investigate its attributes and if necessary "enter" a + * corresponding nested schema. Afterwards, it's expected that one would "leave" such nested + * schema. + * + * All functions assume that the current "context" is a valid schema. Once an instance of this + * class is created the root schema is put on top of the stack. + * + */ +class QMLJS_EXPORT JsonSchema +{ +public: + bool isTypeConstrained() const; + bool acceptsType(const QString &type) const; + QStringList validTypes() const; + + // Applicable on schemas of any type. + bool required() const; + + bool hasTypeSchema() const; + void enterNestedTypeSchema(); + + bool hasUnionSchema() const; + int unionSchemaSize() const; + bool maybeEnterNestedUnionSchema(int index); + + void leaveNestedSchema(); + + // Applicable on schemas of type number/integer. + bool hasMinimum() const; + bool hasMaximum() const; + bool hasExclusiveMinimum(); + bool hasExclusiveMaximum(); + double minimum() const; + double maximum() const; + + // Applicable on schemas of type string. + QString pattern() const; + int minimumLength() const; + int maximumLength() const; + + // Applicable on schemas of type object. + QStringList properties() const; + bool hasPropertySchema(const QString &property) const; + void enterNestedPropertySchema(const QString &property); + + // Applicable on schemas of type array. + bool hasAdditionalItems() const; + + bool hasItemSchema() const; + void enterNestedItemSchema(); + + bool hasItemArraySchema() const; + int itemArraySchemaSize() const; + bool maybeEnterNestedArraySchema(int index); + +private: + friend class JsonSchemaManager; + JsonSchema(JsonObjectValue *rootObject, const JsonSchemaManager *manager); + Q_DISABLE_COPY(JsonSchema) + + enum EvaluationMode { + Normal, + Array, + Union + }; + + void enter(JsonObjectValue *ov, EvaluationMode eval = Normal, int index = -1); + bool maybeEnter(JsonValue *v, EvaluationMode eval, int index); + void evaluate(EvaluationMode eval, int index); + void leave(); + + JsonObjectValue *resolveReference(JsonObjectValue *ov) const; + JsonObjectValue *resolveBase(JsonObjectValue *ov) const; + + JsonObjectValue *currentValue() const; + int currentIndex() const; + + JsonObjectValue *rootValue() const; + + static JsonStringValue *getStringValue(const QString &name, JsonObjectValue *value); + static JsonObjectValue *getObjectValue(const QString &name, JsonObjectValue *value); + static JsonBooleanValue *getBooleanValue(const QString &name, JsonObjectValue *value); + static JsonArrayValue *getArrayValue(const QString &name, JsonObjectValue *value); + static JsonDoubleValue *getDoubleValue(const QString &name, JsonObjectValue *value); + + static QStringList validTypes(JsonObjectValue *v); + static bool typeMatches(const QString &expected, const QString &actual); + static bool isCheckableType(const QString &s); + + QStringList properties(JsonObjectValue *v) const; + JsonObjectValue *propertySchema(const QString &property, JsonObjectValue *v) const; + // TODO: Similar functions for other attributes which require looking into base schemas. + + static bool maybeSchemaName(const QString &s); + + static QString kType(); + static QString kProperties(); + static QString kPatternProperties(); + static QString kAdditionalProperties(); + static QString kItems(); + static QString kAdditionalItems(); + static QString kRequired(); + static QString kDependencies(); + static QString kMinimum(); + static QString kMaximum(); + static QString kExclusiveMinimum(); + static QString kExclusiveMaximum(); + static QString kMinItems(); + static QString kMaxItems(); + static QString kUniqueItems(); + static QString kPattern(); + static QString kMinLength(); + static QString kMaxLength(); + static QString kTitle(); + static QString kDescription(); + static QString kExtends(); + static QString kRef(); + + struct Context + { + JsonObjectValue *m_value; + EvaluationMode m_eval; + int m_index; + }; + + QList m_schemas; + const JsonSchemaManager *m_manager; +}; + + +/*! + * \brief The JsonSchemaManager class + */ +class QMLJS_EXPORT JsonSchemaManager +{ +public: + JsonSchemaManager(const QStringList &searchPaths); + ~JsonSchemaManager(); + + JsonSchema *schemaForFile(const QString &fileName) const; + JsonSchema *schemaByName(const QString &baseName) const; + +private: + struct JsonSchemaData + { + JsonSchemaData(const QString &absoluteFileName, JsonSchema *schema = nullptr) + : m_absoluteFileName(absoluteFileName) + , m_schema(schema) + {} + QString m_absoluteFileName; + JsonSchema *m_schema; + QDateTime m_lastParseAttempt; + }; + + JsonSchema *parseSchema(const QString &schemaFileName) const; + + QStringList m_searchPaths; + mutable QHash m_schemas; + mutable JsonMemoryPool m_pool; +}; + class QMLJS_EXPORT JsonCheck : public AST::Visitor { public: JsonCheck(Document::Ptr doc); ~JsonCheck(); - QList operator()(Utils::JsonSchema *schema); + QList operator()(JsonSchema *schema); private: bool preVisit(AST::Node *) override; @@ -52,14 +428,14 @@ private: }; void processSchema(AST::Node *ast); - bool proceedCheck(Utils::JsonValue::Kind kind, const SourceLocation &location); + bool proceedCheck(JsonValue::Kind kind, const SourceLocation &location); AnalysisData *analysis(); Document::Ptr m_doc; SourceLocation m_firstLoc; - Utils::JsonSchema *m_schema; + JsonSchema *m_schema; QStack m_analysis; }; -} // QmlJs +} // QmlJS diff --git a/src/libs/qmljs/qmljsbundle.cpp b/src/libs/qmljs/qmljsbundle.cpp index e4d536a3da2..28f88fca452 100644 --- a/src/libs/qmljs/qmljsbundle.cpp +++ b/src/libs/qmljs/qmljsbundle.cpp @@ -3,7 +3,7 @@ #include "qmljsbundle.h" -#include +#include "jsoncheck.h" #include #include @@ -11,6 +11,7 @@ #include #include + namespace QmlJS { typedef PersistentTrie::Trie Trie; @@ -45,19 +46,16 @@ Trie QmlBundle::implicitImports() const return m_implicitImports; } - Trie QmlBundle::supportedImports() const { return m_supportedImports; } - void QmlBundle::merge(const QmlBundle &o) { *this = mergeF(o); } - void QmlBundle::intersect(const QmlBundle &o) { *this = intersectF(o); @@ -186,7 +184,7 @@ QString QmlBundle::toString(const QString &indent) return res; } -QStringList QmlBundle::maybeReadTrie(Trie &trie, Utils::JsonObjectValue *config, +QStringList QmlBundle::maybeReadTrie(Trie &trie, JsonObjectValue *config, const QString &path, const QString &propertyName, bool required, bool stripVersions) { @@ -198,12 +196,13 @@ QStringList QmlBundle::maybeReadTrie(Trie &trie, Utils::JsonObjectValue *config, path); return res; } - Utils::JsonValue *imp0 = config->member(propertyName); - Utils::JsonArrayValue *imp = ((imp0 != nullptr) ? imp0->toArray() : nullptr); + + JsonValue *imp0 = config->member(propertyName); + JsonArrayValue *imp = ((imp0 != nullptr) ? imp0->toArray() : nullptr); if (imp != nullptr) { - const QList elements = imp->elements(); - for (Utils::JsonValue *v : elements) { - Utils::JsonStringValue *impStr = ((v != nullptr) ? v->toString() : nullptr); + const QList elements = imp->elements(); + for (JsonValue *v : elements) { + JsonStringValue *impStr = ((v != nullptr) ? v->toString() : nullptr); if (impStr != nullptr) { QString value = impStr->value(); if (stripVersions) { @@ -228,9 +227,8 @@ QStringList QmlBundle::maybeReadTrie(Trie &trie, Utils::JsonObjectValue *config, bool QmlBundle::readFrom(QString path, bool stripVersions, QStringList *errors) { - Utils::JsonMemoryPool pool; + JsonMemoryPool pool; - using namespace Utils; QFile f(path); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { if (errors) diff --git a/src/libs/qmljs/qmljsbundle.h b/src/libs/qmljs/qmljsbundle.h index 5d2058eef48..ebc0d49e251 100644 --- a/src/libs/qmljs/qmljsbundle.h +++ b/src/libs/qmljs/qmljsbundle.h @@ -12,10 +12,10 @@ QT_FORWARD_DECLARE_CLASS(QTextStream) -namespace Utils { class JsonObjectValue; } - namespace QmlJS { +class JsonObjectValue; + /* ! \class QmlJS::QmlBundle @@ -59,7 +59,7 @@ public: private: static void printEscaped(QTextStream &s, const QString &str); static void writeTrie(QTextStream &stream, const Trie &t, const QString &indent); - QStringList maybeReadTrie(Trie &trie, Utils::JsonObjectValue *config, const QString &path, + QStringList maybeReadTrie(Trie &trie, JsonObjectValue *config, const QString &path, const QString &propertyName, bool required = false, bool stripVersions = false); @@ -80,4 +80,5 @@ public: private: QHash m_bundles; }; + } // namespace QmlJS diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 357ca1f85b9..1885bc013ad 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -80,7 +80,6 @@ add_qtc_library(Utils infobar.cpp infobar.h infolabel.cpp infolabel.h itemviews.cpp itemviews.h - json.cpp json.h jsontreeitem.cpp jsontreeitem.h launcherinterface.cpp launcherinterface.h launcherpackets.cpp launcherpackets.h diff --git a/src/libs/utils/json.cpp b/src/libs/utils/json.cpp deleted file mode 100644 index d0f2518af32..00000000000 --- a/src/libs/utils/json.cpp +++ /dev/null @@ -1,722 +0,0 @@ -// 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 "json.h" - -#include "fileutils.h" -#include "qtcassert.h" - -#include -#include - -using namespace Utils; - -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 QVariant::List: { - auto newValue = new (pool) JsonArrayValue; - const QList list = variant.toList(); - for (const QVariant &element : list) - newValue->addElement(build(element, pool)); - return newValue; - } - - case QVariant::Map: { - 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 QVariant::String: - return new (pool) JsonStringValue(variant.toString()); - - case QVariant::Int: - return new (pool) JsonIntValue(variant.toInt()); - - case QVariant::Double: - return new (pool) JsonDoubleValue(variant.toDouble()); - - case QVariant::Bool: - return new (pool) JsonBooleanValue(variant.toBool()); - - case QVariant::Invalid: - 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 QStringList &searchPaths) - : m_searchPaths(searchPaths) -{ - for (const QString &path : searchPaths) { - QDir dir(path); - if (!dir.exists()) - continue; - dir.setNameFilters(QStringList(QLatin1String("*.json"))); - const QList entries = dir.entryInfoList(); - for (const QFileInfo &fi : entries) - m_schemas.insert(fi.baseName(), JsonSchemaData(fi.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 QString &fileName) const -{ - QString baseName(QFileInfo(fileName).baseName()); - - return schemaByName(baseName); -} - -JsonSchema *JsonSchemaManager::schemaByName(const QString &baseName) const -{ - QHash::iterator it = m_schemas.find(baseName); - if (it == m_schemas.end()) { - for (const QString &path : m_searchPaths) { - QFileInfo candidate(path + 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. - QFileInfo currentSchema(schemaData->m_absoluteFileName); - Q_ASSERT(currentSchema.exists()); - if (schemaData->m_lastParseAttempt.isNull() - || schemaData->m_lastParseAttempt < currentSchema.lastModified()) { - schemaData->m_schema = parseSchema(currentSchema.absoluteFilePath()); - } - } - - return schemaData->m_schema; -} - -JsonSchema *JsonSchemaManager::parseSchema(const QString &schemaFileName) const -{ - FileReader reader; - if (reader.fetch(FilePath::fromString(schemaFileName), QIODevice::Text)) { - const QString &contents = QString::fromUtf8(reader.data()); - JsonValue *json = JsonValue::create(contents, &m_pool); - if (json && json->kind() == JsonValue::Object) - return new JsonSchema(json->toObject(), this); - } - - return nullptr; -} diff --git a/src/libs/utils/json.h b/src/libs/utils/json.h deleted file mode 100644 index 4872760ed30..00000000000 --- a/src/libs/utils/json.h +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 - -#pragma once - -#include "utils_global.h" - -#include -#include -#include - -QT_BEGIN_NAMESPACE -class QVariant; -QT_END_NAMESPACE - -namespace Utils { - -class JsonStringValue; -class JsonDoubleValue; -class JsonIntValue; -class JsonObjectValue; -class JsonArrayValue; -class JsonBooleanValue; -class JsonNullValue; - -class QTCREATOR_UTILS_EXPORT JsonMemoryPool -{ -public: - ~JsonMemoryPool(); - - inline void *allocate(size_t size) - { - auto obj = new char[size]; - _objs.append(obj); - return obj; - } - -private: - QList _objs; -}; - -/*! - * \brief The JsonValue class - */ -class QTCREATOR_UTILS_EXPORT JsonValue -{ -public: - enum Kind { - String, - Double, - Int, - Object, - Array, - Boolean, - Null, - Unknown - }; - - virtual ~JsonValue(); - - Kind kind() const { return m_kind; } - static QString kindToString(Kind kind); - - virtual JsonStringValue *toString() { return nullptr; } - virtual JsonDoubleValue *toDouble() { return nullptr; } - virtual JsonIntValue *toInt() { return nullptr; } - virtual JsonObjectValue *toObject() { return nullptr; } - virtual JsonArrayValue *toArray() { return nullptr; } - virtual JsonBooleanValue *toBoolean() { return nullptr; } - virtual JsonNullValue *toNull() { return nullptr; } - - static JsonValue *create(const QString &s, JsonMemoryPool *pool); - void *operator new(size_t size, JsonMemoryPool *pool); - void operator delete(void *); - void operator delete(void *, JsonMemoryPool *); - -protected: - JsonValue(Kind kind); - -private: - static JsonValue *build(const QVariant &varixant, JsonMemoryPool *pool); - - Kind m_kind; -}; - - -/*! - * \brief The JsonStringValue class - */ -class QTCREATOR_UTILS_EXPORT JsonStringValue : public JsonValue -{ -public: - JsonStringValue(const QString &value) - : JsonValue(String) - , m_value(value) - {} - - JsonStringValue *toString() override { return this; } - - const QString &value() const { return m_value; } - -private: - QString m_value; -}; - - -/*! - * \brief The JsonDoubleValue class - */ -class QTCREATOR_UTILS_EXPORT JsonDoubleValue : public JsonValue -{ -public: - JsonDoubleValue(double value) - : JsonValue(Double) - , m_value(value) - {} - - JsonDoubleValue *toDouble() override { return this; } - - double value() const { return m_value; } - -private: - double m_value; -}; - -/*! - * \brief The JsonIntValue class - */ -class QTCREATOR_UTILS_EXPORT JsonIntValue : public JsonValue -{ -public: - JsonIntValue(int value) - : JsonValue(Int) - , m_value(value) - {} - - JsonIntValue *toInt() override { return this; } - - int value() const { return m_value; } - -private: - int m_value; -}; - - -/*! - * \brief The JsonObjectValue class - */ -class QTCREATOR_UTILS_EXPORT JsonObjectValue : public JsonValue -{ -public: - JsonObjectValue() - : JsonValue(Object) - {} - - JsonObjectValue *toObject() override { return this; } - - void addMember(const QString &name, JsonValue *value) { m_members.insert(name, value); } - bool hasMember(const QString &name) const { return m_members.contains(name); } - JsonValue *member(const QString &name) const { return m_members.value(name); } - QHash members() const { return m_members; } - bool isEmpty() const { return m_members.isEmpty(); } - -protected: - JsonObjectValue(Kind kind) - : JsonValue(kind) - {} - -private: - QHash m_members; -}; - - -/*! - * \brief The JsonArrayValue class - */ -class QTCREATOR_UTILS_EXPORT JsonArrayValue : public JsonValue -{ -public: - JsonArrayValue() - : JsonValue(Array) - {} - - JsonArrayValue *toArray() override { return this; } - - void addElement(JsonValue *value) { m_elements.append(value); } - QList elements() const { return m_elements; } - int size() const { return m_elements.size(); } - -private: - QList m_elements; -}; - - -/*! - * \brief The JsonBooleanValue class - */ -class QTCREATOR_UTILS_EXPORT JsonBooleanValue : public JsonValue -{ -public: - JsonBooleanValue(bool value) - : JsonValue(Boolean) - , m_value(value) - {} - - JsonBooleanValue *toBoolean() override { return this; } - - bool value() const { return m_value; } - -private: - bool m_value; -}; - -class QTCREATOR_UTILS_EXPORT JsonNullValue : public JsonValue -{ -public: - JsonNullValue() - : JsonValue(Null) - {} - - JsonNullValue *toNull() override { return this; } -}; - -class JsonSchemaManager; - -/*! - * \brief The JsonSchema class - * - * [NOTE: This is an incomplete implementation and a work in progress.] - * - * This class provides an interface for traversing and evaluating a JSON schema, as described - * in the draft http://tools.ietf.org/html/draft-zyp-json-schema-03. - * - * JSON schemas are recursive in concept. This means that a particular attribute from a schema - * might be also another schema. Therefore, the basic working principle of this API is that - * from within some schema, one can investigate its attributes and if necessary "enter" a - * corresponding nested schema. Afterwards, it's expected that one would "leave" such nested - * schema. - * - * All functions assume that the current "context" is a valid schema. Once an instance of this - * class is created the root schema is put on top of the stack. - * - */ -class QTCREATOR_UTILS_EXPORT JsonSchema -{ -public: - bool isTypeConstrained() const; - bool acceptsType(const QString &type) const; - QStringList validTypes() const; - - // Applicable on schemas of any type. - bool required() const; - - bool hasTypeSchema() const; - void enterNestedTypeSchema(); - - bool hasUnionSchema() const; - int unionSchemaSize() const; - bool maybeEnterNestedUnionSchema(int index); - - void leaveNestedSchema(); - - // Applicable on schemas of type number/integer. - bool hasMinimum() const; - bool hasMaximum() const; - bool hasExclusiveMinimum(); - bool hasExclusiveMaximum(); - double minimum() const; - double maximum() const; - - // Applicable on schemas of type string. - QString pattern() const; - int minimumLength() const; - int maximumLength() const; - - // Applicable on schemas of type object. - QStringList properties() const; - bool hasPropertySchema(const QString &property) const; - void enterNestedPropertySchema(const QString &property); - - // Applicable on schemas of type array. - bool hasAdditionalItems() const; - - bool hasItemSchema() const; - void enterNestedItemSchema(); - - bool hasItemArraySchema() const; - int itemArraySchemaSize() const; - bool maybeEnterNestedArraySchema(int index); - -private: - friend class JsonSchemaManager; - JsonSchema(JsonObjectValue *rootObject, const JsonSchemaManager *manager); - Q_DISABLE_COPY(JsonSchema) - - enum EvaluationMode { - Normal, - Array, - Union - }; - - void enter(JsonObjectValue *ov, EvaluationMode eval = Normal, int index = -1); - bool maybeEnter(JsonValue *v, EvaluationMode eval, int index); - void evaluate(EvaluationMode eval, int index); - void leave(); - - JsonObjectValue *resolveReference(JsonObjectValue *ov) const; - JsonObjectValue *resolveBase(JsonObjectValue *ov) const; - - JsonObjectValue *currentValue() const; - int currentIndex() const; - - JsonObjectValue *rootValue() const; - - static JsonStringValue *getStringValue(const QString &name, JsonObjectValue *value); - static JsonObjectValue *getObjectValue(const QString &name, JsonObjectValue *value); - static JsonBooleanValue *getBooleanValue(const QString &name, JsonObjectValue *value); - static JsonArrayValue *getArrayValue(const QString &name, JsonObjectValue *value); - static JsonDoubleValue *getDoubleValue(const QString &name, JsonObjectValue *value); - - static QStringList validTypes(JsonObjectValue *v); - static bool typeMatches(const QString &expected, const QString &actual); - static bool isCheckableType(const QString &s); - - QStringList properties(JsonObjectValue *v) const; - JsonObjectValue *propertySchema(const QString &property, JsonObjectValue *v) const; - // TODO: Similar functions for other attributes which require looking into base schemas. - - static bool maybeSchemaName(const QString &s); - - static QString kType(); - static QString kProperties(); - static QString kPatternProperties(); - static QString kAdditionalProperties(); - static QString kItems(); - static QString kAdditionalItems(); - static QString kRequired(); - static QString kDependencies(); - static QString kMinimum(); - static QString kMaximum(); - static QString kExclusiveMinimum(); - static QString kExclusiveMaximum(); - static QString kMinItems(); - static QString kMaxItems(); - static QString kUniqueItems(); - static QString kPattern(); - static QString kMinLength(); - static QString kMaxLength(); - static QString kTitle(); - static QString kDescription(); - static QString kExtends(); - static QString kRef(); - - struct Context - { - JsonObjectValue *m_value; - EvaluationMode m_eval; - int m_index; - }; - - QList m_schemas; - const JsonSchemaManager *m_manager; -}; - - -/*! - * \brief The JsonSchemaManager class - */ -class QTCREATOR_UTILS_EXPORT JsonSchemaManager -{ -public: - JsonSchemaManager(const QStringList &searchPaths); - ~JsonSchemaManager(); - - JsonSchema *schemaForFile(const QString &fileName) const; - JsonSchema *schemaByName(const QString &baseName) const; - -private: - struct JsonSchemaData - { - JsonSchemaData(const QString &absoluteFileName, JsonSchema *schema = nullptr) - : m_absoluteFileName(absoluteFileName) - , m_schema(schema) - {} - QString m_absoluteFileName; - JsonSchema *m_schema; - QDateTime m_lastParseAttempt; - }; - - JsonSchema *parseSchema(const QString &schemaFileName) const; - - QStringList m_searchPaths; - mutable QHash m_schemas; - mutable JsonMemoryPool m_pool; -}; - -} // namespace Utils diff --git a/src/libs/utils/utils.qbs b/src/libs/utils/utils.qbs index da82da0608c..fc59852fab1 100644 --- a/src/libs/utils/utils.qbs +++ b/src/libs/utils/utils.qbs @@ -171,8 +171,6 @@ Project { "infolabel.h", "itemviews.cpp", "itemviews.h", - "json.cpp", - "json.h", "jsontreeitem.cpp", "jsontreeitem.h", "launcherinterface.cpp", diff --git a/src/plugins/qmljseditor/qmljseditorplugin.cpp b/src/plugins/qmljseditor/qmljseditorplugin.cpp index b0736b29d15..e4b2538dcb9 100644 --- a/src/plugins/qmljseditor/qmljseditorplugin.cpp +++ b/src/plugins/qmljseditor/qmljseditorplugin.cpp @@ -7,15 +7,16 @@ #include "qmljseditordocument.h" #include "qmljseditorplugin.h" #include "qmljseditortr.h" -#include "qmljshighlighter.h" #include "qmljsoutline.h" #include "qmljsquickfixassist.h" #include "qmltaskmanager.h" #include "quicktoolbar.h" +#include #include #include #include + #include #include #include @@ -26,18 +27,20 @@ #include #include #include + #include #include #include #include + #include #include #include #include #include #include + #include -#include #include #include @@ -77,7 +80,7 @@ public: QPointer m_currentDocument; - Utils::JsonSchemaManager m_jsonManager{ + QmlJS::JsonSchemaManager m_jsonManager{ {ICore::userResourcePath("json/").toString(), ICore::resourcePath("json/").toString()}}; QmlJSEditorFactory m_qmlJSEditorFactory; @@ -215,7 +218,7 @@ void QmlJSEditorPlugin::extensionsInitialized() QmllsSettingsManager::instance()->setupAutoupdate(); } -Utils::JsonSchemaManager *QmlJSEditorPlugin::jsonManager() +QmlJS::JsonSchemaManager *QmlJSEditorPlugin::jsonManager() { return &m_instance->d->m_jsonManager; } diff --git a/src/plugins/qmljseditor/qmljseditorplugin.h b/src/plugins/qmljseditor/qmljseditorplugin.h index aa653ac6cb0..69d1c477086 100644 --- a/src/plugins/qmljseditor/qmljseditorplugin.h +++ b/src/plugins/qmljseditor/qmljseditorplugin.h @@ -5,7 +5,7 @@ #include -namespace Utils { class JsonSchemaManager; } +namespace QmlJS { class JsonSchemaManager; } namespace QmlJSEditor { class QuickToolBar; @@ -23,7 +23,7 @@ public: ~QmlJSEditorPlugin() final; static QmlJSQuickFixAssistProvider *quickFixAssistProvider(); - static Utils::JsonSchemaManager *jsonManager(); + static QmlJS::JsonSchemaManager *jsonManager(); static QuickToolBar *quickToolBar(); private: diff --git a/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp b/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp index 764f125e543..44c1a72f952 100644 --- a/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp +++ b/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp @@ -14,8 +14,6 @@ #include -#include - namespace QmlJSEditor { namespace Internal { @@ -97,7 +95,7 @@ QmlJSTools::SemanticInfo SemanticInfoUpdater::makeNewSemanticInfo(const QmlJS::D semanticInfo.setRootScopeChain(QSharedPointer(scopeChain)); if (doc->language() == Dialect::Json) { - Utils::JsonSchema *schema = QmlJSEditorPlugin::jsonManager()->schemaForFile( + JsonSchema *schema = QmlJSEditorPlugin::jsonManager()->schemaForFile( doc->fileName().toString()); if (schema) { JsonCheck jsonChecker(doc);