File indexing completed on 2024-05-19 15:44:56
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 }