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"