File indexing completed on 2024-05-12 04:39:14

0001 /*
0002     SPDX-FileCopyrightText: 2014 Jørgen Kvalsvik <lycantrophe@lavabit.com>
0003     SPDX-FileCopyrightText: 2014 Kevin Funk <kfunk@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "unknowndeclarationproblem.h"
0009 
0010 #include "clanghelpers.h"
0011 #include "parsesession.h"
0012 #include "../util/clangdebug.h"
0013 #include "../util/clangutils.h"
0014 #include "../util/clangtypes.h"
0015 #include "../clangsettings/clangsettingsmanager.h"
0016 
0017 #include <interfaces/icore.h>
0018 #include <interfaces/iprojectcontroller.h>
0019 #include <interfaces/iproject.h>
0020 
0021 #include <language/duchain/persistentsymboltable.h>
0022 #include <language/duchain/aliasdeclaration.h>
0023 #include <language/duchain/classdeclaration.h>
0024 #include <language/duchain/parsingenvironment.h>
0025 #include <language/duchain/topducontext.h>
0026 #include <language/duchain/duchainlock.h>
0027 #include <language/duchain/duchainutils.h>
0028 #include <custom-definesandincludes/idefinesandincludesmanager.h>
0029 
0030 #include <project/projectmodel.h>
0031 #include <util/path.h>
0032 
0033 #include <KLocalizedString>
0034 
0035 #include <QDir>
0036 #include <QRegularExpression>
0037 
0038 #include <algorithm>
0039 
0040 using namespace KDevelop;
0041 
0042 namespace {
0043 /** Under some conditions, such as when looking up suggestions
0044  * for the undeclared namespace 'std' we will get an awful lot
0045  * of suggestions. This parameter limits how many suggestions
0046  * will pop up, as rarely more than a few will be relevant anyways
0047  *
0048  * Forward declaration suggestions are included in this number
0049  */
0050 const int maxSuggestions = 5;
0051 
0052 /**
0053  * We don't want anything from the bits directory -
0054  * we'd rather prefer forwarding includes, such as <vector>
0055  */
0056 bool isBlacklisted(const QString& path)
0057 {
0058     if (ClangHelpers::isSource(path))
0059         return true;
0060 
0061     // Do not allow including directly from the bits directory.
0062     // Instead use one of the forwarding headers in other directories, when possible.
0063     if (path.contains( QLatin1String("bits") ) && path.contains(QLatin1String("/include/c++/")))
0064         return true;
0065 
0066     return false;
0067 }
0068 
0069 QStringList scanIncludePaths( const QString& identifier, const QDir& dir, int maxDepth = 3 )
0070 {
0071     if (!maxDepth) {
0072         return {};
0073     }
0074 
0075     QStringList candidates;
0076     const auto path = dir.absolutePath();
0077 
0078     if( isBlacklisted( path ) ) {
0079         return {};
0080     }
0081 
0082     const QStringList nameFilters = {identifier, identifier + QLatin1String(".*")};
0083     const auto& files = dir.entryList(nameFilters, QDir::Files);
0084     for (const auto& file : files) {
0085         if (identifier.compare(file, Qt::CaseInsensitive) == 0 || ClangHelpers::isHeader(file)) {
0086             const QString filePath = path + QLatin1Char('/') + file;
0087             clangDebug() << "Found candidate file" << filePath;
0088             candidates.append( filePath );
0089         }
0090     }
0091 
0092     maxDepth--;
0093     const auto& subdirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
0094     for (const auto& subdir : subdirs) {
0095         candidates += scanIncludePaths( identifier, QDir{ path + QLatin1Char('/') + subdir }, maxDepth );
0096     }
0097 
0098     return candidates;
0099 }
0100 
0101 /**
0102  * Find files in dir that match the given identifier. Matches common C++ header file extensions only.
0103  */
0104 QStringList scanIncludePaths( const QualifiedIdentifier& identifier, const KDevelop::Path::List& includes )
0105 {
0106     const auto stripped_identifier = identifier.last().toString();
0107     QStringList candidates;
0108     for( const auto& include : includes ) {
0109         candidates += scanIncludePaths( stripped_identifier, QDir{ include.toLocalFile() } );
0110     }
0111 
0112     std::sort( candidates.begin(), candidates.end() );
0113     candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() );
0114     return candidates;
0115 }
0116 
0117 /**
0118  * Determine how much path is shared between two includes.
0119  *  boost/tr1/unordered_map
0120  *  boost/tr1/unordered_set
0121  * have a shared path of 2 where
0122  *  boost/tr1/unordered_map
0123  *  boost/vector
0124  * have a shared path of 1
0125  */
0126 int sharedPathLevel(const QString& a, const QString& b)
0127 {
0128     int shared = -1;
0129     const QChar dirSeparator = QDir::separator();
0130     for (auto x = a.begin(), y = b.begin(); x != a.end() && y != b.end() && *x == *y; ++x, ++y) {
0131         if (*x == dirSeparator) {
0132             ++shared;
0133         }
0134     }
0135 
0136     return shared;
0137 }
0138 
0139 /**
0140  * Try to find a proper include position from the DUChain:
0141  *
0142  * look at existing imports (i.e. #include's) and find a fitting
0143  * file with the same/similar path to the new include file and use that
0144  *
0145  * TODO: Implement a fallback scheme
0146  */
0147 KDevelop::DocumentRange includeDirectivePosition(const KDevelop::Path& source, const QString& includeFile)
0148 {
0149     static const QRegularExpression mocFilenameExpression(QStringLiteral("(moc_[^\\/\\\\]+\\.cpp$|\\.moc$)") );
0150 
0151     DUChainReadLocker lock;
0152     const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() );
0153     if( !top ) {
0154         clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range";
0155         return KDevelop::DocumentRange::invalid();
0156     }
0157 
0158     int line = -1;
0159 
0160     // look at existing #include statements and re-use them
0161     int currentMatchQuality = -1;
0162     const auto& importedParentContexts = top->importedParentContexts();
0163     for (const auto& import : importedParentContexts) {
0164 
0165         const auto importFilename = import.context(top)->url().str();
0166         const int matchQuality = sharedPathLevel( importFilename , includeFile );
0167         if( matchQuality < currentMatchQuality ) {
0168             continue;
0169         }
0170 
0171         const auto match = mocFilenameExpression.match(importFilename);
0172         if (match.hasMatch()) {
0173             clangDebug() << "moc file detected in" << source.toUrl().toDisplayString() << ":" << importFilename << "-- not using as include insertion location";
0174             continue;
0175         }
0176 
0177         line = import.position.line + 1;
0178         currentMatchQuality = matchQuality;
0179     }
0180 
0181     if( line == -1 ) {
0182         /* Insert at the top of the document */
0183         return {IndexedString(source.pathOrUrl()), {0, 0, 0, 0}};
0184     }
0185 
0186     return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}};
0187 }
0188 
0189 KDevelop::DocumentRange forwardDeclarationPosition(const QualifiedIdentifier& identifier, const KDevelop::Path& source)
0190 {
0191     DUChainReadLocker lock;
0192     const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() );
0193     if( !top ) {
0194         clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range";
0195         return KDevelop::DocumentRange::invalid();
0196     }
0197 
0198     if (!top->findDeclarations(identifier).isEmpty()) {
0199         // Already forward-declared
0200         return KDevelop::DocumentRange::invalid();
0201     }
0202 
0203     int line = std::numeric_limits< int >::max();
0204     const auto& localDeclarations = top->localDeclarations();
0205     for (const auto decl : localDeclarations) {
0206         line = std::min( line, decl->range().start.line );
0207     }
0208 
0209     if( line == std::numeric_limits< int >::max() ) {
0210         return KDevelop::DocumentRange::invalid();
0211     }
0212 
0213     // We want it one line above the first declaration
0214     line = std::max( line - 1, 0 );
0215 
0216     return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}};
0217 }
0218 
0219 /**
0220  * Iteratively build all levels of the current scope. A (missing) type anywhere
0221  * can be arbitrarily namespaced, so we create the permutations of possible
0222  * nestings of namespaces it can currently be in,
0223  *
0224  * TODO: add detection of namespace aliases, such as 'using namespace KDevelop;'
0225  *
0226  * namespace foo {
0227  *      namespace bar {
0228  *          function baz() {
0229  *              type var;
0230  *          }
0231  *      }
0232  * }
0233  *
0234  * Would give:
0235  * foo::bar::baz::type
0236  * foo::bar::type
0237  * foo::type
0238  * type
0239  */
0240 QVector<KDevelop::QualifiedIdentifier> findPossibleQualifiedIdentifiers( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::CursorInRevision& cursor )
0241 {
0242     DUChainReadLocker lock;
0243     const TopDUContext* top = DUChainUtils::standardContextForUrl( file.toUrl() );
0244 
0245     if( !top ) {
0246         clangDebug() << "unable to find standard context for" << file.toLocalFile() << "Not creating duchain candidates";
0247         return {};
0248     }
0249 
0250     const auto* context = top->findContextAt( cursor );
0251     if( !context ) {
0252         clangDebug() << "No context found at" << cursor;
0253         return {};
0254     }
0255 
0256     QVector<KDevelop::QualifiedIdentifier> declarations{ identifier };
0257     auto scopes = context->scopeIdentifier();
0258     declarations.reserve(declarations.size() + scopes.count());
0259     for (; !scopes.isEmpty(); scopes.pop()) {
0260         declarations.append( scopes + identifier );
0261     }
0262 
0263     clangDebug() << "Possible declarations:" << declarations;
0264     return declarations;
0265 }
0266 
0267 }
0268 
0269 QStringList UnknownDeclarationProblem::findMatchingIncludeFiles(const QVector<Declaration*>& declarations)
0270 {
0271     DUChainReadLocker lock;
0272 
0273     QStringList candidates;
0274     for (const auto decl: declarations) {
0275         // skip declarations that don't belong to us
0276         const auto& file = decl->topContext()->parsingEnvironmentFile();
0277         if (!file || file->language() != ParseSession::languageString()) {
0278             continue;
0279         }
0280 
0281         if( dynamic_cast<KDevelop::AliasDeclaration*>( decl ) ) {
0282             continue;
0283         }
0284 
0285         if( decl->isForwardDeclaration() ) {
0286             continue;
0287         }
0288 
0289         const auto filepath = decl->url().toUrl().toLocalFile();
0290 
0291         if( !isBlacklisted( filepath ) ) {
0292             candidates << filepath;
0293             clangDebug() << "Adding" << filepath << "determined from candidate" << decl->toString();
0294         }
0295 
0296         const auto& importers = file->importers();
0297         for (const auto& importer : importers) {
0298             if( importer->imports().count() != 1 && !isBlacklisted( filepath ) ) {
0299                 continue;
0300             }
0301             if( importer->topContext()->localDeclarations().count() ) {
0302                 continue;
0303             }
0304 
0305             const auto filePath = importer->url().toUrl().toLocalFile();
0306             if( isBlacklisted( filePath ) ) {
0307                 continue;
0308             }
0309 
0310             /* This file is a forwarder, such as <vector>
0311              * <vector> does not actually implement the functions, but include other headers that do
0312              * we prefer this to other headers
0313              */
0314             candidates << filePath;
0315             clangDebug() << "Adding forwarder file" << filePath << "to the result set";
0316         }
0317     }
0318 
0319     std::sort( candidates.begin(), candidates.end() );
0320     candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() );
0321     clangDebug() << "Candidates: " << candidates;
0322     return candidates;
0323 }
0324 
0325 namespace {
0326 
0327 /**
0328  * Takes a filepath and the include paths and determines what directive to use.
0329  */
0330 ClangFixit directiveForFile( const QString& includefile, const KDevelop::Path::List& includepaths, const KDevelop::Path& source )
0331 {
0332     const auto sourceFolder = source.parent();
0333     const Path canonicalFile( QFileInfo( includefile ).canonicalFilePath() );
0334 
0335     QString shortestDirective;
0336     bool isRelative = false;
0337 
0338     // we can include the file directly
0339     if (sourceFolder == canonicalFile.parent()) {
0340         shortestDirective = canonicalFile.lastPathSegment();
0341         isRelative = true;
0342     } else {
0343         // find the include directive with the shortest length
0344         for( const auto& includePath : includepaths ) {
0345             QString relative = includePath.relativePath( canonicalFile );
0346             if( relative.startsWith( QLatin1String("./") ) )
0347                 relative.remove(0, 2);
0348 
0349             if( shortestDirective.isEmpty() || relative.length() < shortestDirective.length() ) {
0350                 shortestDirective = relative;
0351                 isRelative = includePath == sourceFolder;
0352             }
0353         }
0354     }
0355 
0356     if( shortestDirective.isEmpty() ) {
0357         // Item not found in include path
0358         return {};
0359     }
0360 
0361     const auto range = DocumentRange(IndexedString(source.pathOrUrl()), includeDirectivePosition(source, canonicalFile.lastPathSegment()));
0362     if( !range.isValid() ) {
0363         clangDebug() << "unable to determine valid position for" << includefile << "in" << source.pathOrUrl();
0364         return {};
0365     }
0366 
0367     QString directive;
0368     if( isRelative ) {
0369         directive = QStringLiteral("#include \"%1\"").arg(shortestDirective);
0370     } else {
0371         directive = QStringLiteral("#include <%1>").arg(shortestDirective);
0372     }
0373     return ClangFixit{directive + QLatin1Char('\n'), range, i18n("Insert \'%1\'", directive), QString()};
0374 }
0375 
0376 KDevelop::Path::List includePaths( const KDevelop::Path& file )
0377 {
0378     // Find project's custom include paths
0379     const auto source = file.toLocalFile();
0380     const auto item = ICore::self()->projectController()->projectModel()->itemForPath( KDevelop::IndexedString( source ) );
0381 
0382     return IDefinesAndIncludesManager::manager()->includes(item);
0383 }
0384 
0385 /**
0386  * Return a list of header files viable for inclusions. All elements will be unique
0387  */
0388 QStringList includeFiles(const QualifiedIdentifier& identifier, const QVector<Declaration*>& declarations, const KDevelop::Path& file)
0389 {
0390     const auto includes = includePaths( file );
0391     if( includes.isEmpty() ) {
0392         clangDebug() << "Include path is empty";
0393         return {};
0394     }
0395 
0396     const auto candidates = UnknownDeclarationProblem::findMatchingIncludeFiles(declarations);
0397     if( !candidates.isEmpty() ) {
0398         // If we find a candidate from the duchain we don't bother scanning the include paths
0399         return candidates;
0400     }
0401 
0402     return scanIncludePaths(identifier, includes);
0403 }
0404 
0405 /**
0406  * Construct viable forward declarations for the type name.
0407  */
0408 ClangFixits forwardDeclarations(const QVector<Declaration*>& matchingDeclarations, const Path& source)
0409 {
0410     DUChainReadLocker lock;
0411     ClangFixits fixits;
0412     for (const auto decl : matchingDeclarations) {
0413         const auto qid = decl->qualifiedIdentifier();
0414 
0415         if (qid.count() > 1) {
0416             // TODO: Currently we're not able to determine what is namespaces, class names etc
0417             // and makes a suitable forward declaration, so just suggest "vanilla" declarations.
0418             continue;
0419         }
0420 
0421         const auto range = forwardDeclarationPosition(qid, source);
0422         if (!range.isValid()) {
0423             continue; // do not know where to insert
0424         }
0425 
0426         if (const auto classDecl = dynamic_cast<ClassDeclaration*>(decl)) {
0427             const auto name = qid.last().toString();
0428 
0429             switch (classDecl->classType()) {
0430             case ClassDeclarationData::Class:
0431                 fixits += {
0432                     QLatin1String("class ") + name + QLatin1String(";\n"), range,
0433                     i18n("Forward declare as 'class'"),
0434                     QString()
0435                 };
0436                 break;
0437             case ClassDeclarationData::Struct:
0438                 fixits += {
0439                     QLatin1String("struct ") + name + QLatin1String(";\n"), range,
0440                     i18n("Forward declare as 'struct'"),
0441                     QString()
0442                 };
0443                 break;
0444             default:
0445                 break;
0446             }
0447         }
0448     }
0449     return fixits;
0450 }
0451 
0452 /**
0453  * Search the persistent symbol table for matching declarations for identifiers @p identifiers
0454  */
0455 QVector<Declaration*> findMatchingDeclarations(const QVector<QualifiedIdentifier>& identifiers)
0456 {
0457     DUChainReadLocker lock;
0458 
0459     QVector<Declaration*> matchingDeclarations;
0460     matchingDeclarations.reserve(identifiers.size());
0461     for (const auto& declaration : identifiers) {
0462         clangDebug() << "Considering candidate declaration" << declaration;
0463         PersistentSymbolTable::self().visitDeclarations(declaration, [&](const IndexedDeclaration& indexedDecl) {
0464             // Skip if the declaration is invalid
0465             if (auto decl = indexedDecl.declaration()) {
0466                 matchingDeclarations << decl;
0467             }
0468             return PersistentSymbolTable::VisitorState::Continue;
0469         });
0470     }
0471     return matchingDeclarations;
0472 }
0473 
0474 ClangFixits fixUnknownDeclaration( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::DocumentRange& docrange )
0475 {
0476     ClangFixits fixits;
0477 
0478     const CursorInRevision cursor{docrange.start().line(), docrange.start().column()};
0479 
0480     const auto possibleIdentifiers = findPossibleQualifiedIdentifiers(identifier, file, cursor);
0481     const auto matchingDeclarations = findMatchingDeclarations(possibleIdentifiers);
0482 
0483     if (ClangSettingsManager::self()->assistantsSettings().forwardDeclare) {
0484         const auto& forwardDeclareFixits = forwardDeclarations(matchingDeclarations, file);
0485         for (const auto& fixit : forwardDeclareFixits) {
0486             fixits << fixit;
0487             if (fixits.size() == maxSuggestions) {
0488                 return fixits;
0489             }
0490         }
0491     }
0492 
0493     const auto includefiles = includeFiles(identifier, matchingDeclarations, file);
0494     if (includefiles.isEmpty()) {
0495         // return early as the computation of the include paths is quite expensive
0496         return fixits;
0497     }
0498 
0499     const auto includepaths = includePaths( file );
0500     clangDebug() << "found include paths for" << file << ":" << includepaths;
0501 
0502     /* create fixits for candidates */
0503     for( const auto& includeFile : includefiles ) {
0504         const auto fixit = directiveForFile( includeFile, includepaths, file /* UP */ );
0505         if (!fixit.range.isValid()) {
0506             clangDebug() << "unable to create directive for" << includeFile << "in" << file.toLocalFile();
0507             continue;
0508         }
0509 
0510         fixits << fixit;
0511 
0512         if (fixits.size() == maxSuggestions) {
0513             return fixits;
0514         }
0515     }
0516 
0517     return fixits;
0518 }
0519 
0520 QString symbolFromDiagnosticSpelling(const QString& str)
0521 {
0522     /* in all error messages the symbol is in the first pair of quotes */
0523     const auto split = str.split( QLatin1Char('\'') );
0524     auto symbol = split.value( 1 );
0525 
0526     if( str.startsWith( QLatin1String("No member named") ) ) {
0527         symbol = split.value( 3 ) + QLatin1String("::") + split.value( 1 );
0528     }
0529     return symbol;
0530 }
0531 
0532 }
0533 
0534 UnknownDeclarationProblem::UnknownDeclarationProblem(CXDiagnostic diagnostic, CXTranslationUnit unit)
0535     : ClangProblem(diagnostic, unit)
0536 {
0537     setSymbol(QualifiedIdentifier(symbolFromDiagnosticSpelling(description())));
0538 }
0539 
0540 void UnknownDeclarationProblem::setSymbol(const QualifiedIdentifier& identifier)
0541 {
0542     m_identifier = identifier;
0543 }
0544 
0545 IAssistant::Ptr UnknownDeclarationProblem::solutionAssistant() const
0546 {
0547     const Path path(finalLocation().document.str());
0548     const auto fixits = allFixits() + fixUnknownDeclaration(m_identifier, path, finalLocation());
0549     return IAssistant::Ptr(new ClangFixitAssistant(fixits));
0550 }