unknowndeclarationproblem.cpp 17.5 KB
Newer Older
Kevin Funk's avatar
Kevin Funk committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
 * Copyright 2014 Jørgen Kvalsvik <lycantrophe@lavabit.com>
 * Copyright 2014 Kevin Funk <kfunk@kde.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License or (at your option) version 3 or any later version
 * accepted by the membership of KDE e.V. (or its successor approved
 * by the membership of KDE e.V.), which shall act as a proxy
 * defined in Section 14 of version 3 of the license.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

23
24
#include "unknowndeclarationproblem.h"

25
#include "clanghelpers.h"
26
#include "parsesession.h"
27
#include "../util/clangdebug.h"
28
#include "../util/clangutils.h"
29
#include "../util/clangtypes.h"
30
#include "../clangsettings/clangsettingsmanager.h"
31
32
33
34
35
36
37
38
39
40
41

#include <interfaces/icore.h>
#include <interfaces/iprojectcontroller.h>
#include <interfaces/iproject.h>

#include <language/duchain/persistentsymboltable.h>
#include <language/duchain/aliasdeclaration.h>
#include <language/duchain/parsingenvironment.h>
#include <language/duchain/topducontext.h>
#include <language/duchain/duchainlock.h>
#include <language/duchain/duchainutils.h>
42
#include <languages/plugins/custom-definesandincludes/idefinesandincludesmanager.h>
43
44

#include <project/projectmodel.h>
Kevin Funk's avatar
Kevin Funk committed
45
#include <util/path.h>
46
47
48
49
50
51
52
53
54

#include <QDir>
#include <QProcess>

#include <algorithm>

using namespace KDevelop;

namespace {
55
/** Under some conditions, such as when looking up suggestions
Kevin Funk's avatar
Kevin Funk committed
56
57
58
59
60
61
62
 * for the undeclared namespace 'std' we will get an awful lot
 * of suggestions. This parameter limits how many suggestions
 * will pop up, as rarely more than a few will be relevant anyways
 *
 * Forward declaration suggestions are included in this number
 */
const int maxSuggestions = 5;
63

64
/**
65
66
67
 * We don't want anything from the bits directory -
 * we'd rather prefer forwarding includes, such as <vector>
 */
68
69
bool isBlacklisted(const QString& path)
{
Kevin Funk's avatar
Kevin Funk committed
70
    if (ClangHelpers::isSource(path))
71
72
73
74
        return true;

    // Do not allow including directly from the bits directory.
    // Instead use one of the forwarding headers in other directories, when possible.
75
    if (path.contains( QLatin1String("bits") ) && path.contains(QLatin1String("/include/c++/")))
76
77
78
        return true;

    return false;
79
80
}

81
QStringList scanIncludePaths( const QString& identifier, const QDir& dir, int maxDepth = 3 )
82
{
83
84
85
86
    if (!maxDepth) {
        return {};
    }

87
88
89
90
91
92
93
    QStringList candidates;
    const auto path = dir.absolutePath();

    if( isBlacklisted( path ) ) {
        return {};
    }

94
95
    const QStringList nameFilters = {identifier, identifier + QLatin1String(".*")};
    for (const auto& file : dir.entryList(nameFilters, QDir::Files)) {
96
97
98
99
        if (identifier.compare(file, Qt::CaseInsensitive) == 0 || ClangHelpers::isHeader(file)) {
            const QString filePath = path + QLatin1Char('/') + file;
            clangDebug() << "Found candidate file" << filePath;
            candidates.append( filePath );
100
101
102
        }
    }

103
    maxDepth--;
104
    for( const auto& subdir : dir.entryList( QDir::Dirs | QDir::NoDotAndDotDot ) )
105
        candidates += scanIncludePaths( identifier, QDir{ path + QLatin1Char('/') + subdir }, maxDepth );
106
107
108
109

    return candidates;
}

110
/**
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
 * Find files in dir that match the given identifier. Matches common C++ header file extensions only.
 */
QStringList scanIncludePaths( const QualifiedIdentifier& identifier, const KDevelop::Path::List& includes )
{
    const auto stripped_identifier = identifier.last().toString();
    QStringList candidates;
    for( const auto& include : includes ) {
        candidates += scanIncludePaths( stripped_identifier, QDir{ include.toLocalFile() } );
    }

    std::sort( candidates.begin(), candidates.end() );
    candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() );
    return candidates;
}

126
/**
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
 * Determine how much path is shared between two includes.
 *  boost/tr1/unordered_map
 *  boost/tr1/unordered_set
 * have a shared path of 2 where
 *  boost/tr1/unordered_map
 *  boost/vector
 * have a shared path of 1
 */
int sharedPathLevel(const QString& a, const QString& b)
{
    int shared = -1;
    for(auto x = a.begin(), y = b.begin(); *x == *y && x != a.end() && y != b.end() ; ++x, ++y ) {
        if( *x == QDir::separator() ) {
            ++shared;
        }
    }

    return shared;
}

/**
Milian Wolff's avatar
Milian Wolff committed
148
149
150
151
152
153
154
 * Try to find a proper include position from the DUChain:
 *
 * 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
 *
 * TODO: Implement a fallback scheme
 */
155
KDevelop::DocumentRange includeDirectivePosition(const KDevelop::Path& source, const QString& includeFile)
156
157
158
159
{
    DUChainReadLocker lock;
    const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() );
    if( !top ) {
160
        clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range";
161
        return KDevelop::DocumentRange::invalid();
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
    }

    int line = -1;

    // look at existing #include statements and re-use them
    int currentMatchQuality = -1;
    for( const auto& import : top->importedParentContexts() ) {

        const int matchQuality = sharedPathLevel( import.context(top)->url().str(), includeFile );
        if( matchQuality < currentMatchQuality ) {
            continue;
        }

        line = import.position.line + 1;
        currentMatchQuality = matchQuality;
    }

    if( line == -1 ) {
        /* Insert at the top of the document */
181
        return {IndexedString(source.pathOrUrl()), {0, 0, 0, 0}};
182
183
    }

184
    return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}};
185
186
}

187
KDevelop::DocumentRange forwardDeclarationPosition(const QualifiedIdentifier& identifier, const KDevelop::Path& source)
188
189
190
191
{
    DUChainReadLocker lock;
    const TopDUContext* top = DUChainUtils::standardContextForUrl( source.toUrl() );
    if( !top ) {
192
        clangDebug() << "unable to find standard context for" << source.toLocalFile() << "Creating null range";
193
        return KDevelop::DocumentRange::invalid();
194
195
    }

196
197
198
199
200
    if (!top->findDeclarations(identifier).isEmpty()) {
        // Already forward-declared
        return KDevelop::DocumentRange::invalid();
    }

201
202
203
204
205
206
    int line = std::numeric_limits< int >::max();
    for( const auto decl : top->localDeclarations() ) {
        line = std::min( line, decl->range().start.line );
    }

    if( line == std::numeric_limits< int >::max() ) {
207
        return KDevelop::DocumentRange::invalid();
208
209
210
211
212
    }

    // We want it one line above the first declaration
    line = std::max( line - 1, 0 );

213
    return {IndexedString(source.pathOrUrl()), {line, 0, line, 0}};
214
215
}

216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/**
 * Iteratively build all levels of the current scope. A (missing) type anywhere
 * can be aribtrarily namespaced, so we create the permutations of possible
 * nestings of namespaces it can currently be in,
 *
 * TODO: add detection of namespace aliases, such as 'using namespace KDevelop;'
 *
 * namespace foo {
 *      namespace bar {
 *          function baz() {
 *              type var;
 *          }
 *      }
 * }
 *
 * Would give:
 * foo::bar::baz::type
 * foo::bar::type
 * foo::type
 * type
 */
QVector<KDevelop::QualifiedIdentifier> findPossibleQualifiedIdentifiers( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::CursorInRevision& cursor )
238
239
240
241
242
{
    DUChainReadLocker lock;
    const TopDUContext* top = DUChainUtils::standardContextForUrl( file.toUrl() );

    if( !top ) {
243
        clangDebug() << "unable to find standard context for" << file.toLocalFile() << "Not creating duchain candidates";
244
245
246
247
248
        return {};
    }

    const auto* context = top->findContextAt( cursor );
    if( !context ) {
249
        clangDebug() << "No context found at" << cursor;
250
251
252
253
254
255
256
257
        return {};
    }

    QVector<KDevelop::QualifiedIdentifier> declarations{ identifier };
    for( auto scopes = context->scopeIdentifier(); !scopes.isEmpty(); scopes.pop() ) {
        declarations.append( scopes + identifier );
    }

258
    clangDebug() << "Possible declarations:" << declarations;
259
260
261
    return declarations;
}

262
QStringList findMatchingIncludeFiles(const QVector<Declaration*> declarations)
263
264
{
    QStringList candidates;
265
266
267
268
269
270
    for (const auto decl: declarations) {
        // skip declarations that don't belong to us
        const auto& file = decl->topContext()->parsingEnvironmentFile();
        if (!file || file->language() != ParseSession::languageString()) {
            continue;
        }
271

272
273
274
        if( dynamic_cast<KDevelop::AliasDeclaration*>( decl ) ) {
            continue;
        }
275

276
277
278
        if( decl->isForwardDeclaration() ) {
            continue;
        }
279

280
        const auto filepath = decl->url().toUrl().toLocalFile();
281

282
283
284
285
286
287
288
        if( !isBlacklisted( filepath ) ) {
            candidates << filepath;
            clangDebug() << "Adding" << filepath << "determined from candidate" << decl->toString();
        }

        for( const auto importer : file->importers() ) {
            if( importer->imports().count() != 1 && !isBlacklisted( filepath ) ) {
289
290
                continue;
            }
291
            if( importer->topContext()->localDeclarations().count() ) {
292
293
294
                continue;
            }

295
296
297
            const auto filePath = importer->url().toUrl().toLocalFile();
            if( isBlacklisted( filePath ) ) {
                continue;
298
299
            }

300
301
302
303
304
305
            /* This file is a forwarder, such as <vector>
             * <vector> does not actually implement the functions, but include other headers that do
             * we prefer this to other headers
             */
            candidates << filePath;
            clangDebug() << "Adding forwarder file" << filePath << "to the result set";
306
307
308
309
310
        }
    }

    std::sort( candidates.begin(), candidates.end() );
    candidates.erase( std::unique( candidates.begin(), candidates.end() ), candidates.end() );
311
    clangDebug() << "Candidates: " << candidates;
312
313
314
    return candidates;
}

315
/**
316
317
 * Takes a filepath and the include paths and determines what directive to use.
 */
318
ClangFixit directiveForFile( const QString& includefile, const KDevelop::Path::List& includepaths, const KDevelop::Path& source )
319
{
320
321
322
    const auto sourceFolder = source.parent();
    const Path canonicalFile( QFileInfo( includefile ).canonicalFilePath() );

323
324
325
    QString shortestDirective;
    bool isRelative = false;

326
327
328
329
330
331
332
333
    // we can include the file directly
    if (sourceFolder == canonicalFile.parent()) {
        shortestDirective = canonicalFile.lastPathSegment();
        isRelative = true;
    } else {
        // find the include directive with the shortest length
        for( const auto& includePath : includepaths ) {
            QString relative = includePath.relativePath( canonicalFile );
334
            if( relative.startsWith( QLatin1String("./") ) )
335
336
337
338
339
340
                relative = relative.mid( 2 );

            if( shortestDirective.isEmpty() || relative.length() < shortestDirective.length() ) {
                shortestDirective = relative;
                isRelative = includePath == sourceFolder;
            }
341
342
343
344
345
346
347
348
        }
    }

    if( shortestDirective.isEmpty() ) {
        // Item not found in include path
        return {};
    }

349
350
    const auto range = DocumentRange(IndexedString(source.pathOrUrl()), includeDirectivePosition(source, canonicalFile.lastPathSegment()));
    if( !range.isValid() ) {
351
        clangDebug() << "unable to determine valid position for" << includefile << "in" << source.pathOrUrl();
352
        return {};
353
354
    }

355
356
    QString directive;
    if( isRelative ) {
357
        directive = QStringLiteral("#include \"%1\"").arg(shortestDirective);
358
    } else {
359
        directive = QStringLiteral("#include <%1>").arg(shortestDirective);
360
    }
361
    return ClangFixit{directive + QLatin1Char('\n'), range, QObject::tr("Insert \'%1\'").arg(directive)};
362
363
364
365
}

KDevelop::Path::List includePaths( const KDevelop::Path& file )
{
366
    // Find project's custom include paths
367
368
369
    const auto source = file.toLocalFile();
    const auto item = ICore::self()->projectController()->projectModel()->itemForPath( KDevelop::IndexedString( source ) );

Sergey Kalinichev's avatar
Sergey Kalinichev committed
370
    return IDefinesAndIncludesManager::manager()->includes(item);
371
372
}

373
/**
374
375
 * Return a list of header files viable for inclusions. All elements will be unique
 */
376
QStringList includeFiles(const QualifiedIdentifier& identifier, const QVector<Declaration*> declarations, const KDevelop::Path& file)
377
378
379
{
    const auto includes = includePaths( file );
    if( includes.isEmpty() ) {
380
        clangDebug() << "Include path is empty";
381
382
383
        return {};
    }

384
    const auto candidates = findMatchingIncludeFiles(declarations);
385
386
387
388
389
    if( !candidates.isEmpty() ) {
        // If we find a candidate from the duchain we don't bother scanning the include paths
        return candidates;
    }

390
    return scanIncludePaths(identifier, includes);
391
392
}

393
/**
Kevin Funk's avatar
Kevin Funk committed
394
395
 * Construct viable forward declarations for the type name.
 */
396
ClangFixits forwardDeclarations(const QVector<Declaration*>& matchingDeclarations, const Path& source)
397
{
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
    ClangFixits fixits;
    for (const auto decl : matchingDeclarations) {
        const auto qid = decl->qualifiedIdentifier();
        if (qid.count() > 1) {
            // TODO: Currently we're not able to determine what is namespaces, class names etc
            // and makes a suitable forward declaration, so just suggest "vanilla" declarations.
            continue;
        }

        const auto range = forwardDeclarationPosition(qid, source);
        if (!range.isValid()) {
            continue; // do not know where to insert
        }

        const auto name = qid.last().toString();
        fixits += {
            {QLatin1String("class ") + name + QLatin1String(";\n"), range, QObject::tr("Forward declare as 'class'")},
            {QLatin1String("struct ") + name + QLatin1String(";\n"), range, QObject::tr("Forward declare as 'struct'")}
        };
417
    }
418
419
    return fixits;
}
420

421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
/**
 * Search the persistent symbol table for matching declarations for identifiers @p identifiers
 */
QVector<Declaration*> findMatchingDeclarations(const QVector<QualifiedIdentifier>& identifiers)
{
    DUChainReadLocker lock;

    QVector<Declaration*> matchingDeclarations;
    matchingDeclarations.reserve(identifiers.size());
    for (const auto& declaration : identifiers) {
        clangDebug() << "Considering candidate declaration" << declaration;
        const IndexedDeclaration* declarations;
        uint declarationCount;
        PersistentSymbolTable::self().declarations( declaration , declarationCount, declarations );

        for (uint i = 0; i < declarationCount; ++i) {
            // Skip if the declaration is invalid or if it is an alias declaration -
            // we want the actual declaration (and its file)
            if (auto decl = declarations[i].declaration()) {
                matchingDeclarations << decl;
            }
        }
    }
    return matchingDeclarations;
445
446
}

447
ClangFixits fixUnknownDeclaration( const QualifiedIdentifier& identifier, const KDevelop::Path& file, const KDevelop::DocumentRange& docrange )
448
{
449
    ClangFixits fixits;
450

451
452
453
454
455
    const CursorInRevision cursor{docrange.start().line(), docrange.start().column()};

    const auto possibleIdentifiers = findPossibleQualifiedIdentifiers(identifier, file, cursor);
    const auto matchingDeclarations = findMatchingDeclarations(possibleIdentifiers);

456
    if (ClangSettingsManager::self()->assistantsSettings().forwardDeclare) {
457
        for (const auto& fixit : forwardDeclarations(matchingDeclarations, file)) {
458
459
460
461
            fixits << fixit;
            if (fixits.size() == maxSuggestions) {
                return fixits;
            }
462
        }
463
464
    }

465
    const auto includefiles = includeFiles(identifier, matchingDeclarations, file);
466
467
468
469
470
471
    if (includefiles.isEmpty()) {
        // return early as the computation of the include paths is quite expensive
        return fixits;
    }

    const auto includepaths = includePaths( file );
472
473
474

    /* create fixits for candidates */
    for( const auto& includeFile : includefiles ) {
475
476
        const auto fixit = directiveForFile( includeFile, includepaths, file /* UP */ );
        if (!fixit.range.isValid()) {
477
            clangDebug() << "unable to create directive for" << includeFile << "in" << file.toLocalFile();
478
479
480
            continue;
        }

481
        fixits << fixit;
482

483
484
485
        if (fixits.size() == maxSuggestions) {
            return fixits;
        }
486
487
488
489
490
    }

    return fixits;
}

Kevin Funk's avatar
Kevin Funk committed
491
492
493
QString symbolFromDiagnosticSpelling(const QString& str)
{
    /* in all error messages the symbol is in in the first pair of quotes */
494
    const auto split = str.split( QLatin1Char('\'') );
Kevin Funk's avatar
Kevin Funk committed
495
496
    auto symbol = split.value( 1 );

497
498
    if( str.startsWith( QLatin1String("No member named") ) ) {
        symbol = split.value( 3 ) + QLatin1String("::") + split.value( 1 );
Kevin Funk's avatar
Kevin Funk committed
499
500
501
502
    }
    return symbol;
}

503
504
}

505
506
UnknownDeclarationProblem::UnknownDeclarationProblem(CXDiagnostic diagnostic, CXTranslationUnit unit)
    : ClangProblem(diagnostic, unit)
507
{
Kevin Funk's avatar
Kevin Funk committed
508
    setSymbol(QualifiedIdentifier(symbolFromDiagnosticSpelling(description())));
509
510
}

511
void UnknownDeclarationProblem::setSymbol(const QualifiedIdentifier& identifier)
512
{
513
    m_identifier = identifier;
514
515
}

Kevin Funk's avatar
Kevin Funk committed
516
IAssistant::Ptr UnknownDeclarationProblem::solutionAssistant() const
517
{
518
    const Path path(finalLocation().document.str());
Kevin Funk's avatar
Kevin Funk committed
519
    const auto fixits = allFixits() + fixUnknownDeclaration(m_identifier, path, finalLocation());
Kevin Funk's avatar
Kevin Funk committed
520
    return IAssistant::Ptr(new ClangFixitAssistant(fixits));
521
}