File indexing completed on 2024-04-28 15:53:09

0001 /*
0002     SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@googlemail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "correctionfilegenerator.h"
0008 
0009 #include <QAction>
0010 #include <QTemporaryFile>
0011 
0012 #include <language/backgroundparser/backgroundparser.h>
0013 #include <language/duchain/duchainlock.h>
0014 #include <language/duchain/duchainutils.h>
0015 #include <language/interfaces/codecontext.h>
0016 #include <interfaces/icore.h>
0017 #include <interfaces/idocumentcontroller.h>
0018 #include <interfaces/ilanguagecontroller.h>
0019 #include <interfaces/iprojectcontroller.h>
0020 #include <parser/parsesession.h>
0021 
0022 #include <KTextEditor/Document>
0023 
0024 #include "duchain/helpers.h"
0025 #include "parser/codehelpers.h"
0026 
0027 #include <QDebug>
0028 #include <QStack>
0029 #include "codegendebug.h"
0030 
0031 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0032 #define SkipEmptyParts Qt::SkipEmptyParts
0033 #else
0034 #define SkipEmptyParts QString::SkipEmptyParts
0035 #endif
0036 
0037 using namespace KDevelop;
0038 
0039 namespace Python {
0040 
0041 TypeCorrection::TypeCorrection()
0042     : m_ui(new Ui::CorrectionWidget)
0043 {
0044 }
0045 
0046 TypeCorrection &TypeCorrection::self()
0047 {
0048     static TypeCorrection instance;
0049     return instance;
0050 }
0051 
0052 void TypeCorrection::doContextMenu(ContextMenuExtension &extension, Context *context)
0053 {
0054     if ( DeclarationContext* declContext = dynamic_cast<DeclarationContext*>(context) ) {
0055         qRegisterMetaType<KDevelop::IndexedDeclaration>("KDevelop::IndexedDeclaration");
0056 
0057         DUChainReadLocker lock;
0058 
0059         KDevelop::Declaration* declaration = declContext->declaration().data();
0060 
0061         if ( declaration && (declaration->kind() == Declaration::Instance
0062                              || (declaration->kind() == Declaration::Type
0063                                  && declaration->abstractType()->whichType() == AbstractType::TypeFunction)) ) {
0064             QAction* action = new QAction(i18n("Specify type for \"%1\"...", declaration->qualifiedIdentifier().toString()), nullptr);
0065             action->setData(QVariant::fromValue(IndexedDeclaration(declaration)));
0066             action->setIcon(QIcon::fromTheme("code-class"));
0067             connect(action, &QAction::triggered, this, &TypeCorrection::executeSpecifyTypeAction);
0068 
0069             extension.addAction(ContextMenuExtension::ExtensionGroup, action);
0070         }
0071     }
0072 }
0073 
0074 void TypeCorrection::executeSpecifyTypeAction()
0075 {
0076     QAction* action = qobject_cast<QAction*>(sender());
0077     if ( ! action ) {
0078         qCWarning(KDEV_PYTHON_CODEGEN) << "slot not invoked by triggering a QAction, should not happen"; // :)
0079         return;
0080     }
0081 
0082     DUChainReadLocker lock;
0083     IndexedDeclaration decl = action->data().value<IndexedDeclaration>();
0084     if ( ! decl.isValid() ) {
0085         decl = Helper::declarationUnderCursor();
0086     }
0087 
0088     if ( ! decl.isValid() ) {
0089         qCWarning(KDEV_PYTHON_CODEGEN) << "No declaration found!";
0090         return;
0091     }
0092 
0093     CorrectionFileGenerator::HintType hintType;
0094     if ( decl.data()->isFunctionDeclaration() ) {
0095         hintType = CorrectionFileGenerator::FunctionReturnHint;
0096     }
0097     else if ( decl.data()->kind() == Declaration::Instance ) {
0098         hintType = CorrectionFileGenerator::LocalVariableHint;
0099     }
0100     else {
0101         qCWarning(KDEV_PYTHON_CODEGEN) << "Correction requested for something that's not a local variable or function.";
0102         return;
0103     }
0104 
0105     CorrectionAssistant *dialog = new CorrectionAssistant(decl, hintType);
0106     dialog->setAttribute(Qt::WA_DeleteOnClose);
0107     dialog->setWindowTitle("Specify type for " + decl.data()->identifier().toString());
0108     connect(dialog, &QDialog::accepted, this, &TypeCorrection::accepted);
0109 
0110     m_ui->setupUi(dialog);
0111     connect(m_ui->buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept);
0112     connect(m_ui->buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
0113     if ( hintType == CorrectionFileGenerator::FunctionReturnHint ) {
0114         m_ui->kindLabel->setText(i18n("Function return type"));
0115     }
0116     else if ( hintType == CorrectionFileGenerator::LocalVariableHint ) {
0117         m_ui->kindLabel->setText(i18n("Local variable"));
0118     }
0119 
0120     m_ui->identifierLabel->setText(decl.data()->qualifiedIdentifier().toString());
0121     m_ui->typeText->setFocus();
0122     dialog->resize(560, 180);
0123     lock.unlock();
0124 
0125     dialog->show();
0126 }
0127 
0128 void TypeCorrection::accepted()
0129 {
0130     CorrectionAssistant *dialog = qobject_cast<CorrectionAssistant*>(sender());
0131     Q_ASSERT(dialog);
0132     if ( ! dialog ) {
0133         qCWarning(KDEV_PYTHON_CODEGEN) << "accepted() called without a sender";
0134         return;
0135     }
0136 
0137     DUChainReadLocker lock;
0138     IndexedDeclaration decl;
0139 
0140     decl = dialog->declaration();
0141 
0142     if ( ! decl.isValid() ) {
0143         decl = Helper::declarationUnderCursor();
0144     }
0145 
0146     if ( ! decl.isValid() ) {
0147         qCWarning(KDEV_PYTHON_CODEGEN) << "No declaration found!";
0148         return;
0149     }
0150 
0151     auto correctionFile = Helper::getLocalCorrectionFile(decl.data()->topContext()->url().toUrl());
0152     if ( correctionFile.isEmpty() ) {
0153         KMessageBox::error(nullptr, i18n("Sorry, cannot create hints for files which are not part of a project."));
0154         return;
0155     }
0156     CorrectionFileGenerator generator(correctionFile.path());
0157 
0158     CorrectionFileGenerator::HintType hintType = dialog->hintType();
0159 
0160     generator.addHint(m_ui->typeText->text(), m_ui->importsText->text().split(',', SkipEmptyParts), decl.data(), hintType);
0161 
0162     qCDebug(KDEV_PYTHON_CODEGEN) << "Forcing a reparse on " << decl.data()->topContext()->url();
0163     ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(decl.data()->topContext()->url()),
0164                                                                          TopDUContext::ForceUpdate);
0165     ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(correctionFile),
0166                                                                          TopDUContext::ForceUpdate);
0167 }
0168 
0169 CorrectionFileGenerator::CorrectionFileGenerator(const QString &filePath)
0170     : m_file(filePath)
0171     , m_filePath(filePath)
0172 {
0173     Q_ASSERT(! filePath.isEmpty());
0174     qCDebug(KDEV_PYTHON_CODEGEN) << "Correction file path: " << filePath;
0175 
0176     QFileInfo info(m_file);
0177     if ( ! info.absoluteDir().exists() ) {
0178         qCDebug(KDEV_PYTHON_CODEGEN) << "Directory does not exist. Creating...";
0179         info.absoluteDir().mkpath(info.absolutePath());
0180     }
0181 
0182     m_file.open(QFile::ReadWrite);
0183 
0184     m_code = QString(m_file.readAll()).split('\n');
0185     m_oldContents = m_code;
0186     m_fileIndents.reset(new FileIndentInformation(m_code));
0187 }
0188 
0189 void CorrectionFileGenerator::addHint(const QString &typeCode, const QStringList &modules, Declaration *forDeclaration,
0190                                       CorrectionFileGenerator::HintType hintType)
0191 {
0192     if ( ! forDeclaration || ! forDeclaration->context() ) {
0193         qCWarning(KDEV_PYTHON_CODEGEN) << "Declaration does not have context!" << (forDeclaration ? forDeclaration->toString() : "");
0194         return;
0195     }
0196 
0197     DUContext* context = forDeclaration->context();
0198     if ( context->type() == DUContext::Function ) {
0199         auto otherImporters = context->importers();
0200         if ( otherImporters.isEmpty() ) {
0201             return;
0202         }
0203         context = otherImporters.first();
0204     }
0205 
0206     // We're in a class if the context of the declaration is a Class or if its
0207     // parent context is a class. This is because a function body has a context
0208     // of type Other.
0209     bool inClass = context->type() == DUContext::Class
0210             || (context->parentContext() && context->parentContext()->type() == DUContext::Class);
0211 
0212     // If the declaration is part of the function's arguments or it's parent
0213     // context is one of a function.
0214     bool inFunction = context->type() == DUContext::Function
0215             || (context->owner() && context->owner()->abstractType()->whichType() == AbstractType::TypeFunction);
0216 
0217     qCDebug(KDEV_PYTHON_CODEGEN) << "Are we in a class: " << inClass;
0218     qCDebug(KDEV_PYTHON_CODEGEN) << "Are we in a function: " << inFunction;
0219 
0220     QString enclosingClassIdentifier, enclosingFunctionIdentifier;
0221 
0222     if ( context->owner() ) {
0223         if ( inClass && inFunction ) {
0224             Declaration *functionDeclaration = context->owner();
0225 
0226             enclosingClassIdentifier = functionDeclaration->context()->owner()->identifier().identifier().str();
0227             enclosingFunctionIdentifier = functionDeclaration->identifier().identifier().str();
0228         }
0229         else if ( inClass ) {
0230             enclosingClassIdentifier = context->owner()->identifier().identifier().str();
0231         }
0232         else if ( inFunction ) {
0233             enclosingFunctionIdentifier = context->owner()->identifier().identifier().str();
0234         }
0235     }
0236 
0237     qCDebug(KDEV_PYTHON_CODEGEN) << "Enclosing class: " << enclosingClassIdentifier;
0238     qCDebug(KDEV_PYTHON_CODEGEN) << "Enclosing function: " << enclosingFunctionIdentifier;
0239 
0240     QString declarationIdentifier = forDeclaration->identifier().identifier().str();
0241 
0242     bool foundClassDeclaration = false;
0243     bool foundFunctionDeclaration = false;
0244 
0245     QString functionIdentifier;
0246 
0247     if ( hintType == FunctionReturnHint ) {
0248         functionIdentifier = declarationIdentifier;
0249     }
0250     else if ( hintType == LocalVariableHint ) {
0251         functionIdentifier = enclosingFunctionIdentifier;
0252     }
0253 
0254     int line = findStructureFor(enclosingClassIdentifier, functionIdentifier);
0255 
0256     if ( line == -1 ) {
0257         line = findStructureFor(enclosingClassIdentifier, QString());
0258     }
0259     else if ( inFunction || hintType == FunctionReturnHint ) {
0260         foundFunctionDeclaration = true;
0261     }
0262 
0263     if ( line == -1 ) {
0264         line = findStructureFor(QString(), QString());
0265     }
0266     else if ( inClass ) {
0267         foundClassDeclaration = true;
0268     }
0269 
0270     qCDebug(KDEV_PYTHON_CODEGEN) << "Found class declaration: " << foundClassDeclaration << enclosingClassIdentifier;
0271     qCDebug(KDEV_PYTHON_CODEGEN) << "Found function declaration: " << foundFunctionDeclaration << functionIdentifier;
0272     qCDebug(KDEV_PYTHON_CODEGEN) << "Line: " << line;
0273 
0274     int indentsForNextStatement = m_fileIndents->indentForLine(line);
0275 
0276     if ( foundClassDeclaration ) {
0277         indentsForNextStatement += DEFAULT_INDENT_LEVEL;
0278     }
0279 
0280     QStringList newCode;
0281     if ( inClass ) {
0282         if ( ! foundClassDeclaration ) {
0283             QString classDeclaration = createStructurePart(enclosingClassIdentifier, ClassType);
0284             classDeclaration.prepend(QString(indentsForNextStatement, ' '));
0285 
0286             newCode.append(classDeclaration);
0287             indentsForNextStatement += DEFAULT_INDENT_LEVEL;
0288         }
0289         else {
0290             line++;
0291         }
0292     }
0293 
0294     if ( inFunction || hintType == FunctionReturnHint ) {
0295         if ( ! foundFunctionDeclaration ) {
0296             QString functionDeclaration;
0297             if ( inClass ) {
0298                 functionDeclaration = createStructurePart(functionIdentifier, MemberFunctionType);
0299             }
0300             else {
0301                 functionDeclaration = createStructurePart(functionIdentifier, FunctionType);
0302             }
0303             functionDeclaration.prepend(QString(indentsForNextStatement, ' '));
0304 
0305             newCode.append(functionDeclaration);
0306             indentsForNextStatement += DEFAULT_INDENT_LEVEL;
0307         }
0308         else {
0309             line++;
0310         }
0311     }
0312 
0313     if ( foundFunctionDeclaration && ! foundClassDeclaration ) {
0314         indentsForNextStatement += DEFAULT_INDENT_LEVEL;
0315     }
0316     QString hintCode;
0317     if ( hintType == FunctionReturnHint ) {
0318         hintCode = "returns = " + typeCode;
0319     }
0320     else if ( hintType == LocalVariableHint ) {
0321         hintCode = "l_" + declarationIdentifier + " = " + typeCode;
0322     }
0323     qCDebug(KDEV_PYTHON_CODEGEN) << "Hint code: " << hintCode;
0324     hintCode.prepend(QString(indentsForNextStatement, ' '));
0325     newCode.append(hintCode);
0326 
0327     for ( int i = 0; i < newCode.length(); i++ ) {
0328         m_code.insert(line + i, newCode.at(i));
0329     }
0330 
0331     // We safely insert any import declaration at the top
0332     foreach ( const QString &moduleName, modules ) {
0333         bool importExists = false;
0334         foreach (const QString &line, m_code) {
0335             if ( ! line.startsWith("import") && ! line.startsWith("from") && ! line.isEmpty() ) {
0336                 break;
0337             }
0338 
0339             // In both import ... and from ... import ..., the second part is what we want
0340             if ( line.section(' ', 1, 1, QString::SectionSkipEmpty) == moduleName.trimmed() ) {
0341                 importExists = true;
0342             }
0343         }
0344 
0345         if ( ! importExists ) {
0346             m_code.prepend("import " + moduleName.trimmed());
0347         }
0348     }
0349 
0350     QTemporaryFile temp;
0351     if ( checkForValidSyntax() && temp.open() ) {
0352         qCDebug(KDEV_PYTHON_CODEGEN) << "File path: " << m_file.fileName();
0353         qCDebug(KDEV_PYTHON_CODEGEN) << "Temporary file path: " << temp.fileName();
0354         QTextStream stream(&temp);
0355         stream << m_code.join("\n");
0356         m_fileIndents.reset(new FileIndentInformation(m_code));
0357         stream.flush();
0358 
0359         bool success = m_file.remove();
0360         success = success ? QFile::rename(temp.fileName(), m_file.fileName()) : false;
0361 
0362         if ( success && m_file.open(QFile::ReadWrite) ) {
0363             qCDebug(KDEV_PYTHON_CODEGEN) << "Successfully saved correction file.";
0364 
0365             m_oldContents = m_code;
0366         }
0367         else {
0368             m_code = m_oldContents;
0369         }
0370     }
0371     else {
0372         qCDebug(KDEV_PYTHON_CODEGEN) << "Something went wrong, reverting changes to correction file";
0373         m_code = m_oldContents;
0374     }
0375 }
0376 
0377 class StructureFindVisitor : public Python::AstDefaultVisitor
0378 {
0379 public:
0380     StructureFindVisitor(const QString &klass, const QString &function)
0381         : m_line(-1)
0382     {
0383         if ( ! klass.isNull() ) {
0384             m_searchedDeclaration.push(klass);
0385         }
0386 
0387         if ( ! function.isNull() ) {
0388             m_searchedDeclaration.push(function);
0389         }
0390     }
0391 
0392     void visitFunctionDefinition(FunctionDefinitionAst* node) override
0393     {
0394         m_declaration.push(node->name->value);
0395 
0396         if ( m_declaration == m_searchedDeclaration ) {
0397             m_line = node->startLine;
0398         }
0399 
0400         AstDefaultVisitor::visitFunctionDefinition(node);
0401 
0402         m_declaration.pop();
0403     }
0404 
0405     void visitClassDefinition(ClassDefinitionAst* node) override
0406     {
0407         m_declaration.push(node->name->value);
0408 
0409         if ( m_declaration == m_searchedDeclaration ) {
0410             m_line = node->startLine;
0411         }
0412 
0413         AstDefaultVisitor::visitClassDefinition(node);
0414 
0415         m_declaration.pop();
0416     }
0417 
0418     int line() const
0419     {
0420         return m_line;
0421     }
0422 
0423 private:
0424     QStack<QString> m_searchedDeclaration;
0425     QStack<QString> m_declaration;
0426     int m_line;
0427 };
0428 
0429 int CorrectionFileGenerator::findStructureFor(const QString &klass, const QString &function)
0430 {
0431     // If we're not looking for a specific thing, return the end of the file
0432     if ( klass.isNull() && function.isNull() ) {
0433         return m_code.length() - 1;
0434     }
0435 
0436     ParseSession parseSession;
0437     parseSession.setContents(m_code.join("\n"));
0438     parseSession.setCurrentDocument(IndexedString(m_filePath));
0439 
0440     QPair<CodeAst::Ptr, bool> parsed = parseSession.parse();
0441 
0442     QString classIdentifier = ( ! klass.isNull() ) ? "class_" + klass : QString();
0443     QString functionIdentifier = ( ! function.isNull() ) ? "function_" + function : QString();
0444 
0445     StructureFindVisitor visitor(classIdentifier, functionIdentifier);
0446     visitor.visitCode(parsed.first.data());
0447 
0448     return visitor.line();
0449 }
0450 
0451 QString CorrectionFileGenerator::createStructurePart(const QString &identifierSuffix, CorrectionFileGenerator::StructureType type)
0452 {
0453     QString code;
0454     QString params;
0455 
0456     switch ( type ) {
0457     case ClassType:
0458         code = "class class_" + identifierSuffix + ":";
0459         break;
0460     case MemberFunctionType:
0461         params = "self";
0462         // Fall through
0463     case FunctionType:
0464         code = "def function_" + identifierSuffix + "(" + params + "):";
0465         break;
0466     }
0467 
0468     return code;
0469 }
0470 
0471 bool CorrectionFileGenerator::checkForValidSyntax()
0472 {
0473     ParseSession parseSession;
0474     parseSession.setContents(m_code.join("\n"));
0475     parseSession.setCurrentDocument(IndexedString(m_filePath));
0476 
0477     QPair<CodeAst::Ptr, bool> parsed = parseSession.parse();
0478 
0479     return parsed.second && parseSession.m_problems.isEmpty();
0480 }
0481 
0482 CorrectionAssistant::CorrectionAssistant(IndexedDeclaration declaration, CorrectionFileGenerator::HintType hintType,
0483                                          QWidget *parent)
0484     : QDialog(parent),
0485       m_declaration(declaration),
0486       m_hintType(hintType)
0487 {
0488 }
0489 
0490 IndexedDeclaration CorrectionAssistant::declaration() const
0491 {
0492     return m_declaration;
0493 }
0494 
0495 CorrectionFileGenerator::HintType CorrectionAssistant::hintType() const
0496 {
0497     return m_hintType;
0498 }
0499 
0500 }
0501 
0502 #include "moc_correctionfilegenerator.cpp"