File indexing completed on 2024-05-05 16:41:32

0001 /*
0002     SPDX-FileCopyrightText: 2011-2012 Sven Brauch <svenbrauch@googlemail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 // Note to confused people reading this code: This is not the parser.
0008 // It's just a minimalist helper class for code completion. The parser is in the parser/ directory.
0009 
0010 #include "helpers.h"
0011 
0012 #include <language/duchain/abstractfunctiondeclaration.h>
0013 #include <language/duchain/duchainutils.h>
0014 #include <language/duchain/ducontext.h>
0015 #include <language/duchain/declaration.h>
0016 #include <language/duchain/types/functiontype.h>
0017 #include <language/duchain/types/integraltype.h>
0018 #include <language/codecompletion/normaldeclarationcompletionitem.h>
0019 
0020 #include <QStringList>
0021 #include <QTextFormat>
0022 
0023 #include <QDebug>
0024 #include "codecompletiondebug.h"
0025 
0026 #include "duchain/declarations/functiondeclaration.h"
0027 #include "parser/codehelpers.h"
0028 
0029 using namespace KDevelop;
0030 
0031 namespace Python {
0032 
0033 QString camelCaseToUnderscore(const QString& camelCase)
0034 {
0035     QString underscore;
0036     for ( int i = 0; i < camelCase.size(); i++ ) {
0037         const QChar& c = camelCase.at(i);
0038         if ( c.isUpper() && i != 0 ) {
0039             underscore.append('_');
0040         }
0041         underscore.append(c.toLower());
0042     }
0043     return underscore;
0044 }
0045 
0046 int identifierMatchQuality(const QString& identifier1_, const QString& identifier2_)
0047 {
0048     QString identifier1 = camelCaseToUnderscore(identifier1_).toLower().replace('.', '_');
0049     QString identifier2 = camelCaseToUnderscore(identifier2_).toLower().replace('.', '_');
0050 
0051     if ( identifier1 == identifier2 ) {
0052         return 3;
0053     }
0054     if ( identifier1.contains(identifier2) || identifier2.contains(identifier1) ) {
0055         return 2;
0056     }
0057     QStringList parts1 = identifier1.split('_');
0058     QStringList parts2 = identifier2.split('_');
0059     parts1.removeAll("");
0060     parts2.removeAll("");
0061     parts1.removeDuplicates();
0062     parts2.removeDuplicates();
0063     if ( parts1.length() > 5 || parts2.length() > 5 ) {
0064         // don't waste time comparing huge identifiers,
0065         // the matching is probably pointless anyways for people using
0066         // more than 5 words for their variable names
0067         return 0;
0068     }
0069     foreach ( const QString& part1, parts1 ) {
0070         foreach ( const QString& part2, parts2 ) {
0071             // Don't take very short name parts into account,
0072             // those are not very descriptive eventually
0073             if ( part1.size() < 3 || part2.size() < 3 ) {
0074                 continue;
0075             }
0076             if ( part1 == part2 ) {
0077                 // partial match
0078                 return 1;
0079             }
0080         }
0081     }
0082     return 0;
0083 }
0084 
0085 typedef QPair<QString, ExpressionParser::Status> keyword;
0086 
0087 static QList<keyword> supportedKeywords;
0088 static QList<keyword> controlChars;
0089 static QList<QString> miscKeywords;
0090 static QList<QString> noCompletionKeywords;
0091 static QMutex keywordPopulationLock;
0092 
0093 // Keywords known to me:
0094 // and       del       for       is        raise    
0095 // assert    elif      from      lambda    return   
0096 // break     else      global    not       try      
0097 // class     except    if        or        while    
0098 // continue  exec      import    pass      yield    
0099 // def       finally   in        print     with
0100 // async     await
0101 
0102 ExpressionParser::ExpressionParser(QString code)
0103     : m_code(code)
0104     , m_cursorPositionInString(m_code.length())
0105 {
0106     keywordPopulationLock.lock();
0107     if ( supportedKeywords.isEmpty() ) {
0108         noCompletionKeywords << "break" << "class" << "continue" << "pass" << "try"
0109                              << "else" << "as" << "finally" << "global" << "lambda";
0110         miscKeywords << "and" << "assert" << "del" << "elif" << "exec" << "if" << "is" << "not" 
0111                      << "or" << "print" << "return" << "while" << "yield" << "with" << "await";
0112         supportedKeywords << keyword("import", ExpressionParser::ImportFound);
0113         supportedKeywords << keyword("from", ExpressionParser::FromFound);
0114         supportedKeywords << keyword("raise", ExpressionParser::RaiseFound);
0115         supportedKeywords << keyword("in", ExpressionParser::InFound);
0116         supportedKeywords << keyword("for", ExpressionParser::ForFound);
0117         supportedKeywords << keyword("class", ExpressionParser::ClassFound);
0118         supportedKeywords << keyword("def", ExpressionParser::DefFound);
0119         supportedKeywords << keyword("except", ExpressionParser::ExceptFound);
0120         controlChars << keyword(":", ExpressionParser::ColonFound);
0121         controlChars << keyword(",", ExpressionParser::CommaFound);
0122         controlChars << keyword("(", ExpressionParser::InitializerFound);
0123         controlChars << keyword("{", ExpressionParser::InitializerFound);
0124         controlChars << keyword("[", ExpressionParser::InitializerFound);
0125         controlChars << keyword(".", ExpressionParser::MemberAccessFound);
0126         controlChars << keyword("=", ExpressionParser::EqualsFound);
0127     }
0128     keywordPopulationLock.unlock();
0129 }
0130 
0131 QString ExpressionParser::getRemainingCode()
0132 {
0133     return m_code.mid(0, m_cursorPositionInString);
0134 }
0135 
0136 QString ExpressionParser::getScannedCode()
0137 {
0138     return m_code.mid(m_cursorPositionInString, m_code.length() - m_cursorPositionInString);
0139 }
0140 
0141 int ExpressionParser::trailingWhitespace()
0142 {
0143     int ws = 0;
0144     int index = m_cursorPositionInString - 1;
0145     while ( index >= 0 ) {
0146         if ( m_code.at(index).isSpace() ) {
0147             ws++;
0148             index --;
0149         }
0150         else {
0151             break;
0152         }
0153     }
0154     return ws;
0155 }
0156 
0157 void ExpressionParser::reset()
0158 {
0159     m_cursorPositionInString = m_code.length();
0160 }
0161 
0162 QString ExpressionParser::skipUntilStatus(ExpressionParser::Status requestedStatus, bool* ok, int* expressionsSkipped)
0163 {
0164     if ( expressionsSkipped ) {
0165         *expressionsSkipped = 0;
0166     }
0167     QString lastExpression;
0168     Status currentStatus = InvalidStatus;
0169     while ( currentStatus != requestedStatus ) {
0170         lastExpression = popExpression(&currentStatus);
0171         qCDebug(KDEV_PYTHON_CODECOMPLETION) << lastExpression << currentStatus;
0172         if ( currentStatus == NothingFound ) {
0173             *ok = ( requestedStatus == NothingFound ); // ok exactly if the caller requested NothingFound as end status
0174             return QString();
0175         }
0176         if ( expressionsSkipped && currentStatus == ExpressionFound ) {
0177             *expressionsSkipped += 1;
0178         }
0179     }
0180     *ok = true;
0181     return lastExpression;
0182 }
0183 
0184 TokenList ExpressionParser::popAll()
0185 {
0186     Status currentStatus = InvalidStatus;
0187     TokenList items;
0188     while ( currentStatus != NothingFound ) {
0189         QString result = popExpression(&currentStatus);
0190         items << TokenListEntry(currentStatus, result, m_cursorPositionInString);
0191     }
0192     std::reverse(items.begin(), items.end());
0193     return items;
0194 }
0195 
0196 bool endsWithSeperatedKeyword(const QString& str, const QString& shouldEndWith) {
0197     bool endsWith = str.endsWith(shouldEndWith);
0198     if ( ! endsWith ) {
0199         return false;
0200     }
0201     int l = shouldEndWith.length();
0202     if ( str.length() == l ) {
0203         return true;
0204     }
0205     if ( str.right(l + 1).at(0).isSpace() ) {
0206         return true;
0207     }
0208     return false;
0209 }
0210 
0211 QString ExpressionParser::popExpression(ExpressionParser::Status* status)
0212 {
0213     const auto remaining = getRemainingCode();
0214     auto trimmed = remaining.trimmed();
0215     auto operatingOn = trimmed.replace('\t', ' ');
0216     bool lineIsEmpty = false;
0217     for ( auto it = remaining.constEnd()-1; it != remaining.constEnd(); it-- ) {
0218         if ( ! it->isSpace() ) {
0219             break;
0220         }
0221         if ( *it == '\n' ) {
0222             lineIsEmpty = true;
0223             break;
0224         }
0225     }
0226     if ( operatingOn.isEmpty() || lineIsEmpty ) {
0227         m_cursorPositionInString = 0;
0228         *status = NothingFound;
0229         return QString();
0230     }
0231     bool lastCharIsSpace = getRemainingCode().right(1).at(0).isSpace();
0232     m_cursorPositionInString -= trailingWhitespace();
0233     if ( operatingOn.endsWith('(') ) {
0234         qCDebug(KDEV_PYTHON_CODECOMPLETION) << "eventual call found";
0235         m_cursorPositionInString -= 1;
0236         *status = EventualCallFound;
0237         return QString();
0238     }
0239     foreach ( const keyword& kw, controlChars ) {
0240         if ( operatingOn.endsWith(kw.first) ) {
0241             m_cursorPositionInString -= kw.first.length();
0242             *status = kw.second;
0243             return QString();
0244         }
0245     }
0246     if ( lastCharIsSpace ) {
0247         foreach ( const keyword& kw, supportedKeywords ) {
0248             if ( endsWithSeperatedKeyword(operatingOn, kw.first) ) {
0249                 m_cursorPositionInString -= kw.first.length();
0250                 *status = kw.second;
0251                 return QString();
0252             }
0253         }
0254         foreach ( const QString& kw, miscKeywords ) {
0255             if ( endsWithSeperatedKeyword(operatingOn, kw) ) {
0256                 m_cursorPositionInString -= kw.length();
0257                 *status = MeaninglessKeywordFound;
0258                 return QString();
0259             }
0260         }
0261         foreach ( const QString& kw, noCompletionKeywords ) {
0262             if ( endsWithSeperatedKeyword(operatingOn, kw) ) {
0263                 m_cursorPositionInString -= kw.length();
0264                 *status = NoCompletionKeywordFound;
0265                 return QString();
0266             }
0267         }
0268     }
0269     // Otherwise, there's a real expression at the cursor, so scan it.
0270     QStringList lines = operatingOn.split('\n');
0271     Python::TrivialLazyLineFetcher f(lines);
0272     int lastLine = lines.length()-1;
0273     KTextEditor::Cursor startCursor;
0274     QString expr = CodeHelpers::expressionUnderCursor(f, KTextEditor::Cursor(lastLine, f.fetchLine(lastLine).length() - 1),
0275                                                       startCursor, true);
0276     if ( expr.isEmpty() ) {
0277         *status = NothingFound;
0278     }
0279     else {
0280         *status = ExpressionFound;
0281     }
0282     m_cursorPositionInString -= expr.length();
0283     return expr;
0284 }
0285 
0286 
0287 // This is stolen from PHP. For credits, see helpers.cpp in PHP.
0288 void createArgumentList(Declaration* dec_, QString& ret, QList< QVariant >* highlighting, int atArg, bool includeTypes)
0289 {
0290     auto dec = dynamic_cast<Python::FunctionDeclaration*>(dec_);
0291     if ( ! dec ) {
0292         return;
0293     }
0294     int textFormatStart = 0;
0295     QTextFormat normalFormat(QTextFormat::CharFormat);
0296     QTextFormat highlightFormat(QTextFormat::CharFormat);
0297     highlightFormat.setBackground(QColor::fromRgb(142, 186, 255));
0298     highlightFormat.setProperty(QTextFormat::FontWeight, 99);
0299     
0300     AbstractFunctionDeclaration* decl = dynamic_cast<AbstractFunctionDeclaration*>(dec);
0301     FunctionType::Ptr functionType = dec->type<FunctionType>();
0302     
0303     if (functionType && decl) {
0304 
0305         QVector<Declaration*> parameters;
0306         if (DUChainUtils::argumentContext(dec))
0307             parameters = DUChainUtils::argumentContext(dec)->localDeclarations();
0308 
0309         ret = '(';
0310         bool first = true;
0311         int num = 0;
0312         
0313         bool skipFirst = false;
0314         if ( dec->context() && dec->context()->type() == DUContext::Class && ! dec->isStatic() ) {
0315             // the function is a class method, and its first argument is "self". Don't display that.
0316             skipFirst = true;
0317         }
0318 
0319         uint defaultParamNum = 0;
0320         int firstDefaultParam = parameters.count() - decl->defaultParametersSize() - skipFirst;
0321 
0322         // disable highlighting when in default arguments, it doesn't make much sense then
0323         bool disableHighlighting = false;
0324         
0325         foreach(Declaration* dec, parameters) {
0326             if ( skipFirst ) {
0327                 skipFirst = false;
0328                 continue;
0329             }
0330             // that has nothing to do with the skip, it's just for the comma
0331             if (first)
0332                 first = false;
0333             else
0334                 ret += ", ";
0335 
0336             bool doHighlight = false;
0337             QTextFormat doFormat;
0338             
0339             if ( num == atArg - 1 )
0340                 doFormat = highlightFormat;
0341             else
0342                 doFormat = normalFormat;
0343             
0344             if ( num == firstDefaultParam ) {
0345                 ret += "[";
0346                 ++defaultParamNum;
0347                 disableHighlighting = true;
0348             }
0349             
0350             if ( ! disableHighlighting ) {
0351                 doHighlight = true;
0352             }
0353             
0354             if ( includeTypes ) {
0355                 if (num < functionType->arguments().count()) {
0356                     if (AbstractType::Ptr type = functionType->arguments().at(num)) {
0357                         if ( type->toString() != "<unknown>" ) {
0358                             ret += type->toString() + ' ';
0359                         }
0360                     }
0361                 }
0362                 
0363                 if (doHighlight) {
0364                     if (highlighting && ret.length() != textFormatStart) {
0365                         //Add a default-highlighting for the passed text
0366                         *highlighting << QVariant(textFormatStart);
0367                         *highlighting << QVariant(ret.length() - textFormatStart);
0368                         *highlighting << QVariant(normalFormat);
0369                         textFormatStart = ret.length();
0370                     }
0371                 }
0372             }
0373             
0374             
0375             ret += dec->identifier().toString();
0376 
0377             if (doHighlight) {
0378                 if (highlighting && ret.length() != textFormatStart) {
0379                     *highlighting << QVariant(textFormatStart + 1);
0380                     *highlighting << QVariant(ret.length() - textFormatStart - 1);
0381                     *highlighting << doFormat;
0382                     textFormatStart = ret.length();
0383                 }
0384             }
0385 
0386             ++num;
0387         }
0388         if ( defaultParamNum != 0 ) {
0389             ret += "]";
0390         }
0391         ret += ')';
0392 
0393         if (highlighting && ret.length() != textFormatStart) {
0394             *highlighting <<  QVariant(textFormatStart);
0395             *highlighting << QVariant(ret.length());
0396             *highlighting << normalFormat;
0397             textFormatStart = ret.length();
0398         }
0399         return;
0400     }
0401 }
0402 
0403 StringFormatter::StringFormatter(const QString &string)
0404     : m_string(string)
0405 {
0406     qCDebug(KDEV_PYTHON_CODECOMPLETION) << "String being parsed: " << string;
0407     QRegExp regex("\\{(\\w+)(?:!([rs]))?(?:\\:(.*))?\\}");
0408     regex.setMinimal(true);
0409     int pos = 0;
0410     while ( (pos = regex.indexIn(string, pos)) != -1 ) {
0411         QString identifier = regex.cap(1);
0412         QString conversionStr = regex.cap(2);
0413         QChar conversion = (conversionStr.isNull() || conversionStr.isEmpty()) ? QChar() : conversionStr.at(0);
0414         QString formatSpec = regex.cap(3);
0415 
0416         qCDebug(KDEV_PYTHON_CODECOMPLETION) << "variable: " << regex.cap(0);
0417 
0418         // The regex guarantees that conversion is only a single character
0419         ReplacementVariable variable(identifier, conversion, formatSpec);
0420         m_replacementVariables.append(variable);
0421 
0422         RangeInString variablePosition(pos, pos + regex.matchedLength());
0423         m_variablePositions.append(variablePosition);
0424 
0425         pos += regex.matchedLength();
0426     }
0427 }
0428 
0429 bool StringFormatter::isInsideReplacementVariable(int cursorPosition) const
0430 {
0431     return getReplacementVariable(cursorPosition) != nullptr;
0432 }
0433 
0434 const ReplacementVariable *StringFormatter::getReplacementVariable(int cursorPosition) const
0435 {
0436     int index = 0;
0437     foreach ( const RangeInString &variablePosition, m_variablePositions ) {
0438         if ( cursorPosition >= variablePosition.beginIndex && cursorPosition <= variablePosition.endIndex ) {
0439             return &m_replacementVariables.at(index);
0440         }
0441         index++;
0442     }
0443 
0444     return nullptr;
0445 }
0446 
0447 RangeInString StringFormatter::getVariablePosition(int cursorPosition) const
0448 {
0449     int index = 0;
0450     foreach ( const RangeInString &variablePosition, m_variablePositions ) {
0451         if ( cursorPosition >= variablePosition.beginIndex && cursorPosition <= variablePosition.endIndex ) {
0452             return m_variablePositions.at(index);
0453         }
0454         index++;
0455     }
0456     return RangeInString();
0457 }
0458 
0459 int StringFormatter::nextIdentifierId() const
0460 {
0461     int highestIdFound = -1;
0462     foreach ( const ReplacementVariable &variable, m_replacementVariables ) {
0463         bool isNumeric;
0464         int identifier = variable.identifier().toInt(&isNumeric);
0465         if ( isNumeric && identifier > highestIdFound ) {
0466             highestIdFound = identifier;
0467         }
0468     }
0469     return highestIdFound + 1;
0470 }
0471 
0472 }