diff options
author | Leandro Melo <leandro.melo@nokia.com> | 2012-02-07 15:30:33 +0100 |
---|---|---|
committer | Leandro Melo <leandro.melo@nokia.com> | 2012-02-08 11:25:07 +0100 |
commit | 26b783ff025a5025879f3e8bee70c0c16d157c91 (patch) | |
tree | 2b225e34f48892b4902258fe44cd143e364b50dd | |
parent | 454e2c3928a84e20d30af77d40f4b8c6f17a5aed (diff) | |
download | qt-creator-26b783ff025a5025879f3e8bee70c0c16d157c91.tar.gz |
Introduce JSON validation
Support basic validation for JSON files according to the
draft http://tools.ietf.org/html/draft-zyp-json-schema-03.
This is not a complete implementation yet, but it should
already be useful, since "type" verification along with
many of the attributes is done.
Change-Id: I364bc98dd92937c5e2ea9cba7e15ed8e03eb9beb
Reviewed-by: Erik Verbruggen <erik.verbruggen@nokia.com>
-rw-r--r-- | src/libs/qmljs/jsoncheck.cpp | 401 | ||||
-rw-r--r-- | src/libs/qmljs/jsoncheck.h | 98 | ||||
-rw-r--r-- | src/libs/qmljs/qmljs-lib.pri | 6 | ||||
-rw-r--r-- | src/libs/qmljs/qmljsstaticanalysismessage.cpp | 33 | ||||
-rw-r--r-- | src/libs/qmljs/qmljsstaticanalysismessage.h | 17 | ||||
-rw-r--r-- | src/libs/utils/json.cpp | 740 | ||||
-rw-r--r-- | src/libs/utils/json.h | 412 | ||||
-rw-r--r-- | src/libs/utils/utils-lib.pri | 8 | ||||
-rw-r--r-- | src/plugins/qmljseditor/qmljseditorplugin.cpp | 11 | ||||
-rw-r--r-- | src/plugins/qmljseditor/qmljseditorplugin.h | 8 | ||||
-rw-r--r-- | src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp | 15 |
11 files changed, 1738 insertions, 11 deletions
diff --git a/src/libs/qmljs/jsoncheck.cpp b/src/libs/qmljs/jsoncheck.cpp new file mode 100644 index 0000000000..1bbed68e38 --- /dev/null +++ b/src/libs/qmljs/jsoncheck.cpp @@ -0,0 +1,401 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (info@qt.nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#include "jsoncheck.h" + +#include <parser/qmljsast_p.h> +#include <utils/qtcassert.h> + +#include <QtCore/QDebug> +#include <QtCore/QLatin1String> +#include <QtCore/QRegExp> +#include <QtCore/QMap> + +#include <cmath> + +using namespace QmlJS; +using namespace QmlJS::AST; +using namespace QmlJS::StaticAnalysis; +using namespace Utils; + +JsonCheck::JsonCheck(Document::Ptr doc) + : m_doc(doc) + , m_schema(0) +{ + QTC_CHECK(m_doc->ast()); +} + +JsonCheck::~JsonCheck() +{} + +QList<Message> JsonCheck::operator ()(JsonSchema *schema) +{ + QTC_ASSERT(schema, return QList<Message>()); + + 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(ObjectLiteral *ast) +{ + if (!proceedCheck(JsonValue::Object, ast->lbraceToken)) + return false; + + analysis()->boostRanking(); + + const QStringList &properties = m_schema->properties(); + if (properties.isEmpty()) + return false; + + QSet<QString> propertiesFound; + for (PropertyNameAndValueList *it = ast->properties; it; it = it->next) { + StringLiteralPropertyName *literalName = cast<StringLiteralPropertyName *>(it->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(it->value); + m_schema->leaveNestedSchema(); + } else { + analysis()->m_messages.append(Message(ErrInvalidPropertyName, + literalName->firstSourceLocation(), + propertyName, QString(), + false)); + } + } else { + analysis()->m_messages.append(Message(ErrStringValueExpected, + it->name->firstSourceLocation(), + QString(), QString(), + false)); + } + } + + QStringList missing; + foreach (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(ArrayLiteral *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 (ElementList *element = ast->elements; element; element = element->next) + processSchema(element->expression); + 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 (ElementList *element = ast->elements; element; element = element->next, ++current) { + if (current < arraySize) { + if (m_schema->maybeEnterNestedArraySchema(current)) { + processSchema(element->expression); + m_schema->leaveNestedSchema(); + } else { + Node::accept(element->expression, 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 QString &literal = ast->value.toString(); + + const QString &pattern = m_schema->pattern(); + if (!pattern.isEmpty()) { + QRegExp regExp(pattern); + if (regExp.indexIn(literal) == -1) { + 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; +} + +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<StaticAnalysis::Message> 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(); +} diff --git a/src/libs/qmljs/jsoncheck.h b/src/libs/qmljs/jsoncheck.h new file mode 100644 index 0000000000..206c8fe231 --- /dev/null +++ b/src/libs/qmljs/jsoncheck.h @@ -0,0 +1,98 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (info@qt.nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#ifndef JSONCHECK_H +#define JSONCHECK_H + +#include "qmljs_global.h" + +#include <qmljs/qmljsdocument.h> +#include <qmljs/parser/qmljsast_p.h> +#include <qmljs/parser/qmljsastvisitor_p.h> +#include <qmljs/qmljsstaticanalysismessage.h> + +#include <utils/json.h> + +#include <QtCore/QString> +#include <QtCore/QSet> +#include <QtCore/QStack> +#include <QtCore/QList> + + +namespace QmlJS { + +class QMLJS_EXPORT JsonCheck : public AST::Visitor +{ +public: + JsonCheck(Document::Ptr doc); + virtual ~JsonCheck(); + + QList<StaticAnalysis::Message> operator()(Utils::JsonSchema *schema); + +private: + virtual bool preVisit(AST::Node *); + virtual void postVisit(AST::Node *); + + virtual bool visit(AST::ObjectLiteral *ast); + virtual bool visit(AST::ArrayLiteral *ast); + virtual bool visit(AST::NullExpression *ast); + virtual bool visit(AST::TrueLiteral *ast); + virtual bool visit(AST::FalseLiteral *ast); + virtual bool visit(AST::NumericLiteral *ast); + virtual bool visit(AST::StringLiteral *ast); + + struct AnalysisData + { + AnalysisData() : m_ranking(0), m_hasMatch(false), m_evaluatinType(false) {} + + void boostRanking(int unit = 1) { m_ranking += unit; } + + int m_ranking; + bool m_hasMatch; + bool m_evaluatinType; + QList<StaticAnalysis::Message> m_messages; + }; + + void processSchema(AST::Node *ast); + bool proceedCheck(Utils::JsonValue::Kind kind, const AST::SourceLocation &location); + + AnalysisData *analysis(); + + Document::Ptr m_doc; + AST::SourceLocation m_firstLoc; + Utils::JsonSchema *m_schema; + QStack<AnalysisData> m_analysis; +}; + +} // QmlJs + +#endif // JSONCHECK_H diff --git a/src/libs/qmljs/qmljs-lib.pri b/src/libs/qmljs/qmljs-lib.pri index 1fa8e57b1d..bbc1d06bda 100644 --- a/src/libs/qmljs/qmljs-lib.pri +++ b/src/libs/qmljs/qmljs-lib.pri @@ -32,7 +32,8 @@ HEADERS += \ $$PWD/qmljscontext.h \ $$PWD/qmljsscopechain.h \ $$PWD/qmljsutils.h \ - $$PWD/qmljsstaticanalysismessage.h + $$PWD/qmljsstaticanalysismessage.h \ + $$PWD/jsoncheck.h SOURCES += \ $$PWD/qmljsbind.cpp \ @@ -56,7 +57,8 @@ SOURCES += \ $$PWD/qmljscontext.cpp \ $$PWD/qmljsscopechain.cpp \ $$PWD/qmljsutils.cpp \ - $$PWD/qmljsstaticanalysismessage.cpp + $$PWD/qmljsstaticanalysismessage.cpp \ + $$PWD/jsoncheck.cpp RESOURCES += \ $$PWD/qmljs.qrc diff --git a/src/libs/qmljs/qmljsstaticanalysismessage.cpp b/src/libs/qmljs/qmljsstaticanalysismessage.cpp index c6b2b4524b..7c3abc30ed 100644 --- a/src/libs/qmljs/qmljsstaticanalysismessage.cpp +++ b/src/libs/qmljs/qmljsstaticanalysismessage.cpp @@ -193,6 +193,30 @@ StaticAnalysisMessages::StaticAnalysisMessages() tr("unintentional empty block, use ({}) for empty object literal")); newMsg(HintPreferNonVarPropertyType, Hint, tr("use %1 instead of 'var' or 'variant' to improve performance"), 1); + newMsg(ErrMissingRequiredProperty, Error, + tr("missing property '%1'"), 1); + newMsg(ErrObjectValueExpected, Error, + tr("object value expected")); + newMsg(ErrArrayValueExpected, Error, + tr("array value expected")); + newMsg(ErrDifferentValueExpected, Error, + tr("%1 value expected"), 1); + newMsg(ErrSmallerNumberValueExpected, Error, + tr("maximum number value is %1"), 1); + newMsg(ErrLargerNumberValueExpected, Error, + tr("minimum number value is %1"), 1); + newMsg(ErrMaximumNumberValueIsExclusive, Error, + tr("maximum number value is exclusive")); + newMsg(ErrMinimumNumberValueIsExclusive, Error, + tr("minimum number value is exclusive")); + newMsg(ErrInvalidStringValuePattern, Error, + tr("string value does not match required pattern")); + newMsg(ErrLongerStringValueExpected, Error, + tr("minimum string value length is %1"), 1); + newMsg(ErrShorterStringValueExpected, Error, + tr("maximum string value length is %1"), 1); + newMsg(ErrInvalidArrayValueLength, Error, + tr("%1 elements expected in array value"), 1); } } // anonymous namespace @@ -208,7 +232,11 @@ Message::Message() : type(UnknownType), severity(Hint) {} -Message::Message(Type type, AST::SourceLocation location, const QString &arg1, const QString &arg2) +Message::Message(Type type, + AST::SourceLocation location, + const QString &arg1, + const QString &arg2, + bool appendTypeId) : location(location), type(type) { QTC_ASSERT(messages()->messages.contains(type), return); @@ -227,7 +255,8 @@ Message::Message(Type type, AST::SourceLocation location, const QString &arg1, c qWarning() << "StaticAnalysis message" << type << "expects exactly two arguments"; message = message.arg(arg1, arg2); } - message.append(QString(" (M%1)").arg(QString::number(prototype.type))); + if (appendTypeId) + message.append(QString(" (M%1)").arg(QString::number(prototype.type))); } bool Message::isValid() const diff --git a/src/libs/qmljs/qmljsstaticanalysismessage.h b/src/libs/qmljs/qmljsstaticanalysismessage.h index 78a0c190cc..bb0f172698 100644 --- a/src/libs/qmljs/qmljsstaticanalysismessage.h +++ b/src/libs/qmljs/qmljsstaticanalysismessage.h @@ -114,7 +114,19 @@ enum Type WarnNumberConstructor = 308, HintBinaryOperatorSpacing = 309, WarnUnintentinalEmptyBlock = 310, - HintPreferNonVarPropertyType = 311 + HintPreferNonVarPropertyType = 311, + ErrMissingRequiredProperty = 312, + ErrObjectValueExpected = 313, + ErrArrayValueExpected = 314, + ErrDifferentValueExpected = 315, + ErrSmallerNumberValueExpected = 316, + ErrLargerNumberValueExpected = 317, + ErrMaximumNumberValueIsExclusive = 318, + ErrMinimumNumberValueIsExclusive = 319, + ErrInvalidStringValuePattern = 320, + ErrLongerStringValueExpected = 321, + ErrShorterStringValueExpected = 322, + ErrInvalidArrayValueLength = 323 }; class QMLJS_EXPORT Message @@ -123,7 +135,8 @@ public: Message(); Message(Type type, AST::SourceLocation location, const QString &arg1 = QString(), - const QString &arg2 = QString()); + const QString &arg2 = QString(), + bool appendTypeId = true); static QList<Type> allMessageTypes(); diff --git a/src/libs/utils/json.cpp b/src/libs/utils/json.cpp new file mode 100644 index 0000000000..5719f8f695 --- /dev/null +++ b/src/libs/utils/json.cpp @@ -0,0 +1,740 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (info@qt.nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#include "json.h" + +#include <utils/qtcassert.h> +#include <utils/fileutils.h> + +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QFile> +#include <QtCore/QStringBuilder> +#include <QtCore/QDebug> + +using namespace Utils; + + +JsonValue::JsonValue(Kind kind) + : m_kind(kind) +{} + +JsonValue::~JsonValue() +{} + +JsonValue *JsonValue::create(const QString &s) +{ + QScriptEngine engine; + QScriptValue jsonParser = engine.evaluate(QLatin1String("JSON.parse")); + QScriptValue value = jsonParser.call(QScriptValue(), QScriptValueList() << s); + if (engine.hasUncaughtException() || !value.isValid()) + return 0; + + return build(value.toVariant()); +} + +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("unkown"); +} + +JsonValue *JsonValue::build(const QVariant &variant) +{ + switch (variant.type()) { + + case QVariant::List: { + JsonArrayValue *newValue = new JsonArrayValue; + foreach (const QVariant element, variant.toList()) + newValue->addElement(build(element)); + return newValue; + } + + case QVariant::Map: { + JsonObjectValue *newValue = new JsonObjectValue; + const QVariantMap variantMap = variant.toMap(); + for (QVariantMap::const_iterator it = variantMap.begin(); it != variantMap.end(); ++it) + newValue->addMember(it.key(), build(it.value())); + return newValue; + } + + case QVariant::String: + return new JsonStringValue(variant.toString()); + + case QVariant::Int: + return new JsonIntValue(variant.toInt()); + + case QVariant::Double: + return new JsonDoubleValue(variant.toDouble()); + + case QVariant::Bool: + return new JsonBooleanValue(variant.toBool()); + + case QVariant::Invalid: + return new JsonNullValue; + + default: + break; + } + + return 0; +} + + +/////////////////////////////////////////////////////////////////////////////// + +const QString JsonSchema::kType(QLatin1String("type")); +const QString JsonSchema::kProperties(QLatin1String("properties")); +const QString JsonSchema::kPatternProperties(QLatin1String("patternProperties")); +const QString JsonSchema::kAdditionalProperties(QLatin1String("additionalProperties")); +const QString JsonSchema::kItems(QLatin1String("items")); +const QString JsonSchema::kAdditionalItems(QLatin1String("additionalItems")); +const QString JsonSchema::kRequired(QLatin1String("required")); +const QString JsonSchema::kDependencies(QLatin1String("dependencies")); +const QString JsonSchema::kMinimum(QLatin1String("minimum")); +const QString JsonSchema::kMaximum(QLatin1String("maximum")); +const QString JsonSchema::kExclusiveMinimum(QLatin1String("exclusiveMinimum")); +const QString JsonSchema::kExclusiveMaximum(QLatin1String("exclusiveMaximum")); +const QString JsonSchema::kMinItems(QLatin1String("minItems")); +const QString JsonSchema::kMaxItems(QLatin1String("maxItems")); +const QString JsonSchema::kUniqueItems(QLatin1String("uniqueItems")); +const QString JsonSchema::kPattern(QLatin1String("pattern")); +const QString JsonSchema::kMinLength(QLatin1String("minLength")); +const QString JsonSchema::kMaxLength(QLatin1String("maxLength")); +const QString JsonSchema::kTitle(QLatin1String("title")); +const QString JsonSchema::kDescription(QLatin1String("description")); +const QString JsonSchema::kExtends(QLatin1String("extends")); +const QString JsonSchema::kRef(QLatin1String("$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)) { + foreach (JsonValue *v, av->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) +{ + if (s == QLatin1String("string") + || s == QLatin1String("number") + || s == QLatin1String("integer") + || s == QLatin1String("boolean") + || s == QLatin1String("object") + || s == QLatin1String("array") + || s == QLatin1String("null")) { + return true; + } + + return false; +} + +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 +{ + QStringList all; + + if (JsonObjectValue *ov = getObjectValue(kProperties, v)) { + foreach (const QString &property, ov->members().keys()) { + if (hasPropertySchema(property)) + all.append(property); + } + } + + if (JsonObjectValue *base = resolveBase(v)) + all.append(properties(base)); + + return all; +} + +QStringList JsonSchema::properties() const +{ + QTC_ASSERT(acceptsType(JsonValue::kindToString(JsonValue::Object)), return QStringList()); + + 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 0; +} + +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 *items* specification in the form of another schema + * or in the form of an array of schemas [Sec. 5.5]. This methods checks whether this is case + * in which the items are a schema. + * + * \return 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 *items* specification in the form of another schema + * or in the form of an array of schemas [Sec. 5.5]. This methods checks whether this is case + * in which the items are an array of schemas. + * + * \return 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 "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 method also marks the context as being inside an array evaluation. + * + * \param index + * \return whether it was necessary to "enter" a schema for the supplied array index + */ +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 method checks whether + * the current schema is one of such. + * + * \return 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 index, which is then assumed to be + * a schema. + * + * The method also marks the context as being inside an union evaluation. + * + * \param index + * \return 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 0); + + return m_schemas.first().m_value; +} + +JsonObjectValue *JsonSchema::currentValue() const +{ + QTC_ASSERT(!m_schemas.isEmpty(), return 0); + + 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 0; +} + +JsonStringValue *JsonSchema::getStringValue(const QString &name, JsonObjectValue *value) +{ + JsonValue *v = value->member(name); + if (!v) + return 0; + + return v->toString(); +} + +JsonObjectValue *JsonSchema::getObjectValue(const QString &name, JsonObjectValue *value) +{ + JsonValue *v = value->member(name); + if (!v) + return 0; + + return v->toObject(); +} + +JsonBooleanValue *JsonSchema::getBooleanValue(const QString &name, JsonObjectValue *value) +{ + JsonValue *v = value->member(name); + if (!v) + return 0; + + return v->toBoolean(); +} + +JsonArrayValue *JsonSchema::getArrayValue(const QString &name, JsonObjectValue *value) +{ + JsonValue *v = value->member(name); + if (!v) + return 0; + + return v->toArray(); +} + +JsonDoubleValue *JsonSchema::getDoubleValue(const QString &name, JsonObjectValue *value) +{ + JsonValue *v = value->member(name); + if (!v) + return 0; + + return v->toDouble(); +} + + +/////////////////////////////////////////////////////////////////////////////// + +JsonSchemaManager::JsonSchemaManager(const QStringList &searchPaths) + : m_searchPaths(searchPaths) +{ + foreach (const QString &path, m_searchPaths) { + QDir dir(path); + if (!dir.exists() && !dir.mkpath(path)) + continue; + dir.setNameFilters(QStringList(QLatin1String("*.json"))); + foreach (const QFileInfo &fi, dir.entryInfoList()) + m_schemas.insert(fi.baseName(), JsonSchemaData(fi.absoluteFilePath())); + } +} + +JsonSchemaManager::~JsonSchemaManager() +{ + foreach (const JsonSchemaData &schemaData, m_schemas) + delete schemaData.m_schema; +} + +/*! + * \brief JsonManager::schemaForFile + * + * Try to find a JSON schema to which the supplied file can be validated 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. + * + * \param fileName - JSON file to be validated + * \return 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<QString, JsonSchemaData>::iterator it = m_schemas.find(baseName); + if (it == m_schemas.end()) { + foreach (const QString &path, m_searchPaths) { + QFileInfo candidate(path % baseName % QLatin1String(".json")); + if (candidate.exists()) { + m_schemas.insert(baseName, candidate.absoluteFilePath()); + break; + } + } + } + + it = m_schemas.find(baseName); + if (it == m_schemas.end()) + return 0; + + 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(schemaFileName, QIODevice::Text)) { + const QString &contents = QString::fromUtf8(reader.data()); + JsonValue *json = JsonValue::create(contents); + if (json && json->kind() == JsonValue::Object) { + return new JsonSchema(json->toObject(), this); + } + } + + return 0; +} diff --git a/src/libs/utils/json.h b/src/libs/utils/json.h new file mode 100644 index 0000000000..ed8f047c59 --- /dev/null +++ b/src/libs/utils/json.h @@ -0,0 +1,412 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (info@qt.nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#ifndef JSON_H +#define JSON_H + +#include "utils_global.h" + +#include <QtCore/QHash> +#include <QtCore/QVector> +#include <QtCore/QScopedPointer> +#include <QtCore/QVariant> +#include <QtCore/QStringList> +#include <QtCore/QLatin1String> +#include <QtCore/QDateTime> +#include <QtScript/QScriptEngine> +#include <QtScript/QScriptValue> +#include <QtScript/QScriptValueIterator> + +namespace Utils { + +class JsonStringValue; +class JsonDoubleValue; +class JsonIntValue; +class JsonObjectValue; +class JsonArrayValue; +class JsonBooleanValue; +class JsonNullValue; + +/*! + * \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 0; } + virtual JsonDoubleValue *toDouble() { return 0; } + virtual JsonIntValue *toInt() { return 0; } + virtual JsonObjectValue *toObject() { return 0; } + virtual JsonArrayValue *toArray() { return 0; } + virtual JsonBooleanValue *toBoolean() { return 0; } + virtual JsonNullValue *toNull() { return 0; } + + static JsonValue *create(const QString &s); + +protected: + JsonValue(Kind kind); + +private: + static JsonValue *build(const QVariant &varixant); + + Kind m_kind; +}; + + +/*! + * \brief The JsonStringValue class + */ +class QTCREATOR_UTILS_EXPORT JsonStringValue : public JsonValue +{ +public: + JsonStringValue(const QString &value) + : JsonValue(String) + , m_value(value) + {} + + virtual JsonStringValue *toString() { 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) + {} + + virtual JsonDoubleValue *toDouble() { 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) + {} + + virtual JsonIntValue *toInt() { 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) + {} + + virtual JsonObjectValue *toObject() { 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<QString, JsonValue *> members() const { return m_members; } + bool isEmpty() const { return m_members.isEmpty(); } + +protected: + JsonObjectValue(Kind kind) + : JsonValue(kind) + {} + +private: + QHash<QString, JsonValue *> m_members; +}; + + +/*! + * \brief The JsonArrayValue class + */ +class QTCREATOR_UTILS_EXPORT JsonArrayValue : public JsonValue +{ +public: + JsonArrayValue() + : JsonValue(Array) + {} + + virtual JsonArrayValue *toArray() { return this; } + + void addElement(JsonValue *value) { m_elements.append(value); } + QList<JsonValue *> elements() const { return m_elements; } + int size() const { return m_elements.size(); } + +private: + QList<JsonValue *> m_elements; +}; + + +/*! + * \brief The JsonBooleanValue class + */ +class QTCREATOR_UTILS_EXPORT JsonBooleanValue : public JsonValue +{ +public: + JsonBooleanValue(bool value) + : JsonValue(Boolean) + , m_value(value) + {} + + virtual JsonBooleanValue *toBoolean() { return this; } + + bool value() const { return m_value; } + +private: + bool m_value; +}; + +class QTCREATOR_UTILS_EXPORT JsonNullValue : public JsonValue +{ +public: + JsonNullValue() + : JsonValue(Null) + {} + + virtual JsonNullValue *toNull() { 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 methods 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 methods for other attributes which require looking into base schemas. + + static bool maybeSchemaName(const QString &s); + + static const QString kType; + static const QString kProperties; + static const QString kPatternProperties; + static const QString kAdditionalProperties; + static const QString kItems; + static const QString kAdditionalItems; + static const QString kRequired; + static const QString kDependencies; + static const QString kMinimum; + static const QString kMaximum; + static const QString kExclusiveMinimum; + static const QString kExclusiveMaximum; + static const QString kMinItems; + static const QString kMaxItems; + static const QString kUniqueItems; + static const QString kPattern; + static const QString kMinLength; + static const QString kMaxLength; + static const QString kTitle; + static const QString kDescription; + static const QString kExtends; + static const QString kRef; + + struct Context + { + JsonObjectValue *m_value; + EvaluationMode m_eval; + int m_index; + }; + + QVector<Context> m_schemas; + const JsonSchemaManager *m_manager; +}; + + +/*! + * \brief The JsonSchemaManager class + */ +class QTCREATOR_UTILS_EXPORT JsonSchemaManager +{ +public: + JsonSchemaManager(const QStringList &searchPaths); + ~JsonSchemaManager(); + + Utils::JsonSchema *schemaForFile(const QString &fileName) const; + Utils::JsonSchema *schemaByName(const QString &baseName) const; + +private: + struct JsonSchemaData + { + JsonSchemaData(const QString &absoluteFileName, Utils::JsonSchema *schema = 0) + : m_absoluteFileName(absoluteFileName) + , m_schema(schema) + {} + QString m_absoluteFileName; + Utils::JsonSchema *m_schema; + QDateTime m_lastParseAttempt; + }; + + Utils::JsonSchema *parseSchema(const QString &schemaFileName) const; + + QStringList m_searchPaths; + mutable QHash<QString, JsonSchemaData> m_schemas; +}; + +} // namespace Utils + +#endif // JSON_H diff --git a/src/libs/utils/utils-lib.pri b/src/libs/utils/utils-lib.pri index cf8a0265ad..638f99d001 100644 --- a/src/libs/utils/utils-lib.pri +++ b/src/libs/utils/utils-lib.pri @@ -5,7 +5,7 @@ dll { } INCLUDEPATH += $$PWD -QT += network +QT += network script win32-msvc* { # disable warnings caused by botan headers @@ -93,7 +93,8 @@ SOURCES += $$PWD/environment.cpp \ $$PWD/flowlayout.cpp \ $$PWD/networkaccessmanager.cpp \ $$PWD/persistentsettings.cpp \ - $$PWD/completingtextedit.cpp + $$PWD/completingtextedit.cpp \ + $$PWD/json.cpp win32 { SOURCES += \ @@ -201,7 +202,8 @@ HEADERS += \ $$PWD/flowlayout.h \ $$PWD/networkaccessmanager.h \ $$PWD/persistentsettings.h \ - $$PWD/completingtextedit.h + $$PWD/completingtextedit.h \ + $$PWD/json.h FORMS += $$PWD/filewizardpage.ui \ $$PWD/projectintropage.ui \ diff --git a/src/plugins/qmljseditor/qmljseditorplugin.cpp b/src/plugins/qmljseditor/qmljseditorplugin.cpp index 165c521837..8a52237cfd 100644 --- a/src/plugins/qmljseditor/qmljseditorplugin.cpp +++ b/src/plugins/qmljseditor/qmljseditorplugin.cpp @@ -70,6 +70,7 @@ #include <texteditor/textfilewizard.h> #include <texteditor/texteditoractionhandler.h> #include <utils/qtcassert.h> +#include <utils/json.h> #include <QtCore/QtPlugin> #include <QtCore/QDebug> @@ -98,7 +99,10 @@ QmlJSEditorPlugin::QmlJSEditorPlugin() : m_actionHandler(0), m_quickFixAssistProvider(0), m_reformatFileAction(0), - m_currentEditor(0) + m_currentEditor(0), + m_jsonManager(new Utils::JsonSchemaManager( + QStringList() << Core::ICore::instance()->userResourcePath() + QLatin1String("/json/") + << Core::ICore::instance()->resourcePath() + QLatin1String("/json/"))) { m_instance = this; } @@ -285,6 +289,11 @@ void QmlJSEditorPlugin::initializeEditor(QmlJSEditor::QmlJSTextEditorWidget *edi TextEditor::TextEditorSettings::instance()->initializeEditor(editor); } +Utils::JsonSchemaManager *QmlJSEditorPlugin::jsonManager() const +{ + return m_jsonManager.data(); +} + void QmlJSEditorPlugin::followSymbolUnderCursor() { Core::EditorManager *em = Core::EditorManager::instance(); diff --git a/src/plugins/qmljseditor/qmljseditorplugin.h b/src/plugins/qmljseditor/qmljseditorplugin.h index 46056fa3f8..3905f8b001 100644 --- a/src/plugins/qmljseditor/qmljseditorplugin.h +++ b/src/plugins/qmljseditor/qmljseditorplugin.h @@ -36,10 +36,15 @@ #include <extensionsystem/iplugin.h> #include <coreplugin/icontext.h> #include <coreplugin/id.h> + #include <QtCore/QPointer> QT_FORWARD_DECLARE_CLASS(QAction) +namespace Utils { +class JsonSchemaManager; +} + namespace TextEditor { class TextEditorActionHandler; } // namespace TextEditor @@ -91,6 +96,8 @@ public: void initializeEditor(QmlJSEditor::QmlJSTextEditorWidget *editor); + Utils::JsonSchemaManager *jsonManager() const; + public Q_SLOTS: void followSymbolUnderCursor(); void findUsages(); @@ -118,6 +125,7 @@ private: QAction *m_reformatFileAction; QPointer<QmlJSTextEditorWidget> m_currentEditor; + QScopedPointer<Utils::JsonSchemaManager> m_jsonManager; }; } // namespace Internal diff --git a/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp b/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp index 12f1dc9ee1..31422d2f53 100644 --- a/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp +++ b/src/plugins/qmljseditor/qmljssemanticinfoupdater.cpp @@ -31,14 +31,20 @@ **************************************************************************/ #include "qmljssemanticinfoupdater.h" +#include "qmljseditorplugin.h" #include <qmljs/qmljsmodelmanagerinterface.h> #include <qmljs/qmljsdocument.h> #include <qmljs/qmljscheck.h> +#include <qmljs/jsoncheck.h> #include <qmljs/qmljscontext.h> #include <qmljs/qmljslink.h> #include <qmljstools/qmljsmodelmanager.h> +#include <coreplugin/icore.h> + +#include <utils/json.h> + namespace QmlJSEditor { namespace Internal { @@ -125,7 +131,14 @@ SemanticInfo SemanticInfoUpdater::makeNewSemanticInfo(const QmlJS::Document::Ptr ScopeChain *scopeChain = new ScopeChain(doc, semanticInfo.context); semanticInfo.m_rootScopeChain = QSharedPointer<const ScopeChain>(scopeChain); - if (doc->language() != Document::JsonLanguage) { + if (doc->language() == Document::JsonLanguage) { + Utils::JsonSchema *schema = + QmlJSEditorPlugin::instance()->jsonManager()->schemaForFile(doc->fileName()); + if (schema) { + JsonCheck jsonChecker(doc); + semanticInfo.staticAnalysisMessages = jsonChecker(schema); + } + } else { Check checker(doc, semanticInfo.context); semanticInfo.staticAnalysisMessages = checker(); } |