1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
|
/*
SPDX-FileCopyrightText: 2014 Denis Steckelmacher <steckdenis@yahoo.fr>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "nodejs.h"
#include "../helper.h"
#include "../parsesession.h"
#include <language/duchain/duchain.h>
#include <language/duchain/topducontext.h>
#include <language/duchain/declaration.h>
#include <language/duchain/types/structuretype.h>
#include <language/duchain/types/integraltype.h>
#include <util/path.h>
#include <QFile>
#include <QStandardPaths>
using namespace KDevelop;
namespace QmlJS {
NodeJS::NodeJS()
{
}
NodeJS& NodeJS::instance()
{
static NodeJS* i = nullptr;
if (!i) {
i = new NodeJS();
}
return *i;
}
void NodeJS::initialize(DeclarationBuilder* builder)
{
QMutexLocker lock(&m_mutex);
// Create "module", a structure that may contain "exports" if the module
// refers to module.exports
createObject(QStringLiteral("module"), 1, builder);
// Create "exports", that can also contain the exported symbols of the module
createObject(QStringLiteral("exports"), 2, builder);
}
void NodeJS::createObject(const QString& name, int index, DeclarationBuilder* builder)
{
Identifier identifier(name);
StructureType::Ptr type(new StructureType);
auto* decl = builder->openDeclaration<Declaration>(identifier, RangeInRevision());
type->setDeclaration(decl);
decl->setAlwaysForceDirect(true);
decl->setKind(Declaration::Type); // Not exactly what the user would expect, but this ensures that QmlJS::getInternalContext does not recurse infinitely
decl->setInternalContext(builder->openContext(
(QmlJS::AST::Node*)nullptr + index, // Index is used to disambiguate the contexts. "node" is never dereferenced and is only stored in a hash table
RangeInRevision(),
DUContext::Class,
QualifiedIdentifier(identifier)
));
builder->closeContext();
builder->openType(type);
builder->closeAndAssignType();
}
DeclarationPointer NodeJS::moduleExports(const QString& moduleName, const IndexedString& url)
{
QString urlStr = url.str();
QString fileName = moduleFileName(moduleName, urlStr);
DeclarationPointer exports;
if (fileName.isEmpty() || urlStr.contains(QLatin1String("__builtin_ecmascript.js")) || urlStr == fileName) {
return exports;
}
ReferencedTopDUContext topContext = ParseSession::contextOfFile(fileName, url, 0);
DUChainReadLocker lock;
if (topContext) {
static const QualifiedIdentifier idModule(QStringLiteral("module"));
static const QualifiedIdentifier idExports(QStringLiteral("exports"));
// Try "module.exports". If this declaration exists, it contains the
// module's exports
exports = getDeclaration(idModule, topContext.data());
if (exports && exports->internalContext()) {
exports = getDeclaration(idExports, exports->internalContext(), false);
}
// Try "exports", that always exist, has a structure type, and contains
// the exported symbols
if (!exports) {
exports = getDeclaration(idExports, topContext.data());
}
}
return exports;
}
DeclarationPointer NodeJS::moduleMember(const QString& moduleName,
const QString& memberName,
const IndexedString& url)
{
DeclarationPointer module = moduleExports(moduleName, url);
DeclarationPointer member;
if (module) {
member = QmlJS::getDeclaration(
QualifiedIdentifier(memberName),
QmlJS::getInternalContext(module),
false
);
}
return member;
}
Path::List NodeJS::moduleDirectories(const QString& url)
{
Path::List paths;
// QML/JS ships several modules that exist only in binary form in Node.js
const QStringList& dirs = QStandardPaths::locateAll(
QStandardPaths::GenericDataLocation,
QStringLiteral("kdevqmljssupport/nodejsmodules"),
QStandardPaths::LocateDirectory
);
paths.reserve(dirs.size());
for (auto& dir : dirs) {
paths.append(Path(dir));
}
// url/../node_modules, then url/../../node_modules, etc
Path path(url);
path.addPath(QStringLiteral(".."));
const int maxPathSize = path.isLocalFile() ? 1 : 2;
while (path.segments().size() > maxPathSize) {
paths.append(path.cd(QStringLiteral("node_modules")));
path.addPath(QStringLiteral(".."));
}
return paths;
}
QString NodeJS::moduleFileName(const QString& moduleName, const QString& url)
{
QMutexLocker lock(&m_mutex);
auto pair = qMakePair(moduleName, url);
const auto cachedModuleFileNameIt = m_cachedModuleFileNames.constFind(pair);
if (cachedModuleFileNameIt != m_cachedModuleFileNames.constEnd()) {
return *cachedModuleFileNameIt;
}
QString& fileName = m_cachedModuleFileNames[pair];
// Absolute and relative URLs
if (moduleName.startsWith(QLatin1Char('/')) || moduleName.startsWith(QLatin1Char('.'))) {
// NOTE: This is not portable to Windows, but the Node.js documentation
// only talks about module names that start with /, ./ and ../ .
fileName = fileOrDirectoryPath(Path(url).cd(QStringLiteral("..")).cd(moduleName).toLocalFile());
return fileName;
}
// Try all the paths that might contain modules
const auto& paths = moduleDirectories(url);
for (auto& path : paths) {
fileName = fileOrDirectoryPath(path.cd(moduleName).toLocalFile());
if (!fileName.isEmpty()) {
break;
}
}
return fileName;
}
QString NodeJS::fileOrDirectoryPath(const QString& baseName)
{
if (QFile::exists(baseName)) {
return baseName;
} else if (QFile::exists(baseName + QLatin1String(".js"))) {
return baseName + QLatin1String(".js");
} else if (QFile::exists(baseName + QLatin1String("/index.js"))) {
// TODO: package.json files currently not supported
return baseName + QLatin1String("/index.js");
}
return QString();
}
}
|