1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qbs.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39
40 #include "scriptengine.h"
41
42 #include "filecontextbase.h"
43 #include "jsimports.h"
44 #include "propertymapinternal.h"
45 #include "scriptimporter.h"
46 #include "preparescriptobserver.h"
47
48 #include <buildgraph/artifact.h>
49 #include <jsextensions/jsextensions.h>
50 #include <logging/translator.h>
51 #include <tools/error.h>
52 #include <tools/fileinfo.h>
53 #include <tools/profiling.h>
54 #include <tools/qbsassert.h>
55 #include <tools/qttools.h>
56 #include <tools/stlutils.h>
57 #include <tools/stringconstants.h>
58
59 #include <QtCore/qdebug.h>
60 #include <QtCore/qdiriterator.h>
61 #include <QtCore/qfile.h>
62 #include <QtCore/qfileinfo.h>
63 #include <QtCore/qtextstream.h>
64 #include <QtCore/qtimer.h>
65
66 #include <QtScript/qscriptclass.h>
67 #include <QtScript/qscriptvalueiterator.h>
68
69 #include <functional>
70 #include <set>
71 #include <utility>
72
73 namespace qbs {
74 namespace Internal {
75
getterFuncHelperProperty()76 static QString getterFuncHelperProperty() { return QStringLiteral("qbsdata"); }
77
78 const bool debugJSImports = false;
79
operator ==(const ScriptEngine::PropertyCacheKey & lhs,const ScriptEngine::PropertyCacheKey & rhs)80 bool operator==(const ScriptEngine::PropertyCacheKey &lhs,
81 const ScriptEngine::PropertyCacheKey &rhs)
82 {
83 return lhs.m_propertyMap == rhs.m_propertyMap
84 && lhs.m_moduleName == rhs.m_moduleName
85 && lhs.m_propertyName == rhs.m_propertyName;
86 }
87
88
89
combineHash(QHashValueType h1,QHashValueType h2,QHashValueType seed)90 static inline QHashValueType combineHash(QHashValueType h1, QHashValueType h2, QHashValueType seed)
91 {
92 // stolen from qHash(QPair)
93 return ((h1 << 16) | (h1 >> 16)) ^ h2 ^ seed;
94 }
95
qHash(const ScriptEngine::PropertyCacheKey & k,QHashValueType seed=0)96 QHashValueType qHash(const ScriptEngine::PropertyCacheKey &k, QHashValueType seed = 0)
97 {
98 return combineHash(qHash(k.m_moduleName),
99 combineHash(qHash(k.m_propertyName), qHash(k.m_propertyMap), seed), seed);
100 }
101
102 std::mutex ScriptEngine::m_creationDestructionMutex;
103
ScriptEngine(Logger & logger,EvalContext evalContext,QObject * parent)104 ScriptEngine::ScriptEngine(Logger &logger, EvalContext evalContext, QObject *parent)
105 : QScriptEngine(parent), m_scriptImporter(new ScriptImporter(this)),
106 m_modulePropertyScriptClass(nullptr),
107 m_propertyCacheEnabled(true), m_active(false), m_logger(logger), m_evalContext(evalContext),
108 m_observer(new PrepareScriptObserver(this, UnobserveMode::Disabled))
109 {
110 setProcessEventsInterval(1000); // For the cancelation mechanism to work.
111 m_cancelationError = currentContext()->throwValue(tr("Execution canceled"));
112 QScriptValue objectProto = globalObject().property(QStringLiteral("Object"));
113 m_definePropertyFunction = objectProto.property(QStringLiteral("defineProperty"));
114 QBS_ASSERT(m_definePropertyFunction.isFunction(), /* ignore */);
115 m_emptyFunction = evaluate(QStringLiteral("(function(){})"));
116 QBS_ASSERT(m_emptyFunction.isFunction(), /* ignore */);
117 // Initially push a new context to turn off scope chain insanity mode.
118 QScriptEngine::pushContext();
119 installQbsBuiltins();
120 extendJavaScriptBuiltins();
121 }
122
create(Logger & logger,EvalContext evalContext,QObject * parent)123 ScriptEngine *ScriptEngine::create(Logger &logger, EvalContext evalContext, QObject *parent)
124 {
125 std::lock_guard<std::mutex> lock(m_creationDestructionMutex);
126 return new ScriptEngine(logger, evalContext, parent);
127 }
128
~ScriptEngine()129 ScriptEngine::~ScriptEngine()
130 {
131 m_creationDestructionMutex.lock();
132 connect(this, &QObject::destroyed, std::bind(&std::mutex::unlock, &m_creationDestructionMutex));
133
134 releaseResourcesOfScriptObjects();
135 delete (m_scriptImporter);
136 if (m_elapsedTimeImporting != -1) {
137 m_logger.qbsLog(LoggerInfo, true) << Tr::tr("Setting up imports took %1.")
138 .arg(elapsedTimeString(m_elapsedTimeImporting));
139 }
140 delete m_modulePropertyScriptClass;
141 delete m_productPropertyScriptClass;
142 }
143
import(const FileContextBaseConstPtr & fileCtx,QScriptValue & targetObject,ObserveMode observeMode)144 void ScriptEngine::import(const FileContextBaseConstPtr &fileCtx, QScriptValue &targetObject,
145 ObserveMode observeMode)
146 {
147 installImportFunctions();
148 m_currentDirPathStack.push(FileInfo::path(fileCtx->filePath()));
149 m_extensionSearchPathsStack.push(fileCtx->searchPaths());
150 m_observeMode = observeMode;
151
152 for (const JsImport &jsImport : fileCtx->jsImports())
153 import(jsImport, targetObject);
154 if (m_observeMode == ObserveMode::Enabled) {
155 for (QScriptValue &sv : m_requireResults)
156 observeImport(sv);
157 m_requireResults.clear();
158 }
159
160 m_currentDirPathStack.pop();
161 m_extensionSearchPathsStack.pop();
162 uninstallImportFunctions();
163 }
164
import(const JsImport & jsImport,QScriptValue & targetObject)165 void ScriptEngine::import(const JsImport &jsImport, QScriptValue &targetObject)
166 {
167 QBS_ASSERT(targetObject.isObject(), return);
168 QBS_ASSERT(targetObject.engine() == this, return);
169
170 if (debugJSImports)
171 qDebug() << "[ENGINE] import into " << jsImport.scopeName;
172
173 QScriptValue jsImportValue = m_jsImportCache.value(jsImport);
174 if (jsImportValue.isValid()) {
175 if (debugJSImports)
176 qDebug() << "[ENGINE] " << jsImport.filePaths << " (cache hit)";
177 } else {
178 if (debugJSImports)
179 qDebug() << "[ENGINE] " << jsImport.filePaths << " (cache miss)";
180 jsImportValue = newObject();
181 for (const QString &filePath : jsImport.filePaths)
182 importFile(filePath, jsImportValue);
183 m_jsImportCache.insert(jsImport, jsImportValue);
184 std::vector<QString> &filePathsForScriptValue
185 = m_filePathsPerImport[jsImportValue.objectId()];
186 for (const QString &fp : jsImport.filePaths)
187 filePathsForScriptValue.push_back(fp);
188 }
189
190 QScriptValue sv = newObject();
191 sv.setPrototype(jsImportValue);
192 sv.setProperty(StringConstants::importScopeNamePropertyInternal(), jsImport.scopeName);
193 targetObject.setProperty(jsImport.scopeName, sv);
194 if (m_observeMode == ObserveMode::Enabled)
195 observeImport(jsImportValue);
196 }
197
observeImport(QScriptValue & jsImport)198 void ScriptEngine::observeImport(QScriptValue &jsImport)
199 {
200 if (!m_observer->addImportId(jsImport.objectId()))
201 return;
202 QScriptValueIterator it(jsImport);
203 while (it.hasNext()) {
204 it.next();
205 if (it.flags() & QScriptValue::PropertyGetter)
206 continue;
207 QScriptValue property = it.value();
208 if (!property.isFunction())
209 continue;
210 setObservedProperty(jsImport, it.name(), property);
211 }
212 }
213
clearImportsCache()214 void ScriptEngine::clearImportsCache()
215 {
216 m_jsImportCache.clear();
217 }
218
checkContext(const QString & operation,const DubiousContextList & dubiousContexts)219 void ScriptEngine::checkContext(const QString &operation,
220 const DubiousContextList &dubiousContexts)
221 {
222 for (const DubiousContext &info : dubiousContexts) {
223 if (info.context != evalContext())
224 continue;
225 QString warning;
226 switch (info.context) {
227 case EvalContext::PropertyEvaluation:
228 warning = Tr::tr("Suspicious use of %1 during property evaluation.").arg(operation);
229 if (info.suggestion == DubiousContext::SuggestMoving)
230 warning += QLatin1Char(' ') + Tr::tr("Should this call be in a Probe instead?");
231 break;
232 case EvalContext::RuleExecution:
233 warning = Tr::tr("Suspicious use of %1 during rule execution.").arg(operation);
234 if (info.suggestion == DubiousContext::SuggestMoving) {
235 warning += QLatin1Char(' ')
236 + Tr::tr("Should this call be in a JavaScriptCommand instead?");
237 }
238 break;
239 case EvalContext::ModuleProvider:
240 case EvalContext::ProbeExecution:
241 case EvalContext::JsCommand:
242 QBS_ASSERT(false, continue);
243 break;
244 }
245 m_logger.printWarning(ErrorInfo(warning, currentContext()->backtrace()));
246 return;
247 }
248 }
249
addPropertyRequestedFromArtifact(const Artifact * artifact,const Property & property)250 void ScriptEngine::addPropertyRequestedFromArtifact(const Artifact *artifact,
251 const Property &property)
252 {
253 m_propertiesRequestedFromArtifact[artifact->filePath()] << property;
254 }
255
addImportRequestedInScript(qint64 importValueId)256 void ScriptEngine::addImportRequestedInScript(qint64 importValueId)
257 {
258 // Import list is assumed to be small, so let's not use a set.
259 if (!contains(m_importsRequestedInScript, importValueId))
260 m_importsRequestedInScript.push_back(importValueId);
261 }
262
importedFilesUsedInScript() const263 std::vector<QString> ScriptEngine::importedFilesUsedInScript() const
264 {
265 std::vector<QString> files;
266 for (qint64 usedImport : m_importsRequestedInScript) {
267 const auto it = m_filePathsPerImport.find(usedImport);
268 QBS_CHECK(it != m_filePathsPerImport.cend());
269 const std::vector<QString> &filePathsForImport = it->second;
270 for (const QString &fp : filePathsForImport)
271 if (!contains(files, fp))
272 files.push_back(fp);
273 }
274 return files;
275 }
276
enableProfiling(bool enable)277 void ScriptEngine::enableProfiling(bool enable)
278 {
279 m_elapsedTimeImporting = enable ? 0 : -1;
280 }
281
addToPropertyCache(const QString & moduleName,const QString & propertyName,const PropertyMapConstPtr & propertyMap,const QVariant & value)282 void ScriptEngine::addToPropertyCache(const QString &moduleName, const QString &propertyName,
283 const PropertyMapConstPtr &propertyMap, const QVariant &value)
284 {
285 m_propertyCache.insert(PropertyCacheKey(moduleName, propertyName, propertyMap), value);
286 }
287
retrieveFromPropertyCache(const QString & moduleName,const QString & propertyName,const PropertyMapConstPtr & propertyMap)288 QVariant ScriptEngine::retrieveFromPropertyCache(const QString &moduleName,
289 const QString &propertyName, const PropertyMapConstPtr &propertyMap)
290 {
291 return m_propertyCache.value(PropertyCacheKey(moduleName, propertyName, propertyMap));
292 }
293
defineProperty(QScriptValue & object,const QString & name,const QScriptValue & descriptor)294 void ScriptEngine::defineProperty(QScriptValue &object, const QString &name,
295 const QScriptValue &descriptor)
296 {
297 QScriptValue arguments = newArray();
298 arguments.setProperty(0, object);
299 arguments.setProperty(1, name);
300 arguments.setProperty(2, descriptor);
301 QScriptValue result = m_definePropertyFunction.call(QScriptValue(), arguments);
302 QBS_ASSERT(!hasErrorOrException(result), qDebug() << name << result.toString());
303 }
304
js_observedGet(QScriptContext * context,QScriptEngine *,ScriptPropertyObserver * const observer)305 static QScriptValue js_observedGet(QScriptContext *context, QScriptEngine *,
306 ScriptPropertyObserver * const observer)
307 {
308 const QScriptValue data = context->callee().property(getterFuncHelperProperty());
309 const QScriptValue value = data.property(2);
310 observer->onPropertyRead(data.property(0), data.property(1).toVariant().toString(), value);
311 return value;
312 }
313
setObservedProperty(QScriptValue & object,const QString & name,const QScriptValue & value)314 void ScriptEngine::setObservedProperty(QScriptValue &object, const QString &name,
315 const QScriptValue &value)
316 {
317 QScriptValue data = newArray();
318 data.setProperty(0, object);
319 data.setProperty(1, name);
320 data.setProperty(2, value);
321 QScriptValue getterFunc = newFunction(js_observedGet,
322 static_cast<ScriptPropertyObserver *>(m_observer.get()));
323 getterFunc.setProperty(getterFuncHelperProperty(), data);
324 object.setProperty(name, getterFunc, QScriptValue::PropertyGetter);
325 if (m_observer->unobserveMode() == UnobserveMode::Enabled)
326 m_observedProperties.emplace_back(object, name, value);
327 }
328
unobserveProperties()329 void ScriptEngine::unobserveProperties()
330 {
331 for (auto &elem : m_observedProperties) {
332 QScriptValue &object = std::get<0>(elem);
333 const QString &name = std::get<1>(elem);
334 const QScriptValue &value = std::get<2>(elem);
335 object.setProperty(name, QScriptValue(), QScriptValue::PropertyGetter);
336 object.setProperty(name, value, QScriptValue::PropertyFlags());
337 }
338 m_observedProperties.clear();
339 }
340
js_deprecatedGet(QScriptContext * context,QScriptEngine * qtengine)341 static QScriptValue js_deprecatedGet(QScriptContext *context, QScriptEngine *qtengine)
342 {
343 const auto engine = static_cast<const ScriptEngine *>(qtengine);
344 const QScriptValue data = context->callee().property(getterFuncHelperProperty());
345 engine->logger().qbsWarning()
346 << ScriptEngine::tr("Property %1 is deprecated. Please use %2 instead.").arg(
347 data.property(0).toString(), data.property(1).toString());
348 return data.property(2);
349 }
350
setDeprecatedProperty(QScriptValue & object,const QString & oldName,const QString & newName,const QScriptValue & value)351 void ScriptEngine::setDeprecatedProperty(QScriptValue &object, const QString &oldName,
352 const QString &newName, const QScriptValue &value)
353 {
354 QScriptValue data = newArray();
355 data.setProperty(0, oldName);
356 data.setProperty(1, newName);
357 data.setProperty(2, value);
358 QScriptValue getterFunc = newFunction(js_deprecatedGet);
359 getterFunc.setProperty(getterFuncHelperProperty(), data);
360 object.setProperty(oldName, getterFunc, QScriptValue::PropertyGetter
361 | QScriptValue::SkipInEnumeration);
362 }
363
environment() const364 QProcessEnvironment ScriptEngine::environment() const
365 {
366 return m_environment;
367 }
368
setEnvironment(const QProcessEnvironment & env)369 void ScriptEngine::setEnvironment(const QProcessEnvironment &env)
370 {
371 m_environment = env;
372 }
373
importFile(const QString & filePath,QScriptValue & targetObject)374 void ScriptEngine::importFile(const QString &filePath, QScriptValue &targetObject)
375 {
376 AccumulatingTimer importTimer(m_elapsedTimeImporting != -1 ? &m_elapsedTimeImporting : nullptr);
377 QScriptValue &evaluationResult = m_jsFileCache[filePath];
378 if (evaluationResult.isValid()) {
379 ScriptImporter::copyProperties(evaluationResult, targetObject);
380 return;
381 }
382 QFile file(filePath);
383 if (Q_UNLIKELY(!file.open(QFile::ReadOnly)))
384 throw ErrorInfo(tr("Cannot open '%1'.").arg(filePath));
385 QTextStream stream(&file);
386 setupDefaultCodec(stream);
387 const QString sourceCode = stream.readAll();
388 file.close();
389 m_currentDirPathStack.push(FileInfo::path(filePath));
390 evaluationResult = m_scriptImporter->importSourceCode(sourceCode, filePath, targetObject);
391 m_currentDirPathStack.pop();
392 }
393
findExtensionDir(const QStringList & searchPaths,const QString & extensionPath)394 static QString findExtensionDir(const QStringList &searchPaths, const QString &extensionPath)
395 {
396 for (const QString &searchPath : searchPaths) {
397 const QString dirPath = searchPath + QStringLiteral("/imports/") + extensionPath;
398 QFileInfo fi(dirPath);
399 if (fi.exists() && fi.isDir())
400 return dirPath;
401 }
402 return {};
403 }
404
mergeExtensionObjects(const QScriptValueList & lst)405 static QScriptValue mergeExtensionObjects(const QScriptValueList &lst)
406 {
407 QScriptValue result;
408 for (const QScriptValue &v : lst) {
409 if (!result.isValid()) {
410 result = v;
411 continue;
412 }
413 QScriptValueIterator svit(v);
414 while (svit.hasNext()) {
415 svit.next();
416 result.setProperty(svit.name(), svit.value());
417 }
418 }
419 return result;
420 }
421
loadInternalExtension(QScriptContext * context,ScriptEngine * engine,const QString & uri)422 static QScriptValue loadInternalExtension(QScriptContext *context, ScriptEngine *engine,
423 const QString &uri)
424 {
425 const QString name = uri.mid(4); // remove the "qbs." part
426 QScriptValue extensionObj = JsExtensions::loadExtension(engine, name);
427 if (!extensionObj.isValid()) {
428 return context->throwError(ScriptEngine::tr("loadExtension: "
429 "cannot load extension '%1'.").arg(uri));
430 }
431 return extensionObj;
432 }
433
js_loadExtension(QScriptContext * context,QScriptEngine * qtengine)434 QScriptValue ScriptEngine::js_loadExtension(QScriptContext *context, QScriptEngine *qtengine)
435 {
436 if (context->argumentCount() < 1) {
437 return context->throwError(
438 ScriptEngine::tr("The loadExtension function requires "
439 "an extension name."));
440 }
441
442 const auto engine = static_cast<const ScriptEngine *>(qtengine);
443 ErrorInfo deprWarning(Tr::tr("The loadExtension() function is deprecated and will be "
444 "removed in a future version of Qbs. Use require() "
445 "instead."), context->backtrace());
446 engine->logger().printWarning(deprWarning);
447
448 return js_require(context, qtengine);
449 }
450
js_loadFile(QScriptContext * context,QScriptEngine * qtengine)451 QScriptValue ScriptEngine::js_loadFile(QScriptContext *context, QScriptEngine *qtengine)
452 {
453 if (context->argumentCount() < 1) {
454 return context->throwError(
455 ScriptEngine::tr("The loadFile function requires a file path."));
456 }
457
458 const auto engine = static_cast<const ScriptEngine *>(qtengine);
459 ErrorInfo deprWarning(Tr::tr("The loadFile() function is deprecated and will be "
460 "removed in a future version of Qbs. Use require() "
461 "instead."), context->backtrace());
462 engine->logger().printWarning(deprWarning);
463
464 return js_require(context, qtengine);
465 }
466
js_require(QScriptContext * context,QScriptEngine * qtengine)467 QScriptValue ScriptEngine::js_require(QScriptContext *context, QScriptEngine *qtengine)
468 {
469 const auto engine = static_cast<ScriptEngine *>(qtengine);
470 if (context->argumentCount() < 1) {
471 return context->throwError(
472 ScriptEngine::tr("The require function requires a module name or path."));
473 }
474
475 const QString moduleName = context->argument(0).toString();
476
477 // First try to load a named module if the argument doesn't look like a file path
478 if (!moduleName.contains(QLatin1Char('/'))) {
479 if (engine->m_extensionSearchPathsStack.empty())
480 return context->throwError(
481 ScriptEngine::tr("require: internal error. No search paths."));
482
483 if (engine->m_logger.debugEnabled()) {
484 engine->m_logger.qbsDebug()
485 << "[require] loading extension " << moduleName;
486 }
487
488 QString moduleNameAsPath = moduleName;
489 moduleNameAsPath.replace(QLatin1Char('.'), QLatin1Char('/'));
490 const QStringList searchPaths = engine->m_extensionSearchPathsStack.top();
491 const QString dirPath = findExtensionDir(searchPaths, moduleNameAsPath);
492 if (dirPath.isEmpty()) {
493 if (moduleName.startsWith(QStringLiteral("qbs.")))
494 return loadInternalExtension(context, engine, moduleName);
495 } else {
496 QDirIterator dit(dirPath, StringConstants::jsFileWildcards(),
497 QDir::Files | QDir::Readable);
498 QScriptValueList values;
499 std::vector<QString> filePaths;
500 try {
501 while (dit.hasNext()) {
502 const QString filePath = dit.next();
503 if (engine->m_logger.debugEnabled()) {
504 engine->m_logger.qbsDebug()
505 << "[require] importing file " << filePath;
506 }
507 QScriptValue obj = engine->newObject();
508 engine->importFile(filePath, obj);
509 values << obj;
510 filePaths.push_back(filePath);
511 }
512 } catch (const ErrorInfo &e) {
513 return context->throwError(e.toString());
514 }
515
516 if (!values.empty()) {
517 const QScriptValue mergedValue = mergeExtensionObjects(values);
518 engine->m_requireResults.push_back(mergedValue);
519 engine->m_filePathsPerImport[mergedValue.objectId()] = filePaths;
520 return mergedValue;
521 }
522 }
523
524 // The module name might be a file name component, which is assumed to be to a JavaScript
525 // file located in the current directory search path; try that next
526 }
527
528 if (engine->m_currentDirPathStack.empty()) {
529 return context->throwError(
530 ScriptEngine::tr("require: internal error. No current directory."));
531 }
532
533 QScriptValue result;
534 try {
535 const QString filePath = FileInfo::resolvePath(engine->m_currentDirPathStack.top(),
536 moduleName);
537 result = engine->newObject();
538 engine->importFile(filePath, result);
539 static const QString scopeNamePrefix = QStringLiteral("_qbs_scope_");
540 const QString scopeName = scopeNamePrefix + QString::number(qHash(filePath), 16);
541 result.setProperty(StringConstants::importScopeNamePropertyInternal(), scopeName);
542 context->thisObject().setProperty(scopeName, result);
543 engine->m_requireResults.push_back(result);
544 engine->m_filePathsPerImport[result.objectId()] = { filePath };
545 } catch (const ErrorInfo &e) {
546 result = context->throwError(e.toString());
547 }
548
549 return result;
550 }
551
modulePropertyScriptClass() const552 QScriptClass *ScriptEngine::modulePropertyScriptClass() const
553 {
554 return m_modulePropertyScriptClass;
555 }
556
setModulePropertyScriptClass(QScriptClass * modulePropertyScriptClass)557 void ScriptEngine::setModulePropertyScriptClass(QScriptClass *modulePropertyScriptClass)
558 {
559 m_modulePropertyScriptClass = modulePropertyScriptClass;
560 }
561
addResourceAcquiringScriptObject(ResourceAcquiringScriptObject * obj)562 void ScriptEngine::addResourceAcquiringScriptObject(ResourceAcquiringScriptObject *obj)
563 {
564 m_resourceAcquiringScriptObjects.push_back(obj);
565 }
566
releaseResourcesOfScriptObjects()567 void ScriptEngine::releaseResourcesOfScriptObjects()
568 {
569 if (m_resourceAcquiringScriptObjects.empty())
570 return;
571 std::for_each(m_resourceAcquiringScriptObjects.begin(), m_resourceAcquiringScriptObjects.end(),
572 std::mem_fn(&ResourceAcquiringScriptObject::releaseResources));
573 m_resourceAcquiringScriptObjects.clear();
574 }
575
addCanonicalFilePathResult(const QString & filePath,const QString & resultFilePath)576 void ScriptEngine::addCanonicalFilePathResult(const QString &filePath,
577 const QString &resultFilePath)
578 {
579 if (gatherFileResults())
580 m_canonicalFilePathResult.insert(filePath, resultFilePath);
581 }
582
addFileExistsResult(const QString & filePath,bool exists)583 void ScriptEngine::addFileExistsResult(const QString &filePath, bool exists)
584 {
585 if (gatherFileResults())
586 m_fileExistsResult.insert(filePath, exists);
587 }
588
addDirectoryEntriesResult(const QString & path,QDir::Filters filters,const QStringList & entries)589 void ScriptEngine::addDirectoryEntriesResult(const QString &path, QDir::Filters filters,
590 const QStringList &entries)
591 {
592 if (gatherFileResults()) {
593 m_directoryEntriesResult.insert(
594 std::pair<QString, quint32>(path, static_cast<quint32>(filters)),
595 entries);
596 }
597 }
598
addFileLastModifiedResult(const QString & filePath,const FileTime & fileTime)599 void ScriptEngine::addFileLastModifiedResult(const QString &filePath, const FileTime &fileTime)
600 {
601 if (gatherFileResults())
602 m_fileLastModifiedResult.insert(filePath, fileTime);
603 }
604
imports() const605 Set<QString> ScriptEngine::imports() const
606 {
607 Set<QString> filePaths;
608 for (auto it = m_jsImportCache.cbegin(); it != m_jsImportCache.cend(); ++it) {
609 const JsImport &jsImport = it.key();
610 for (const QString &filePath : jsImport.filePaths)
611 filePaths << filePath;
612 }
613 for (const auto &kv : m_filePathsPerImport) {
614 for (const QString &fp : kv.second)
615 filePaths << fp;
616 }
617 return filePaths;
618 }
619
argumentList(const QStringList & argumentNames,const QScriptValue & context)620 QScriptValueList ScriptEngine::argumentList(const QStringList &argumentNames,
621 const QScriptValue &context)
622 {
623 QScriptValueList result;
624 for (const auto &name : argumentNames)
625 result += context.property(name);
626 return result;
627 }
628
lastErrorLocation(const QScriptValue & v,const CodeLocation & fallbackLocation) const629 CodeLocation ScriptEngine::lastErrorLocation(const QScriptValue &v,
630 const CodeLocation &fallbackLocation) const
631 {
632 const QScriptValue &errorVal = lastErrorValue(v);
633 const CodeLocation errorLoc(errorVal.property(StringConstants::fileNameProperty()).toString(),
634 errorVal.property(QStringLiteral("lineNumber")).toInt32(),
635 errorVal.property(QStringLiteral("expressionCaretOffset")).toInt32(),
636 false);
637 return errorLoc.isValid() ? errorLoc : fallbackLocation;
638 }
639
lastError(const QScriptValue & v,const CodeLocation & fallbackLocation) const640 ErrorInfo ScriptEngine::lastError(const QScriptValue &v, const CodeLocation &fallbackLocation) const
641 {
642 const QString msg = lastErrorString(v);
643 CodeLocation errorLocation = lastErrorLocation(v);
644 if (errorLocation.isValid())
645 return ErrorInfo(msg, errorLocation);
646 const QStringList backtrace = uncaughtExceptionBacktraceOrEmpty();
647 if (!backtrace.empty()) {
648 ErrorInfo e(msg, backtrace);
649 if (e.hasLocation())
650 return e;
651 }
652 return ErrorInfo(msg, fallbackLocation);
653 }
654
cancel()655 void ScriptEngine::cancel()
656 {
657 QTimer::singleShot(0, this, [this] { abort(); });
658 }
659
abort()660 void ScriptEngine::abort()
661 {
662 abortEvaluation(m_cancelationError);
663 }
664
gatherFileResults() const665 bool ScriptEngine::gatherFileResults() const
666 {
667 return evalContext() == EvalContext::PropertyEvaluation
668 || evalContext() == EvalContext::ProbeExecution;
669 }
670
671 class JSTypeExtender
672 {
673 public:
JSTypeExtender(ScriptEngine * engine,const QString & typeName)674 JSTypeExtender(ScriptEngine *engine, const QString &typeName)
675 : m_engine(engine)
676 {
677 m_proto = engine->globalObject().property(typeName)
678 .property(QStringLiteral("prototype"));
679 QBS_ASSERT(m_proto.isObject(), return);
680 m_descriptor = engine->newObject();
681 }
682
addFunction(const QString & name,const QString & code)683 void addFunction(const QString &name, const QString &code)
684 {
685 QScriptValue f = m_engine->evaluate(code);
686 QBS_ASSERT(f.isFunction(), return);
687 m_descriptor.setProperty(QStringLiteral("value"), f);
688 m_engine->defineProperty(m_proto, name, m_descriptor);
689 }
690
691 private:
692 ScriptEngine *const m_engine;
693 QScriptValue m_proto;
694 QScriptValue m_descriptor;
695 };
696
js_consoleError(QScriptContext * context,QScriptEngine * engine,Logger * logger)697 static QScriptValue js_consoleError(QScriptContext *context, QScriptEngine *engine, Logger *logger)
698 {
699 if (Q_UNLIKELY(context->argumentCount() != 1))
700 return context->throwError(QScriptContext::SyntaxError,
701 QStringLiteral("console.error() expects 1 argument"));
702 logger->qbsLog(LoggerError) << context->argument(0).toString();
703 return engine->undefinedValue();
704 }
705
js_consoleWarn(QScriptContext * context,QScriptEngine * engine,Logger * logger)706 static QScriptValue js_consoleWarn(QScriptContext *context, QScriptEngine *engine, Logger *logger)
707 {
708 if (Q_UNLIKELY(context->argumentCount() != 1))
709 return context->throwError(QScriptContext::SyntaxError,
710 QStringLiteral("console.warn() expects 1 argument"));
711 logger->qbsWarning() << context->argument(0).toString();
712 return engine->undefinedValue();
713 }
714
js_consoleInfo(QScriptContext * context,QScriptEngine * engine,Logger * logger)715 static QScriptValue js_consoleInfo(QScriptContext *context, QScriptEngine *engine, Logger *logger)
716 {
717 if (Q_UNLIKELY(context->argumentCount() != 1))
718 return context->throwError(QScriptContext::SyntaxError,
719 QStringLiteral("console.info() expects 1 argument"));
720 logger->qbsInfo() << context->argument(0).toString();
721 return engine->undefinedValue();
722 }
723
js_consoleDebug(QScriptContext * context,QScriptEngine * engine,Logger * logger)724 static QScriptValue js_consoleDebug(QScriptContext *context, QScriptEngine *engine, Logger *logger)
725 {
726 if (Q_UNLIKELY(context->argumentCount() != 1))
727 return context->throwError(QScriptContext::SyntaxError,
728 QStringLiteral("console.debug() expects 1 argument"));
729 logger->qbsDebug() << context->argument(0).toString();
730 return engine->undefinedValue();
731 }
732
js_consoleLog(QScriptContext * context,QScriptEngine * engine,Logger * logger)733 static QScriptValue js_consoleLog(QScriptContext *context, QScriptEngine *engine, Logger *logger)
734 {
735 return js_consoleDebug(context, engine, logger);
736 }
737
installQbsBuiltins()738 void ScriptEngine::installQbsBuiltins()
739 {
740 globalObject().setProperty(StringConstants::qbsModule(), m_qbsObject = newObject());
741
742 globalObject().setProperty(QStringLiteral("console"), m_consoleObject = newObject());
743 installConsoleFunction(QStringLiteral("debug"), &js_consoleDebug);
744 installConsoleFunction(QStringLiteral("error"), &js_consoleError);
745 installConsoleFunction(QStringLiteral("info"), &js_consoleInfo);
746 installConsoleFunction(QStringLiteral("log"), &js_consoleLog);
747 installConsoleFunction(QStringLiteral("warn"), &js_consoleWarn);
748 }
749
extendJavaScriptBuiltins()750 void ScriptEngine::extendJavaScriptBuiltins()
751 {
752 JSTypeExtender arrayExtender(this, QStringLiteral("Array"));
753 arrayExtender.addFunction(QStringLiteral("contains"),
754 QStringLiteral("(function(e){return this.indexOf(e) !== -1;})"));
755 arrayExtender.addFunction(QStringLiteral("containsAll"),
756 QStringLiteral("(function(e){var $this = this;"
757 "return e.every(function (v) { return $this.contains(v) });})"));
758 arrayExtender.addFunction(QStringLiteral("containsAny"),
759 QStringLiteral("(function(e){var $this = this;"
760 "return e.some(function (v) { return $this.contains(v) });})"));
761 arrayExtender.addFunction(QStringLiteral("uniqueConcat"),
762 QStringLiteral("(function(other){"
763 "var r = this.concat();"
764 "var s = {};"
765 "r.forEach(function(x){ s[x] = true; });"
766 "other.forEach(function(x){"
767 "if (!s[x]) {"
768 "s[x] = true;"
769 "r.push(x);"
770 "}"
771 "});"
772 "return r;})"));
773
774 JSTypeExtender stringExtender(this, QStringLiteral("String"));
775 stringExtender.addFunction(QStringLiteral("contains"),
776 QStringLiteral("(function(e){return this.indexOf(e) !== -1;})"));
777 stringExtender.addFunction(QStringLiteral("startsWith"),
778 QStringLiteral("(function(e){return this.slice(0, e.length) === e;})"));
779 stringExtender.addFunction(QStringLiteral("endsWith"),
780 QStringLiteral("(function(e){return this.slice(-e.length) === e;})"));
781 }
782
installFunction(const QString & name,int length,QScriptValue * functionValue,FunctionSignature f,QScriptValue * targetObject=nullptr)783 void ScriptEngine::installFunction(const QString &name, int length, QScriptValue *functionValue,
784 FunctionSignature f, QScriptValue *targetObject = nullptr)
785 {
786 if (!functionValue->isValid())
787 *functionValue = newFunction(f, length);
788 (targetObject ? *targetObject : globalObject()).setProperty(name, *functionValue);
789 }
790
installQbsFunction(const QString & name,int length,FunctionSignature f)791 void ScriptEngine::installQbsFunction(const QString &name, int length, FunctionSignature f)
792 {
793 QScriptValue functionValue;
794 installFunction(name, length, &functionValue, f, &m_qbsObject);
795 }
796
installConsoleFunction(const QString & name,QScriptValue (* f)(QScriptContext *,QScriptEngine *,Logger *))797 void ScriptEngine::installConsoleFunction(const QString &name,
798 QScriptValue (*f)(QScriptContext *, QScriptEngine *, Logger *))
799 {
800 m_consoleObject.setProperty(name, newFunction(f, &m_logger));
801 }
802
loadFileString()803 static QString loadFileString() { return QStringLiteral("loadFile"); }
loadExtensionString()804 static QString loadExtensionString() { return QStringLiteral("loadExtension"); }
requireString()805 static QString requireString() { return QStringLiteral("require"); }
806
installImportFunctions()807 void ScriptEngine::installImportFunctions()
808 {
809 installFunction(loadFileString(), 1, &m_loadFileFunction, js_loadFile);
810 installFunction(loadExtensionString(), 1, &m_loadExtensionFunction, js_loadExtension);
811 installFunction(requireString(), 1, &m_requireFunction, js_require);
812 }
813
uninstallImportFunctions()814 void ScriptEngine::uninstallImportFunctions()
815 {
816 globalObject().setProperty(loadFileString(), QScriptValue());
817 globalObject().setProperty(loadExtensionString(), QScriptValue());
818 globalObject().setProperty(requireString(), QScriptValue());
819 }
820
PropertyCacheKey(QString moduleName,QString propertyName,PropertyMapConstPtr propertyMap)821 ScriptEngine::PropertyCacheKey::PropertyCacheKey(QString moduleName,
822 QString propertyName, PropertyMapConstPtr propertyMap)
823 : m_moduleName(std::move(moduleName))
824 , m_propertyName(std::move(propertyName))
825 , m_propertyMap(std::move(propertyMap))
826 {
827 }
828
829 } // namespace Internal
830 } // namespace qbs
831