1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "projectdescriptionreader.h"
5#include "fmt.h"
6
7#include <QtCore/qcoreapplication.h>
8#include <QtCore/qfile.h>
9#include <QtCore/qjsonarray.h>
10#include <QtCore/qjsondocument.h>
11#include <QtCore/qjsonobject.h>
12#include <QtCore/qset.h>
13
14#include <algorithm>
15#include <functional>
16
17using std::placeholders::_1;
18
19class Validator
20{
21public:
22 Validator(QString *errorString)
23 : m_errorString(errorString)
24 {
25 }
26
27 bool isValidProjectDescription(const QJsonArray &projects)
28 {
29 return std::all_of(first: projects.begin(), last: projects.end(),
30 pred: std::bind(f: &Validator::isValidProjectObject, args: this, args: _1));
31 }
32
33private:
34 bool isValidProject(const QJsonObject &project)
35 {
36 static const QSet<QString> requiredKeys = {
37 QStringLiteral("projectFile"),
38 };
39 static const QSet<QString> allowedKeys
40 = QSet<QString>(requiredKeys)
41 << QStringLiteral("codec")
42 << QStringLiteral("excluded")
43 << QStringLiteral("includePaths")
44 << QStringLiteral("sources")
45 << QStringLiteral("compileCommands")
46 << QStringLiteral("subProjects")
47 << QStringLiteral("translations");
48 QSet<QString> actualKeys;
49 for (auto it = project.constBegin(), end = project.constEnd(); it != end; ++it)
50 actualKeys.insert(value: it.key());
51 const QSet<QString> missingKeys = requiredKeys - actualKeys;
52 if (!missingKeys.isEmpty()) {
53 *m_errorString = FMT::tr(sourceText: "Missing keys in project description: %1.").arg(
54 a: missingKeys.values().join(sep: QLatin1String(", ")));
55 return false;
56 }
57 const QSet<QString> unexpected = actualKeys - allowedKeys;
58 if (!unexpected.isEmpty()) {
59 *m_errorString = FMT::tr(sourceText: "Unexpected keys in project %1: %2").arg(
60 args: project.value(QStringLiteral("projectFile")).toString(),
61 args: unexpected.values().join(sep: QLatin1String(", ")));
62 return false;
63 }
64 return isValidProjectDescription(projects: project.value(QStringLiteral("subProjects")).toArray());
65 }
66
67 bool isValidProjectObject(const QJsonValue &v)
68 {
69 if (!v.isObject()) {
70 *m_errorString = FMT::tr(sourceText: "JSON object expected.");
71 return false;
72 }
73 return isValidProject(project: v.toObject());
74 }
75
76 QString *m_errorString;
77};
78
79static QJsonArray readRawProjectDescription(const QString &filePath, QString *errorString)
80{
81 errorString->clear();
82 QFile file(filePath);
83 if (!file.open(flags: QIODevice::ReadOnly)) {
84 *errorString = FMT::tr(sourceText: "Cannot open project description file '%1'.\n")
85 .arg(a: filePath);
86 return {};
87 }
88 QJsonParseError parseError;
89 QJsonDocument doc = QJsonDocument::fromJson(json: file.readAll(), error: &parseError);
90 if (doc.isNull()) {
91 *errorString = FMT::tr(sourceText: "%1 in %2 at offset %3.\n")
92 .arg(args: parseError.errorString(), args: filePath)
93 .arg(a: parseError.offset);
94 return {};
95 }
96 QJsonArray result = doc.isArray() ? doc.array() : QJsonArray{doc.object()};
97 Validator validator(errorString);
98 if (!validator.isValidProjectDescription(projects: result))
99 return {};
100 return result;
101}
102
103class ProjectConverter
104{
105public:
106 ProjectConverter(QString *errorString)
107 : m_errorString(*errorString)
108 {
109 }
110
111 Projects convertProjects(const QJsonArray &rawProjects)
112 {
113 Projects result;
114 result.reserve(n: rawProjects.size());
115 for (const QJsonValue rawProject : rawProjects) {
116 Project project = convertProject(v: rawProject);
117 if (!m_errorString.isEmpty())
118 break;
119 result.push_back(x: std::move(project));
120 }
121 return result;
122 }
123
124private:
125 Project convertProject(const QJsonValue &v)
126 {
127 if (!v.isObject())
128 return {};
129 Project result;
130 QJsonObject obj = v.toObject();
131 result.filePath = stringValue(obj, key: QLatin1String("projectFile"));
132 result.compileCommands = stringValue(obj, key: QLatin1String("compileCommands"));
133 result.codec = stringValue(obj, key: QLatin1String("codec"));
134 result.excluded = stringListValue(obj, key: QLatin1String("excluded"));
135 result.includePaths = stringListValue(obj, key: QLatin1String("includePaths"));
136 result.sources = stringListValue(obj, key: QLatin1String("sources"));
137 if (obj.contains(key: QLatin1String("translations")))
138 result.translations = stringListValue(obj, key: QLatin1String("translations"));
139 result.subProjects = convertProjects(rawProjects: obj.value(key: QLatin1String("subProjects")).toArray());
140 return result;
141 }
142
143 bool checkType(const QJsonValue &v, QJsonValue::Type t, const QString &key)
144 {
145 if (v.type() == t)
146 return true;
147 m_errorString = FMT::tr(sourceText: "Key %1 should be %2 but is %3.").arg(args: key, args: jsonTypeName(t),
148 args: jsonTypeName(t: v.type()));
149 return false;
150 }
151
152 static QString jsonTypeName(QJsonValue::Type t)
153 {
154 // ### If QJsonValue::Type was declared with Q_ENUM we could just query QMetaEnum.
155 switch (t) {
156 case QJsonValue::Null:
157 return QStringLiteral("null");
158 case QJsonValue::Bool:
159 return QStringLiteral("bool");
160 case QJsonValue::Double:
161 return QStringLiteral("double");
162 case QJsonValue::String:
163 return QStringLiteral("string");
164 case QJsonValue::Array:
165 return QStringLiteral("array");
166 case QJsonValue::Object:
167 return QStringLiteral("object");
168 case QJsonValue::Undefined:
169 return QStringLiteral("undefined");
170 }
171 return QStringLiteral("unknown");
172 }
173
174 QString stringValue(const QJsonObject &obj, const QString &key)
175 {
176 if (!m_errorString.isEmpty())
177 return {};
178 QJsonValue v = obj.value(key);
179 if (v.isUndefined())
180 return {};
181 if (!checkType(v, t: QJsonValue::String, key))
182 return {};
183 return v.toString();
184 }
185
186 QStringList stringListValue(const QJsonObject &obj, const QString &key)
187 {
188 if (!m_errorString.isEmpty())
189 return {};
190 QJsonValue v = obj.value(key);
191 if (v.isUndefined())
192 return {};
193 if (!checkType(v, t: QJsonValue::Array, key))
194 return {};
195 return toStringList(v, key);
196 }
197
198 QStringList toStringList(const QJsonValue &v, const QString &key)
199 {
200 QStringList result;
201 const QJsonArray a = v.toArray();
202 result.reserve(asize: a.count());
203 for (const QJsonValue v : a) {
204 if (!v.isString()) {
205 m_errorString = FMT::tr(sourceText: "Unexpected type %1 in string array in key %2.")
206 .arg(args: jsonTypeName(t: v.type()), args: key);
207 return {};
208 }
209 result.append(t: v.toString());
210 }
211 return result;
212 }
213
214 QString &m_errorString;
215};
216
217Projects readProjectDescription(const QString &filePath, QString *errorString)
218{
219 const QJsonArray rawProjects = readRawProjectDescription(filePath, errorString);
220 if (!errorString->isEmpty())
221 return {};
222 ProjectConverter converter(errorString);
223 Projects result = converter.convertProjects(rawProjects);
224 if (!errorString->isEmpty())
225 return {};
226 return result;
227}
228

source code of qttools/src/linguist/shared/projectdescriptionreader.cpp