File indexing completed on 2024-05-05 16:42:28

0001 /*
0002     SPDX-FileCopyrightText: 2011-2013 Sven Brauch <svenbrauch@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "helpers.h"
0008 
0009 #include <QList>
0010 #include <QProcess>
0011 #include <QSettings>
0012 #include <QStandardPaths>
0013 
0014 #include <QDebug>
0015 #include "duchaindebug.h"
0016 
0017 #include <language/duchain/types/unsuretype.h>
0018 #include <language/duchain/types/integraltype.h>
0019 #include <language/duchain/types/containertypes.h>
0020 #include <language/duchain/types/functiontype.h>
0021 #include <language/duchain/duchainutils.h>
0022 #include <language/duchain/duchainlock.h>
0023 #include <language/duchain/duchain.h>
0024 #include <language/duchain/classdeclaration.h>
0025 #include <language/duchain/aliasdeclaration.h>
0026 #include <language/duchain/types/typeutils.h>
0027 #include <language/backgroundparser/backgroundparser.h>
0028 #include <interfaces/iproject.h>
0029 #include <interfaces/iprojectcontroller.h>
0030 #include <interfaces/icore.h>
0031 #include <interfaces/ilanguagecontroller.h>
0032 #include <interfaces/idocumentcontroller.h>
0033 #include <util/path.h>
0034 
0035 #include <KTextEditor/View>
0036 #include <KConfigGroup>
0037 
0038 #include "ast.h"
0039 #include "types/hintedtype.h"
0040 #include "types/unsuretype.h"
0041 #include "types/indexedcontainer.h"
0042 #include "kdevpythonversion.h"
0043 #include "expressionvisitor.h"
0044 
0045 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0046 #define SkipEmptyParts Qt::SkipEmptyParts
0047 #else
0048 #define SkipEmptyParts QString::SkipEmptyParts
0049 #endif
0050 
0051 using namespace KDevelop;
0052 
0053 namespace Python {
0054 
0055 QMap<IProject*, QVector<QUrl>> Helper::cachedCustomIncludes;
0056 QMap<IProject*, QVector<QUrl>> Helper::cachedSearchPaths;
0057 QVector<QUrl> Helper::projectSearchPaths;
0058 QStringList Helper::dataDirs;
0059 IndexedString Helper::documentationFile;
0060 DUChainPointer<TopDUContext> Helper::documentationFileContext = DUChainPointer<TopDUContext>(nullptr);
0061 QStringList Helper::correctionFileDirs;
0062 QString Helper::localCorrectionFileDir;
0063 QMutex Helper::cacheMutex;
0064 QMutex Helper::projectPathLock;
0065 
0066 void Helper::scheduleDependency(const IndexedString& dependency, int betterThanPriority)
0067 {
0068     BackgroundParser* bgparser = KDevelop::ICore::self()->languageController()->backgroundParser();
0069     bool needsReschedule = true;
0070     if ( bgparser->isQueued(dependency) ) {
0071         const auto priority= bgparser->priorityForDocument(dependency);
0072         if ( priority > betterThanPriority - 1 ) {
0073             bgparser->removeDocument(dependency);
0074         }
0075         else {
0076             needsReschedule = false;
0077         }
0078     }
0079     if ( needsReschedule ) {
0080         bgparser->addDocument(dependency, TopDUContext::ForceUpdate, betterThanPriority - 1,
0081                               nullptr, ParseJob::FullSequentialProcessing);
0082     }
0083 }
0084 
0085 IndexedDeclaration Helper::declarationUnderCursor(bool allowUse)
0086 {
0087     KDevelop::IDocument* doc = ICore::self()->documentController()->activeDocument();
0088     const auto view = ICore::self()->documentController()->activeTextDocumentView();
0089     if ( doc && doc->textDocument() && view ) {
0090         DUChainReadLocker lock;
0091         const auto cursor = view->cursorPosition();
0092         if ( allowUse ) {
0093             return IndexedDeclaration(DUChainUtils::itemUnderCursor(doc->url(), cursor).declaration);
0094         }
0095         else {
0096             return DUChainUtils::declarationInLine(cursor, DUChainUtils::standardContextForUrl(doc->url()));
0097         }
0098     }
0099 
0100     return KDevelop::IndexedDeclaration();
0101 }
0102 
0103 Declaration* Helper::accessAttribute(const AbstractType::Ptr accessed,
0104                                      const IndexedIdentifier& attribute,
0105                                      const TopDUContext* topContext)
0106 {
0107     if ( ! accessed ) {
0108         return nullptr;
0109     }
0110     // if the type is unsure, search all the possibilities (but return the first match)
0111     auto structureTypes = Helper::filterType<StructureType>(accessed,
0112         [](AbstractType::Ptr toFilter) {
0113             auto type = Helper::resolveAliasType(toFilter);
0114             return type && type->whichType() == AbstractType::TypeStructure;
0115         },
0116         [](AbstractType::Ptr toMap) {
0117             return Helper::resolveAliasType(toMap).staticCast<StructureType>();
0118         }
0119     );
0120     auto docFileContext = Helper::getDocumentationFileContext();
0121 
0122     for ( const auto& type: structureTypes ) {
0123         auto searchContexts = Helper::internalContextsForClass(type, topContext);
0124         for ( const auto ctx: searchContexts ) {
0125             auto found = ctx->findDeclarations(attribute, CursorInRevision::invalid(),
0126                                                topContext, DUContext::DontSearchInParent);
0127             if ( !found.isEmpty() && (
0128                    found.last()->topContext() != docFileContext ||
0129                    ctx->topContext() == docFileContext) ) {
0130                 // never consider decls from the builtins
0131                 return found.last();
0132             }
0133         }
0134     }
0135     return nullptr;
0136 }
0137 
0138 AbstractType::Ptr Helper::resolveAliasType(const AbstractType::Ptr eventualAlias)
0139 {
0140     return TypeUtils::resolveAliasType(eventualAlias);
0141 }
0142 
0143 AbstractType::Ptr Helper::extractTypeHints(AbstractType::Ptr type)
0144 {
0145     return Helper::foldTypes(Helper::filterType<AbstractType>(type, [](AbstractType::Ptr t) -> bool {
0146         auto hint = t.dynamicCast<HintedType>();
0147         return !hint || hint->isValid();
0148     }));
0149 }
0150 
0151 Helper::FuncInfo Helper::functionForCalled(Declaration* called, bool isAlias)
0152 {
0153     if ( ! called ) {
0154         return { nullptr, false };
0155     }
0156     else if ( called->isFunctionDeclaration() ) {
0157         return { static_cast<FunctionDeclaration*>( called ), false };
0158     }
0159     // If we're calling a type object (isAlias == true), look for a constructor.
0160     static const IndexedIdentifier initId(KDevelop::Identifier("__init__"));
0161 
0162     // Otherwise look for a `__call__()` method.
0163     static const IndexedIdentifier callId(KDevelop::Identifier("__call__"));
0164 
0165     auto attr = accessAttribute(called->abstractType(), (isAlias ? initId : callId), called->topContext());
0166     return { dynamic_cast<FunctionDeclaration*>(attr), isAlias };
0167 }
0168 
0169 Declaration* Helper::declarationForName(const QString& name, const CursorInRevision& location,
0170                                         KDevelop::DUChainPointer<const DUContext> context)
0171 {
0172     DUChainReadLocker lock(DUChain::lock());
0173     auto identifier = KDevelop::Identifier(name);
0174     auto localDeclarations = context->findLocalDeclarations(identifier, location, nullptr,
0175                                                             AbstractType::Ptr(), DUContext::DontResolveAliases);
0176     if ( !localDeclarations.isEmpty() ) {
0177         return localDeclarations.last();
0178     }
0179 
0180     QList<Declaration*> declarations;
0181     const DUContext* currentContext = context.data();
0182     bool findInNext = true, findBeyondUse = false;
0183     do {
0184         if (findInNext) {
0185             CursorInRevision findUntil = findBeyondUse ? currentContext->topContext()->range().end : location;
0186             declarations = currentContext->findDeclarations(identifier, findUntil);
0187 
0188             for (Declaration* declaration: declarations) {
0189                 if (declaration->context()->type() != DUContext::Class ||
0190                     (currentContext->type() == DUContext::Function && declaration->context() == currentContext->parentContext())) {
0191                      // Declarations from class decls must be referenced through `self.<foo>`, except
0192                      //  in their local scope (handled above) or when used as default arguments for methods of the same class.
0193                      // Otherwise, we're done!
0194                     return declaration;
0195                 }
0196             }
0197             if (!declarations.isEmpty()) {
0198                 // If we found declarations but rejected all of them (i.e. didn't return), we need to keep searching.
0199                 findInNext = true;
0200                 declarations.clear();
0201             }
0202         }
0203 
0204         if (!findBeyondUse && currentContext->owner() && currentContext->owner()->isFunctionDeclaration()) {
0205             // Names in the body may be defined after the function definition, before the function is called.
0206             // Note: only the parameter list has type DUContext::Function, so we have to do this instead.
0207             findBeyondUse = findInNext = true;
0208         }
0209     } while ((currentContext = currentContext->parentContext()));
0210 
0211     return nullptr;
0212 }
0213 
0214 
0215 Declaration* Helper::declarationForName(const Python::NameAst* name, CursorInRevision location,
0216                                         KDevelop::DUChainPointer<const DUContext> context)
0217 {
0218     const Ast* checkNode = name;
0219     while ((checkNode = checkNode->parent)) {
0220         switch (checkNode->astType) {
0221           default:
0222             continue;
0223           case Ast::ListComprehensionAstType:
0224           case Ast::SetComprehensionAstType:
0225           case Ast::DictionaryComprehensionAstType:
0226           case Ast::GeneratorExpressionAstType:
0227             // Variables in comprehensions are used before their definition. `[foo for foo in bar]`
0228             auto cmpEnd = CursorInRevision(checkNode->endLine, checkNode->endCol);
0229             if (cmpEnd > location) {
0230                 location = cmpEnd;
0231             }
0232         }
0233     }
0234     return declarationForName(name->identifier->value, location, context);
0235 }
0236 
0237 QVector<DUContext*> Helper::internalContextsForClass(const StructureType::Ptr classType,
0238                         const TopDUContext* context, ContextSearchFlags flags, int depth)
0239 {
0240     QVector<DUContext*> searchContexts;
0241     if ( ! classType ) {
0242         return searchContexts;
0243     }
0244     if ( auto c = classType->internalContext(context) ) {
0245         searchContexts << c;
0246     }
0247     auto decl = Helper::resolveAliasDeclaration(classType->declaration(context));
0248     if ( auto classDecl = dynamic_cast<ClassDeclaration*>(decl) ) {
0249         FOREACH_FUNCTION ( const auto& base, classDecl->baseClasses ) {
0250             if ( flags == PublicOnly && base.access == KDevelop::Declaration::Private ) {
0251                 continue;
0252             }
0253             auto baseClassType = base.baseClass.type<StructureType>();
0254             // recursive call, because the base class will have more base classes eventually
0255             if ( depth < 10 ) {
0256                 searchContexts.append(Helper::internalContextsForClass(baseClassType, context, flags, depth + 1));
0257             }
0258         }
0259     }
0260     return searchContexts;
0261 }
0262 
0263 Declaration* Helper::resolveAliasDeclaration(Declaration* decl)
0264 {
0265     AliasDeclaration* alias = dynamic_cast<AliasDeclaration*>(decl);
0266     if ( alias ) {
0267         DUChainReadLocker lock;
0268         return alias->aliasedDeclaration().data();
0269     }
0270     else
0271         return decl;
0272 }
0273 
0274 QStringList Helper::getDataDirs() {
0275     if ( Helper::dataDirs.isEmpty() ) {
0276         Helper::dataDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "kdevpythonsupport/documentation_files", QStandardPaths::LocateDirectory);
0277     }
0278     return Helper::dataDirs;
0279 }
0280 
0281 KDevelop::IndexedString Helper::getDocumentationFile()
0282 {
0283     if ( Helper::documentationFile.isEmpty() ) {
0284         auto path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kdevpythonsupport/documentation_files/builtindocumentation.py");
0285         Helper::documentationFile = IndexedString(path);
0286     }
0287     return Helper::documentationFile;
0288 }
0289 
0290 ReferencedTopDUContext Helper::getDocumentationFileContext()
0291 {
0292     if ( Helper::documentationFileContext ) {
0293         return ReferencedTopDUContext(Helper::documentationFileContext.data());
0294     }
0295     else {
0296         DUChainReadLocker lock;
0297         auto file = Helper::getDocumentationFile();
0298         ReferencedTopDUContext ctx = ReferencedTopDUContext(DUChain::self()->chainForDocument(file));
0299         Helper::documentationFileContext = DUChainPointer<TopDUContext>(ctx.data());
0300         return ctx;
0301     }
0302 }
0303 
0304 // stolen from KUrl. duh.
0305 static QString _relativePath(const QString &base_dir, const QString &path)
0306 {
0307    QString _base_dir(QDir::cleanPath(base_dir));
0308    QString _path(QDir::cleanPath(path.isEmpty() || QDir::isRelativePath(path) ? _base_dir+QLatin1Char('/')+path : path));
0309 
0310    if (_base_dir.isEmpty())
0311       return _path;
0312 
0313    if (_base_dir[_base_dir.length()-1] != QLatin1Char('/'))
0314       _base_dir.append(QLatin1Char('/') );
0315 
0316    const QStringList list1 = _base_dir.split(QLatin1Char('/'), SkipEmptyParts);
0317    const QStringList list2 = _path.split(QLatin1Char('/'), SkipEmptyParts);
0318 
0319    // Find where they meet
0320    int level = 0;
0321    int maxLevel = qMin(list1.count(), list2.count());
0322    while((level < maxLevel) && (list1[level] == list2[level])) level++;
0323 
0324    QString result;
0325    // Need to go down out of the first path to the common branch.
0326    for(int i = level; i < list1.count(); i++)
0327       result.append(QLatin1String("../"));
0328 
0329    // Now up up from the common branch to the second path.
0330    for(int i = level; i < list2.count(); i++)
0331       result.append(list2[i]).append(QLatin1Char('/'));
0332 
0333    if ((level < list2.count()) && (path[path.length()-1] != QLatin1Char('/')))
0334       result.truncate(result.length()-1);
0335 
0336    return result;
0337 }
0338 
0339 QUrl Helper::getCorrectionFile(const QUrl& document)
0340 {
0341     if ( Helper::correctionFileDirs.isEmpty() ) {
0342         Helper::correctionFileDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
0343                                                                "kdevpythonsupport/correction_files/",
0344                                                                QStandardPaths::LocateDirectory);
0345     }
0346 
0347     foreach (QString correctionFileDir, correctionFileDirs) {
0348         foreach ( const QUrl& basePath, Helper::getSearchPaths(QUrl()) ) {
0349             if ( ! basePath.isParentOf(document) ) {
0350                 continue;
0351             }
0352             auto base = basePath.path();
0353             auto doc = document.path();
0354             auto relative = _relativePath(base, doc);
0355             auto fullPath = correctionFileDir + "/" + relative;
0356             if ( QFile::exists(fullPath) ) {
0357                 return QUrl::fromLocalFile(fullPath).adjusted(QUrl::NormalizePathSegments);
0358             }
0359         }
0360     }
0361     return {};
0362 }
0363 
0364 QUrl Helper::getLocalCorrectionFile(const QUrl& document)
0365 {
0366     if ( Helper::localCorrectionFileDir.isNull() ) {
0367         Helper::localCorrectionFileDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "kdevpythonsupport/correction_files/";
0368     }
0369 
0370     auto absolutePath = QUrl();
0371     foreach ( const auto& basePath, Helper::getSearchPaths(QUrl()) ) {
0372         if ( ! basePath.isParentOf(document) ) {
0373             continue;
0374         }
0375         auto path = QDir(basePath.path()).relativeFilePath(document.path());
0376         absolutePath = QUrl::fromLocalFile(Helper::localCorrectionFileDir + path);
0377         break;
0378     }
0379     return absolutePath;
0380 }
0381 
0382 QString Helper::getPythonExecutablePath(IProject* project)
0383 {
0384     if ( project ) {
0385         auto interpreter = project->projectConfiguration()->group("pythonsupport").readEntry("interpreter");
0386         if ( !interpreter.isEmpty() ) {
0387             // we have a user-configured interpreter, try using it
0388             QFile f(interpreter);
0389             if ( f.exists() ) {
0390                 return interpreter;
0391             }
0392             qCWarning(KDEV_PYTHON_DUCHAIN) << "Custom python interpreter" << interpreter << "configured for project" << project->name() << "is invalid, using default";
0393         }
0394     }
0395 
0396     // Find python 3 (https://www.python.org/dev/peps/pep-0394/)
0397     auto result = QStandardPaths::findExecutable("python" PYTHON_VERSION_STR);
0398     if ( ! result.isEmpty() ) {
0399         return result;
0400     }
0401     result = QStandardPaths::findExecutable("python" PYTHON_VERSION_MAJOR_STR);
0402     if ( ! result.isEmpty() ) {
0403         return result;
0404     }
0405     result = QStandardPaths::findExecutable("python");
0406     if ( ! result.isEmpty() ) {
0407         return result;
0408     }
0409 
0410 #ifdef Q_OS_WIN
0411     QStringList extraPaths;
0412     // Check for default CPython installation path, because the
0413     // installer does not add the path to $PATH.
0414     QStringList keys = {
0415         "HKEY_LOCAL_MACHINE\\Software\\Python\\PythonCore\\PYTHON_VERSION\\InstallPath",
0416         "HKEY_LOCAL_MACHINE\\Software\\Python\\PythonCore\\PYTHON_VERSION-32\\InstallPath",
0417         "HKEY_CURRENT_USER\\Software\\Python\\PythonCore\\PYTHON_VERSION\\InstallPath",
0418         "HKEY_CURRENT_USER\\Software\\Python\\PythonCore\\PYTHON_VERSION-32\\InstallPath"
0419     };
0420     auto version = QString(PYTHON_VERSION_STR);
0421     foreach ( QString key, keys ) {
0422         key.replace("PYTHON_VERSION", version);
0423         QSettings base(key.left(key.indexOf("Python")), QSettings::NativeFormat);
0424         if ( ! base.childGroups().contains("Python") ) {
0425             continue;
0426         }
0427         QSettings keySettings(key, QSettings::NativeFormat);
0428         auto path = keySettings.value("Default").toString();
0429         if ( ! path.isEmpty() ) {
0430             extraPaths << path;
0431             break;
0432         }
0433     }
0434     result = QStandardPaths::findExecutable("python", extraPaths);
0435     if ( ! result.isEmpty() ) {
0436         return result;
0437     }
0438 #endif
0439     // fallback
0440     return PYTHON_EXECUTABLE;
0441 }
0442 
0443 QVector<QUrl> Helper::getSearchPaths(const QUrl& workingOnDocument)
0444 {
0445     QMutexLocker lock(&Helper::cacheMutex);
0446     QVector<QUrl> searchPaths;
0447     // search in the projects, as they're packages and likely to be installed or added to PYTHONPATH later
0448     // and also add custom include paths that are defined in the projects
0449 
0450     auto project = ICore::self()->projectController()->findProjectForUrl(workingOnDocument);
0451     {
0452         QMutexLocker lock(&Helper::projectPathLock);
0453         searchPaths << Helper::projectSearchPaths;
0454         searchPaths << Helper::cachedCustomIncludes.value(project);
0455     }
0456     
0457     foreach ( const QString& path, getDataDirs() ) {
0458         searchPaths.append(QUrl::fromLocalFile(path));
0459     }
0460 
0461     if ( !cachedSearchPaths.contains(project) ) {
0462         QVector<QUrl> cachedForProject;
0463         qCDebug(KDEV_PYTHON_DUCHAIN) << "*** Collecting search paths...";
0464         QStringList getpath;
0465         getpath << "-c" << "import sys; sys.stdout.write('$|$'.join(sys.path))";
0466         
0467         QProcess python;
0468         python.start(getPythonExecutablePath(project), getpath);
0469         python.waitForFinished(1000);
0470         QString pythonpath = QString::fromUtf8(python.readAllStandardOutput());
0471 
0472         if ( ! pythonpath.isEmpty() ) {
0473             const auto paths = pythonpath.split("$|$", SkipEmptyParts);
0474             foreach ( const QString& path, paths ) {
0475                 cachedForProject.append(QUrl::fromLocalFile(path));
0476             }
0477         }
0478         else {
0479             qCWarning(KDEV_PYTHON_DUCHAIN) << "Could not get search paths! Defaulting to stupid stuff.";
0480             searchPaths.append(QUrl::fromLocalFile("/usr/lib/python" PYTHON_VERSION_STR));
0481             searchPaths.append(QUrl::fromLocalFile("/usr/lib/python" PYTHON_VERSION_STR "/site-packages"));
0482             QString path = qgetenv("PYTHONPATH");
0483             QStringList paths = path.split(':');
0484             foreach ( const QString& path, paths ) {
0485                 cachedForProject.append(QUrl::fromLocalFile(path));
0486             }
0487         }
0488         qCDebug(KDEV_PYTHON_DUCHAIN) << " *** Done. Got search paths: " << cachedSearchPaths;
0489         cachedSearchPaths.insert(project, cachedForProject);
0490     }
0491     
0492     searchPaths.append(cachedSearchPaths.value(project));
0493     
0494     auto dir = workingOnDocument.adjusted(QUrl::RemoveFilename);
0495     if ( ! dir.isEmpty() ) {
0496         // search in the current packages
0497         searchPaths.append(dir);
0498     }
0499     
0500     return searchPaths;
0501 }
0502 
0503 bool Helper::isUsefulType(AbstractType::Ptr type)
0504 {
0505     return TypeUtils::isUsefulType(type);
0506 }
0507 
0508 AbstractType::Ptr Helper::contentOfIterable(const AbstractType::Ptr iterable, const TopDUContext* topContext)
0509 {
0510     auto types = filterType<StructureType>(iterable,
0511         [](AbstractType::Ptr t) { return t->whichType() == AbstractType::TypeStructure; } );
0512 
0513     static const IndexedIdentifier iterId(KDevelop::Identifier("__iter__"));
0514     static const IndexedIdentifier nextId(KDevelop::Identifier("__next__"));
0515     AbstractType::Ptr content(new IntegralType(IntegralType::TypeMixed));
0516 
0517     for ( const auto& type: types ) {
0518         if ( auto map = type.dynamicCast<MapType>() ) {
0519             // Iterating over dicts gets keys, not values
0520             content = mergeTypes(content, map->keyType().abstractType());
0521             continue;
0522         }
0523         else if ( auto list = type.dynamicCast<ListType>() ) {
0524             content = mergeTypes(content, list->contentType().abstractType());
0525             continue;
0526         }
0527         else if ( auto indexed = type.dynamicCast<IndexedContainer>() ) {
0528             content = mergeTypes(content, indexed->asUnsureType());
0529             continue;
0530         }
0531         DUChainReadLocker lock;
0532         // Content of an iterable object is iterable.__iter__().__next__().
0533         if ( auto iterFunc = dynamic_cast<FunctionDeclaration*>(accessAttribute(type, iterId, topContext)) ) {
0534             if ( auto iterator = iterFunc->type<FunctionType>()->returnType().dynamicCast<StructureType>() ) {
0535                 if ( auto nextFunc = dynamic_cast<FunctionDeclaration*>(accessAttribute(iterator, nextId, topContext)) ) {
0536                     content = mergeTypes(content, nextFunc->type<FunctionType>()->returnType());
0537                 }
0538             }
0539         }
0540     }
0541     return content;
0542 }
0543 
0544 AbstractType::Ptr Helper::mergeTypes(AbstractType::Ptr type, const AbstractType::Ptr newType)
0545 {
0546     UnsureType::Ptr ret;
0547     return TypeUtils::mergeTypes<Python::UnsureType>(type, newType);
0548 }
0549 
0550 }