/* Copyright 2008 David Nolden This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "missingincludeitem.h" #include #include #include #include #include #include #include #include #include #include #include "../cppduchain/navigation/navigationwidget.h" #include "../cppduchain/typeutils.h" #include "../cppduchain/templateparameterdeclaration.h" #include "../cppduchain/expressionevaluationresult.h" #include "../cpputils.h" #include "model.h" #include "helpers.h" #include "sourcemanipulation.h" //Whether relative urls like "../bla" should be allowed. Even if this is false, they will be preferred over global urls. bool allowDotDot = true; const uint maxDeclarationCount = 30; using namespace KTextEditor; using namespace KDevelop; namespace Cpp { int sharedPathLevel(const QString& a, const QString& b) { int levelsShared = -1; for(int i = 0, c = qMin(a.length(), b.length()); i < c; ++i) { const QChar aC = a.at(i); const QChar bC = b.at(i); if (aC != bC) { break; } else if (aC == QDir::separator()) { ++levelsShared; } } return levelsShared; } /** * Try to find a proper include position from the DUChain: * * first look at existing imports (i.e. #include's) and find a fitting * file with the same/similar path to the new include file and use that * * otherwise fallback and use the first valid code line before @p maxLine */ int findIncludeLineFromDUChain(KTextEditor::Document* document, int maxLine, const QString& includeFile) { DUChainReadLocker lock; TopDUContext* top = DUChainUtils::standardContextForUrl(document->url()); if (!top) { return -1; } int line = -1; // first approach: look at existing #include statements and re-use them int currentMatchQuality = -1; foreach(const DUContext::Import& import, top->importedParentContexts()) { if (import.position.line > maxLine) { continue; } const int matchQuality = sharedPathLevel(import.context(top)->url().str(), includeFile); if (matchQuality >= currentMatchQuality) { line = import.position.line + 1; currentMatchQuality = matchQuality; } } if (line == -1) { // Makes sure the line is not in a comment, moving it behind if needed. // Just does very simple matching, should be ok for header copyright-notices and such. Cpp::SourceCodeInsertion insertion(top); int firstValid = insertion.firstValidCodeLineBefore(maxLine); if(firstValid <= maxLine) { line = firstValid; } } return line; } ///Decide whether the file is allowed to be included directly. If yes, this should return false. bool isBlacklistedInclude(const KUrl& url) { QString fileName = url.fileName(); if(isSource(fileName)) return true; //Do not allow including directly from the bits directory. Instead use one of the forwarding headers in other directories, when possible. if(url.upUrl().fileName() == "bits" && url.path().contains("/include/c++/")) { return true; } return false; } QualifiedIdentifier removeTemplateParameters(const QualifiedIdentifier& baseIdentifier) { QualifiedIdentifier identifier; for(int a = 0; a < baseIdentifier.count(); ++a) { Identifier part = baseIdentifier.at(a); part.clearTemplateIdentifiers(); identifier.push(part); } return identifier; } QList itemsForFile(const QString& displayTextPrefix, const QString& file, const Path::List& includePaths, const Path& currentPath, const IndexedDeclaration& decl, uint argumentHintDepth, QSet& directives) { QList ret; //We have found a potential declaration. Now find the shortest include path. QString shortestDirective; bool isRelativeToCurrentDir = false; if(isSource(file)) return ret; const Path canonicalFile(QFileInfo(file).canonicalFilePath()); foreach(const Path& includePath, includePaths) { QString relative = includePath.relativePath( canonicalFile ); if(relative.startsWith("./")) relative = relative.mid(2); if(shortestDirective.isEmpty() || (relative.length() < shortestDirective.length() && (allowDotDot || !relative.startsWith(".."))) || (shortestDirective.startsWith("..") && !relative.startsWith(".."))) { shortestDirective = relative; isRelativeToCurrentDir = includePath == currentPath; } } if(!shortestDirective.isEmpty()) { if(isRelativeToCurrentDir) shortestDirective = "\"" + shortestDirective + "\""; else shortestDirective = "<" + shortestDirective + ">"; if(!directives.contains(shortestDirective)) ret << KDevelop::CompletionTreeItemPointer(new MissingIncludeCompletionItem(shortestDirective, file, displayTextPrefix, decl, (int)argumentHintDepth)); directives.insert(shortestDirective); } return ret; } struct DirectiveShorterThan { bool operator()(const KDevelop::CompletionTreeItemPointer& lhs, const KDevelop::CompletionTreeItemPointer& rhs) { const MissingIncludeCompletionItem* l = dynamic_cast(lhs.data()); const MissingIncludeCompletionItem* r = dynamic_cast(rhs.data()); if(l && r) return l->m_addedInclude.length() < r->m_addedInclude.length(); return false; } }; QStringList candidateIncludeFiles(Declaration* decl) { QStringList ret; bool inBlacklistDir = isBlacklistedInclude(decl->url().toUrl()); foreach(KDevelop::ParsingEnvironmentFilePointer ptr, decl->topContext()->parsingEnvironmentFile()->importers()) { if(ptr->imports().count() == 1 || inBlacklistDir) { if(isBlacklistedInclude(ptr->url().toUrl())) continue; //This file is a forwader, add it to the list //Forwarders must be completely empty if(ptr->topContext()->localDeclarations().count()) continue; QString file(ptr->url().toUrl().toLocalFile()); ret << file; } } if(!inBlacklistDir) ret << decl->url().toUrl().toLocalFile(); return ret; } /** * Try to find include candidates based solely on the string of the unknown id @p id * * Example: We have 'QState' in our source file, it is unknown * This method then looks through the include paths used by @p source and returns all * files matching the file name 'QState' * * @note DUChain must be locked */ QStringList candidateIncludeFilesFromNameMatcher(const QList& includeItems, const QualifiedIdentifier& id) { QStringList result; for (const IncludeItem& item : includeItems) { // we never want to have directories in the result set if (item.isDirectory) continue; if (item.name == id.toString() && !isBlacklistedInclude(item.url())) { TopDUContext* top = DUChainUtils::standardContextForUrl(item.url()); // if this file was already parsed, and we don't find a declaration for id => discard if (top && top->findDeclarations(id).isEmpty()) { continue; } result << item.url().toLocalFile(); } } return result; } KSharedPtr includeDirectiveFromUrl(const KUrl& fromUrl, const IndexedDeclaration& decl) { KSharedPtr item; if(decl.data()) { QSet temp; QStringList candidateFiles = candidateIncludeFiles(decl.data()); QList items; const Path fromPath(fromUrl); const auto &includePaths = CppUtils::findIncludePaths(fromPath.toLocalFile()); foreach(const QString& file, candidateFiles) items += itemsForFile(QString(), file, includePaths, fromPath, decl, 0, temp); qSort::iterator, DirectiveShorterThan>(items.begin(), items.end(), DirectiveShorterThan()); if(!items.isEmpty()) { item = KSharedPtr(dynamic_cast(items.begin()->data())); } } return item; } QList missingIncludeCompletionItems(const QString& expression, const QString& displayTextPrefix, const Cpp::ExpressionEvaluationResult& expressionResult, const KDevelop::DUContextPointer& context, int argumentHintDepth, bool needInstance) { DUChainReadLocker lock; if (!context) return {}; AbstractType::Ptr type = TypeUtils::targetType(expressionResult.type.abstractType(), context->topContext()); //Collect all visible "using namespace" imports QList imports = context->findDeclarations( globalImportIdentifier() ); QSet prefixes; prefixes.insert(QualifiedIdentifier()); foreach(Declaration* importDecl, imports) { NamespaceAliasDeclaration* aliasDecl = dynamic_cast(importDecl); if(aliasDecl) { prefixes.insert(aliasDecl->importIdentifier()); }else{ kDebug() << "Import is not based on NamespaceAliasDeclaration"; } } QualifiedIdentifier namespaceScope = context->scopeIdentifier(false); for(int a = 1; a <= namespaceScope.count(); ++a) prefixes << namespaceScope.left(a); //Also search within enclosing namespaces QList ret; QList blacklistRet; QualifiedIdentifier identifier; if(type) { DelayedType::Ptr delayed = type.cast(); if(delayed) //Remove all template parameters, because the symbol-table doesn't know about those identifier = removeTemplateParameters(delayed->identifier().identifier().identifier()); IdentifiedType* idType = dynamic_cast(type.unsafeData()); if(idType) { identifier = removeTemplateParameters(idType->qualifiedIdentifier()); } }else{ //expression probably contains a part that needs to be resolved if(expression.contains(".") || expression.contains("->")) { ///@todo Check if parts of the expression are unresolved, like in "unresolvedClass.callFunction" kDebug() << "doing nothing with expression" << expression; }else{ kDebug() << "looking up" << expression << "as qualified identifier"; identifier = removeTemplateParameters(QualifiedIdentifier(expression)); QList visibleDecls = context->findDeclarations(identifier); foreach(Declaration* decl, visibleDecls) { if(!decl->isForwardDeclaration()) return ret; //Do not search for fitting declarations if a valid one is visible right now from here } } } if(identifier.isEmpty()) { return ret; } KUrl currentUrl(context->topContext()->url().str()); const auto currentPath = Path(currentUrl).parent(); Cpp::EnvironmentFilePointer env(dynamic_cast(context->topContext()->parsingEnvironmentFile().data())); if(!env) return ret; Path::List includePaths; foreach(const IndexedString& path, env->includePaths()) includePaths << Path(path.toUrl()); includePaths.prepend(currentPath); QSet directives; QSet haveForwardDeclarationItems; ///Search the persistent symbol table foreach(QualifiedIdentifier prefix, prefixes) { prefix.setExplicitlyGlobal(false); const IndexedDeclaration* declarations; uint declarationCount; QualifiedIdentifier id = prefix + identifier; PersistentSymbolTable::self().declarations( id, declarationCount, declarations ); if(declarationCount > maxDeclarationCount) declarationCount = maxDeclarationCount; for(uint a = 0; a < declarationCount; ++a) { KDevelop::ParsingEnvironmentFilePointer env = DUChain::self()->environmentFileForDocument(declarations[a].indexedTopContext()); if(!env || !dynamic_cast(env.data())) continue; Declaration* decl = declarations[a].declaration(); if(!decl) continue; if(dynamic_cast(decl)) continue; if(!isSource(context->url().str())) { if(decl && (decl->context()->type() == DUContext::Namespace || decl->context()->type() == DUContext::Global) && !needInstance && (decl->type() || decl->type()) ) { if(!haveForwardDeclarationItems.contains(decl->id())) ret += KDevelop::CompletionTreeItemPointer( new ForwardDeclarationItem(DeclarationPointer(decl)) ); haveForwardDeclarationItems.insert(decl->id()); } } if(decl && !decl->isForwardDeclaration()) { if(context->topContext()->imports(decl->topContext(), CursorInRevision::invalid())) continue; QString file(decl->url().toUrl().toLocalFile()); bool inBlacklistDir = isBlacklistedInclude(decl->url().toUrl()); auto candidateFiles = candidateIncludeFiles(decl); kDebug() << "candidates from DUChain:" << candidateFiles; for (const QString& file : candidateFiles) { ret += itemsForFile(displayTextPrefix, file, includePaths, currentPath, decl, argumentHintDepth, directives); } if (inBlacklistDir) { blacklistRet += itemsForFile(displayTextPrefix, file, includePaths, currentPath, decl, argumentHintDepth, directives); } } } } lock.unlock(); // NOTE: this will acquire the foreground lock and thus we must not hold the duchain lock here const QList includeItems = CppUtils::allFilesInIncludePath(currentUrl.toLocalFile(), false, QString()); lock.lock(); if (!context) return ret; auto candidateFiles = candidateIncludeFilesFromNameMatcher(includeItems, identifier); kDebug() << "candidates from name matching:" << candidateFiles; for (const QString& file : candidateFiles) { ret += itemsForFile(displayTextPrefix, file, includePaths, currentPath, IndexedDeclaration(), argumentHintDepth, directives); } if(ret.isEmpty()) ret += blacklistRet; { //If there is non-relative include directives, remove the relative ones QList relativeIncludes; QList nonRelativeIncludes; for(QList::iterator it = ret.begin(); it != ret.end(); ) { MissingIncludeCompletionItem* currentItem = dynamic_cast(it->data()); if(currentItem) { if(currentItem->m_addedInclude.contains("\"../") || currentItem->m_addedInclude.contains("<../")) relativeIncludes << *it; else nonRelativeIncludes << *it; } ++it; } if(!nonRelativeIncludes.isEmpty()) { foreach(KDevelop::CompletionTreeItemPointer relative, relativeIncludes) ret.removeAll(relative); } } qSort::iterator, DirectiveShorterThan>(ret.begin(), ret.end(), DirectiveShorterThan()); return ret; } MissingIncludeCompletionItem::MissingIncludeCompletionItem(const QString& addedInclude, const QString& canonicalFile, const QString& displayTextPrefix, const IndexedDeclaration& decl, int argumentHintDepth) : m_argumentHintDepth(argumentHintDepth) , m_addedInclude(addedInclude) , m_canonicalPath(canonicalFile) , m_displayTextPrefix(displayTextPrefix) , m_decl(decl) { } #define RETURN_CACHED_ICON(name) {static QIcon icon(KIcon(name).pixmap(QSize(16, 16))); return icon;} QVariant MissingIncludeCompletionItem::data(const QModelIndex& index, int role, const KDevelop::CodeCompletionModel* model) const { DUChainReadLocker lock(DUChain::lock(), 500); if(!lock.locked()) { kDebug(9007) << "Failed to lock the du-chain in time"; return QVariant(); } if(role == Qt::DecorationRole) if(index.column() == KTextEditor::CodeCompletionModel::Icon) RETURN_CACHED_ICON("CTparents") switch (role) { case KTextEditor::CodeCompletionModel::IsExpandable: return QVariant(true); case KTextEditor::CodeCompletionModel::ExpandingWidget: { if(!m_decl.data()) return QVariant(); Cpp::NavigationWidget* nav = new Cpp::NavigationWidget(DeclarationPointer(m_decl.data()), TopDUContextPointer(m_decl.data()->topContext())); model->addNavigationWidget(this, nav); QVariant v; v.setValue((QWidget*)nav); return v; } case Qt::DisplayRole: switch (index.column()) { case KTextEditor::CodeCompletionModel::Prefix: return i18n("Add include directive"); case KTextEditor::CodeCompletionModel::Name: { const QString suffix = "#include " + m_addedInclude; // note: m_displayTextPrefix already contains the spaces on the right if(!m_decl.data()) return i18nc("file content unknown", "%1, %2", m_displayTextPrefix, suffix); else if(m_decl.data()->kind() == Declaration::Namespace) return QString("%1namespace %2, %3").arg(m_displayTextPrefix, m_decl.data()->identifier().toString(), suffix); else { return QString("%1%2, %3").arg(m_displayTextPrefix, m_decl.data()->toString()).arg(suffix); } } } break; case KTextEditor::CodeCompletionModel::ItemSelected: { if(!m_decl.data()) return QVariant(); return QVariant( Cpp::NavigationWidget::shortDescription(m_decl.data()) ); } } return QVariant(); } QString MissingIncludeCompletionItem::lineToInsert() const { return "#include " + m_addedInclude; } void MissingIncludeCompletionItem::execute(KTextEditor::Document* document, const KTextEditor::Range& word) { // first try to find a proper include position from the DUChain int line = findIncludeLineFromDUChain(document, word.start().line(), m_canonicalPath); // in case we couldn't find a duchain for this document, fallback to simple // "parsing" of the file and look for other #include statements we could // use as a position to add our line to // TODO: never add inside comments! if (line == -1) { int lastLineWithInclude = -1; int checkLines = word.start().line() -1; // make sure we don't add an include in a conditional #if or #ifdef block int rppConditionalLevel = 0; for(int a = 0; a < checkLines; ++a) { QString lineText = document->line(a).trimmed(); if(lineText.startsWith("#if")) { rppConditionalLevel++; } else if (rppConditionalLevel > 0 && lineText.startsWith("#endif")) { rppConditionalLevel--; } else if(rppConditionalLevel == 0 && lineText.startsWith("#include")) { QString ending = lineText; if(!ending.isEmpty()) ending = ending.left( ending.length()-1 ).trimmed(); //Remove the last > or " if(!ending.endsWith(".moc")) lastLineWithInclude = a; } } if (lastLineWithInclude != -1) { line = lastLineWithInclude; } } // otherwise we just add ad the beginning for the document // TODO: skip first comment block, if it exists document->insertLine(line, lineToInsert()); #ifndef TEST_COMPLETION CodeCompletionModel::self()->startCompletionAfterParsing(IndexedString(document->url())); #endif } int MissingIncludeCompletionItem::inheritanceDepth() const { return 0; } ForwardDeclarationItem::ForwardDeclarationItem(KDevelop::DeclarationPointer decl) : NormalDeclarationCompletionItem(decl) { } QVariant ForwardDeclarationItem::data(const QModelIndex& index, int role, const KDevelop::CodeCompletionModel* model) const { if(role == Qt::DisplayRole && index.column() == KTextEditor::CodeCompletionModel::Prefix) return i18n("Add Forward-Declaration"); if(role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { RETURN_CACHED_ICON("dialog-ok"); ///@todo Better icon for the create-forward declaration action } QVariant ret = NormalDeclarationCompletionItem::data(index, role, model); if(role == Qt::DisplayRole && index.column() == KTextEditor::CodeCompletionModel::Name) { //Add some text behind the item, so we get some more info in minimal completion mode DUChainReadLocker lock(DUChain::lock()); if(m_declaration) return QString(m_declaration->qualifiedIdentifier().toString() + ": " + i18n("forward-declare")); return ret; // return m_displayTextPrefix + ": " + i18n("Add Forward-Declaration"); } return ret; } void ForwardDeclarationItem::execute(KTextEditor::Document* document, const KTextEditor::Range& word) { DUChainReadLocker lock(DUChain::lock()); if(m_declaration) { TopDUContext* top = DUChainUtils::standardContextForUrl(document->url()); if(!top) return; Cpp::SourceCodeInsertion insertion(top); insertion.setInsertBefore(KDevelop::SimpleCursor(word.start())); insertion.insertForwardDeclaration(m_declaration.data()); lock.unlock(); insertion.changes().setReplacementPolicy(DocumentChangeSet::WarnOnFailedChange); if(!insertion.changes().applyAllChanges()) return; } } }