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