forked from qt-creator/qt-creator
Clang: Provide tooltips
This includes also the query data for the help system (F1) for an
identifier under cursor.
Regressions (libclang changes necessary):
- Function signatures do not contain default values.
- Aliases are not resolved for/at:
- template types
- qualified name of a type
Fixes/Improvements:
- Resolve "auto"
- On a template type, show also the template parameter.
- For a typedef like
typedef long long superlong;
the tooltip was "long long superlong", which was confusing.
Now, "long long" is shown.
New:
- Show first or \brief paragraph of a documentation comment.
- Show size of a class at definition.
- Show size of a field member in class definition.
Task-number: QTCREATORBUG-11259
Change-Id: Ie1a07930d0e882015d07dc43e35bb81a685cdeb8
Reviewed-by: Marco Bubke <marco.bubke@qt.io>
This commit is contained in:
531
src/tools/clangbackend/source/clangtooltipinfocollector.cpp
Normal file
531
src/tools/clangbackend/source/clangtooltipinfocollector.cpp
Normal file
@@ -0,0 +1,531 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2018 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
**
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "clangtooltipinfocollector.h"
|
||||
|
||||
#include "clangbackend_global.h"
|
||||
#include "clangstring.h"
|
||||
#include "cursor.h"
|
||||
#include "sourcerange.h"
|
||||
#include "unsavedfiles.h"
|
||||
#include "unsavedfile.h"
|
||||
|
||||
#include <clangsupport/sourcerangecontainer.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/textfileformat.h>
|
||||
|
||||
#include <utf8string.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QTextCodec>
|
||||
|
||||
namespace ClangBackEnd {
|
||||
|
||||
namespace {
|
||||
|
||||
Utf8StringVector qualificationPrefixAsVector(const Cursor &cursor)
|
||||
{
|
||||
Utf8StringVector result;
|
||||
|
||||
for (Cursor parent = cursor.semanticParent();
|
||||
parent.isValid() && (parent.kind() == CXCursor_Namespace || parent.isCompoundType());
|
||||
parent = parent.semanticParent()) {
|
||||
result.prepend(parent.spelling());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Utf8String qualificationPrefix(const Cursor &cursor)
|
||||
{
|
||||
// TODO: Implement with qualificationPrefixAsVector()
|
||||
Utf8String qualifiedName;
|
||||
|
||||
for (Cursor parent = cursor.semanticParent();
|
||||
parent.isValid() && (parent.kind() == CXCursor_Namespace);
|
||||
parent = parent.semanticParent()) {
|
||||
qualifiedName = parent.spelling() + Utf8StringLiteral("::") + qualifiedName;
|
||||
}
|
||||
|
||||
return qualifiedName;
|
||||
}
|
||||
|
||||
Utf8String displayName(const Cursor &cursor)
|
||||
{
|
||||
if (cursor.kind() == CXCursor_ClassTemplate) {
|
||||
// TODO: The qualification should be part of the display name. Fix this in libclang.
|
||||
return qualificationPrefix(cursor) + cursor.displayName();
|
||||
}
|
||||
|
||||
return cursor.displayName();
|
||||
}
|
||||
|
||||
Utf8String textForFunctionLike(const Cursor &cursor)
|
||||
{
|
||||
#ifdef IS_PRETTY_DECL_SUPPORTED
|
||||
CXPrintingPolicy policy = clang_getCursorPrintingPolicy(cursor.cx());
|
||||
clang_PrintingPolicy_setProperty(policy, CXPrintingPolicy_FullyQualifiedName, 1);
|
||||
clang_PrintingPolicy_setProperty(policy, CXPrintingPolicy_TerseOutput, 1);
|
||||
// Avoid printing attributes/pragmas
|
||||
clang_PrintingPolicy_setProperty(policy, CXPrintingPolicy_PolishForDeclaration, 1);
|
||||
clang_PrintingPolicy_setProperty(policy, CXPrintingPolicy_SuppressInitializers, 1);
|
||||
const Utf8String prettyPrinted = ClangString(
|
||||
clang_getCursorPrettyPrinted(cursor.cx(), policy));
|
||||
clang_PrintingPolicy_dispose(policy);
|
||||
return prettyPrinted;
|
||||
#else
|
||||
// Printing function declarations with displayName() is quite limited:
|
||||
// * result type is not included
|
||||
// * parameter names are not included
|
||||
// * templates in the result type are not included
|
||||
// * no full qualification of the function name
|
||||
return Utf8String(cursor.resultType().spelling())
|
||||
+ Utf8StringLiteral(" ")
|
||||
+ qualificationPrefix(cursor)
|
||||
+ Utf8String(cursor.displayName());
|
||||
#endif
|
||||
}
|
||||
|
||||
Utf8String textForEnumConstantDecl(const Cursor &cursor)
|
||||
{
|
||||
const Cursor semanticParent = cursor.semanticParent();
|
||||
QTC_ASSERT(semanticParent.kind() == CXCursor_EnumDecl, return Utf8String());
|
||||
|
||||
const Type enumType = semanticParent.enumType();
|
||||
if (enumType.isUnsigned())
|
||||
return Utf8String::number(cursor.enumConstantUnsignedValue());
|
||||
return Utf8String::number(cursor.enumConstantValue());
|
||||
}
|
||||
|
||||
Utf8String textForInclusionDirective(const Cursor &cursor)
|
||||
{
|
||||
const CXFile includedFile = cursor.includedFile();
|
||||
const Utf8String fileName = ClangString(clang_getFileName(includedFile));
|
||||
|
||||
return QDir::toNativeSeparators(fileName.toString());
|
||||
}
|
||||
|
||||
Utf8String textForAnyTypeAlias(const Cursor &cursor)
|
||||
{
|
||||
// For a CXCursor_TypeAliasTemplateDecl the type of cursor/referenced
|
||||
// is invalid, so we do not get the underlying type. This here solely
|
||||
// reports the unresolved name instead of the empty string.
|
||||
if (cursor.kind() == CXCursor_TypeAliasTemplateDecl)
|
||||
return cursor.displayName();
|
||||
|
||||
return cursor.type().alias().utf8Spelling();
|
||||
}
|
||||
|
||||
bool includeSizeForCursor(const Cursor &cursor)
|
||||
{
|
||||
return cursor.isCompoundType()
|
||||
|| cursor.kind() == CXCursor_EnumDecl
|
||||
|| cursor.kind() == CXCursor_UnionDecl
|
||||
|| cursor.kind() == CXCursor_FieldDecl;
|
||||
}
|
||||
|
||||
Utf8String sizeInBytes(const Cursor &cursor)
|
||||
{
|
||||
if (includeSizeForCursor(cursor)) {
|
||||
bool ok = false;
|
||||
const long long size = cursor.type().sizeOf(&ok);
|
||||
if (ok)
|
||||
return Utf8String::number(size);
|
||||
}
|
||||
|
||||
return Utf8String();
|
||||
}
|
||||
|
||||
Cursor referencedCursor(const Cursor &cursor)
|
||||
{
|
||||
// Query the referenced cursor directly instead of first testing with cursor.isReference().
|
||||
// cursor.isReference() reports false for e.g. CXCursor_DeclRefExpr or CXCursor_CallExpr
|
||||
// although it returns a valid cursor.
|
||||
const Cursor referenced = cursor.referenced();
|
||||
if (referenced.isValid())
|
||||
return referenced;
|
||||
|
||||
const Cursor definition = cursor.definition();
|
||||
if (definition.isValid())
|
||||
return definition;
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
class ToolTipInfoCollector
|
||||
{
|
||||
public:
|
||||
ToolTipInfoCollector(UnsavedFiles &unsavedFiles,
|
||||
const Utf8String &textCodecName,
|
||||
const Utf8String &mainFilePath,
|
||||
CXTranslationUnit cxTranslationUnit);
|
||||
|
||||
ToolTipInfo collect(uint line, uint column) const;
|
||||
|
||||
private:
|
||||
Utf8String text(const Cursor &cursor, const Cursor &referenced) const;
|
||||
Utf8String textForMacroExpansion(const Cursor &cursor) const;
|
||||
Utf8String textForNamespaceAlias(const Cursor &cursor) const;
|
||||
|
||||
ToolTipInfo qDocInfo(const Cursor &cursor) const;
|
||||
|
||||
CXSourceLocation toCXSourceLocation(uint line, uint column) const;
|
||||
|
||||
UnsavedFile unsavedFile(const Utf8String &filePath) const;
|
||||
Utf8String lineRange(const Utf8String &filePath, unsigned fromLine, unsigned toLine) const;
|
||||
|
||||
private:
|
||||
UnsavedFiles &m_unsavedFiles;
|
||||
const Utf8String m_textCodecName;
|
||||
|
||||
const Utf8String m_mainFilePath;
|
||||
CXTranslationUnit m_cxTranslationUnit = nullptr;
|
||||
|
||||
};
|
||||
|
||||
ToolTipInfoCollector::ToolTipInfoCollector(UnsavedFiles &unsavedFiles,
|
||||
const Utf8String &textCodecName,
|
||||
const Utf8String &mainFilePath,
|
||||
CXTranslationUnit cxTranslationUnit)
|
||||
: m_unsavedFiles(unsavedFiles)
|
||||
, m_textCodecName(textCodecName)
|
||||
, m_mainFilePath(mainFilePath)
|
||||
, m_cxTranslationUnit(cxTranslationUnit)
|
||||
{
|
||||
}
|
||||
|
||||
Utf8String ToolTipInfoCollector::text(const Cursor &cursor, const Cursor &referenced) const
|
||||
{
|
||||
if (cursor.kind() == CXCursor_MacroExpansion)
|
||||
return textForMacroExpansion(referenced);
|
||||
|
||||
if (referenced.kind() == CXCursor_EnumConstantDecl)
|
||||
return textForEnumConstantDecl(referenced);
|
||||
|
||||
if (referenced.kind() == CXCursor_InclusionDirective)
|
||||
return textForInclusionDirective(referenced);
|
||||
|
||||
if (referenced.kind() == CXCursor_Namespace)
|
||||
return qualificationPrefix(referenced) + referenced.spelling();
|
||||
|
||||
if (referenced.kind() == CXCursor_NamespaceAlias)
|
||||
return textForNamespaceAlias(referenced);
|
||||
|
||||
if (referenced.isAnyTypeAlias())
|
||||
return textForAnyTypeAlias(referenced);
|
||||
|
||||
if (referenced.isFunctionLike())
|
||||
return textForFunctionLike(referenced);
|
||||
|
||||
if (referenced.type().canonical().isBuiltinType())
|
||||
return referenced.type().canonical().builtinTypeToString();
|
||||
|
||||
if (referenced.kind() == CXCursor_VarDecl)
|
||||
return referenced.type().spelling(); // e.g. "Zii<int>"
|
||||
|
||||
const Type referencedType = referenced.type();
|
||||
if (referencedType.isValid()) {
|
||||
// Generally, the type includes the qualification but has this limitations:
|
||||
// * namespace aliases are not resolved
|
||||
// * outer class of a inner template class is not included
|
||||
// The type includes the qualification, but not resolved namespace aliases.
|
||||
|
||||
// For a CXType_Record, this also includes e.g. "const " as prefix.
|
||||
return referencedType.canonical().utf8Spelling();
|
||||
}
|
||||
|
||||
return displayName(referenced);
|
||||
}
|
||||
|
||||
Utf8String ToolTipInfoCollector::textForMacroExpansion(const Cursor &cursor) const
|
||||
{
|
||||
QTC_ASSERT(cursor.kind() == CXCursor_MacroDefinition, return Utf8String());
|
||||
|
||||
const SourceRange sourceRange = cursor.sourceRange();
|
||||
const SourceLocation start = sourceRange.start();
|
||||
const SourceLocation end = sourceRange.end();
|
||||
|
||||
return lineRange(start.filePath(), start.line(), end.line());
|
||||
}
|
||||
|
||||
Utf8String ToolTipInfoCollector::textForNamespaceAlias(const Cursor &cursor) const
|
||||
{
|
||||
// TODO: Add some libclang API to get the aliased name straight away.
|
||||
|
||||
CXToken *cxTokens = nullptr;
|
||||
uint cxTokenCount = 0;
|
||||
|
||||
clang_tokenize(m_cxTranslationUnit, cursor.cxSourceRange(), &cxTokens, &cxTokenCount);
|
||||
|
||||
Utf8String aliasedName;
|
||||
// Start at 3 in order to skip these tokens: namespace X =
|
||||
for (uint i = 3; i < cxTokenCount; ++i)
|
||||
aliasedName += ClangString(clang_getTokenSpelling(m_cxTranslationUnit, cxTokens[i]));
|
||||
|
||||
clang_disposeTokens(m_cxTranslationUnit, cxTokens, cxTokenCount);
|
||||
|
||||
return aliasedName;
|
||||
}
|
||||
|
||||
static Utf8String typeName(const Type &type)
|
||||
{
|
||||
return type.declaration().spelling();
|
||||
}
|
||||
|
||||
static Utf8String qdocMark(const Cursor &cursor)
|
||||
{
|
||||
if (cursor.kind() == CXCursor_ClassTemplate)
|
||||
return cursor.spelling();
|
||||
|
||||
if (cursor.type().kind() == CXType_Enum
|
||||
|| cursor.type().kind() == CXType_Typedef
|
||||
|| cursor.type().kind() == CXType_Record)
|
||||
return typeName(cursor.type());
|
||||
|
||||
Utf8String text = cursor.displayName();
|
||||
if (cursor.kind() == CXCursor_FunctionDecl) {
|
||||
// TODO: Remove this workaround by fixing this in
|
||||
// libclang with the help of CXPrintingPolicy.
|
||||
text.replace(Utf8StringLiteral("<>"), Utf8String());
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
static ToolTipInfo::QdocCategory qdocCategory(const Cursor &cursor)
|
||||
{
|
||||
if (cursor.isFunctionLike())
|
||||
return ToolTipInfo::Function;
|
||||
|
||||
if (cursor.kind() == CXCursor_MacroDefinition)
|
||||
return ToolTipInfo::Macro;
|
||||
|
||||
if (cursor.kind() == CXCursor_EnumConstantDecl)
|
||||
return ToolTipInfo::Enum;
|
||||
|
||||
if (cursor.type().kind() == CXType_Enum)
|
||||
return ToolTipInfo::Enum;
|
||||
|
||||
if (cursor.kind() == CXCursor_InclusionDirective)
|
||||
return ToolTipInfo::Brief;
|
||||
|
||||
// TODO: Handle CXCursor_NamespaceAlias, too?!
|
||||
if (cursor.kind() == CXCursor_Namespace)
|
||||
return ToolTipInfo::ClassOrNamespace;
|
||||
|
||||
if (cursor.isCompoundType())
|
||||
return ToolTipInfo::ClassOrNamespace;
|
||||
|
||||
if (cursor.kind() == CXCursor_NamespaceAlias)
|
||||
return ToolTipInfo::ClassOrNamespace;
|
||||
|
||||
if (cursor.type().kind() == CXType_Typedef)
|
||||
return ToolTipInfo::Typedef;
|
||||
|
||||
if (cursor.type().kind() == CXType_Record)
|
||||
return ToolTipInfo::ClassOrNamespace;
|
||||
|
||||
if (cursor.kind() == CXCursor_TypeAliasTemplateDecl)
|
||||
return ToolTipInfo::Typedef;
|
||||
|
||||
if (cursor.kind() == CXCursor_ClassTemplate)
|
||||
return ToolTipInfo::ClassOrNamespace;
|
||||
|
||||
return ToolTipInfo::Unknown;
|
||||
}
|
||||
|
||||
static Utf8String name(const Cursor &cursor)
|
||||
{
|
||||
if (cursor.type().kind() == CXType_Record || cursor.kind() == CXCursor_EnumDecl)
|
||||
return typeName(cursor.type());
|
||||
|
||||
return cursor.spelling();
|
||||
}
|
||||
|
||||
static Utf8StringVector qDocIdCandidates(const Cursor &cursor)
|
||||
{
|
||||
Utf8StringVector components = qualificationPrefixAsVector(cursor);
|
||||
if (components.isEmpty())
|
||||
return { name(cursor) };
|
||||
|
||||
components << name(cursor);
|
||||
Utf8StringVector result;
|
||||
Utf8String name;
|
||||
for (auto it = components.rbegin(); it != components.rend(); ++it) {
|
||||
if (name.isEmpty())
|
||||
name = *it;
|
||||
else
|
||||
name = *it + (Utf8StringLiteral("::") + name);
|
||||
|
||||
result.prepend(name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Add libclang API for this?!
|
||||
static bool isBuiltinOrPointerToBuiltin(const Type &type)
|
||||
{
|
||||
Type theType = type;
|
||||
|
||||
if (theType.isBuiltinType())
|
||||
return true;
|
||||
|
||||
// TODO: Simplify
|
||||
// TODO: Test with **
|
||||
while (theType.pointeeType().isValid()) {
|
||||
theType = theType.pointeeType();
|
||||
if (theType.isBuiltinType())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ToolTipInfo ToolTipInfoCollector::qDocInfo(const Cursor &cursor) const
|
||||
{
|
||||
ToolTipInfo result;
|
||||
|
||||
if (isBuiltinOrPointerToBuiltin(cursor.type()))
|
||||
return result;
|
||||
|
||||
result.setQdocIdCandidates(qDocIdCandidates(cursor));
|
||||
result.setQdocMark(qdocMark(cursor));
|
||||
result.setQdocCategory(qdocCategory(cursor));
|
||||
|
||||
if (cursor.type().kind() == CXType_Record) {
|
||||
result.setQdocIdCandidates(qDocIdCandidates(cursor.type().declaration()));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cursor.kind() == CXCursor_VarDecl || cursor.kind() == CXCursor_FieldDecl) {
|
||||
result.setQdocMark(typeName(cursor.type()));
|
||||
// maybe template instantiation
|
||||
if (cursor.type().kind() == CXType_Unexposed && cursor.type().canonical().kind() == CXType_Record) {
|
||||
result.setQdocIdCandidates(qDocIdCandidates(cursor.type().canonical().declaration()));
|
||||
result.setQdocCategory(ToolTipInfo::ClassOrNamespace);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle also RValueReference()
|
||||
if (cursor.type().isLValueReference()) {
|
||||
const Cursor pointeeTypeDeclaration = cursor.type().pointeeType().declaration();
|
||||
result.setQdocIdCandidates(qDocIdCandidates(pointeeTypeDeclaration));
|
||||
result.setQdocMark(pointeeTypeDeclaration.spelling());
|
||||
result.setQdocCategory(qdocCategory(pointeeTypeDeclaration));
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CXSourceLocation ToolTipInfoCollector::toCXSourceLocation(uint line, uint column) const
|
||||
{
|
||||
return clang_getLocation(m_cxTranslationUnit,
|
||||
clang_getFile(m_cxTranslationUnit,
|
||||
m_mainFilePath.constData()),
|
||||
line,
|
||||
column);
|
||||
}
|
||||
|
||||
UnsavedFile ToolTipInfoCollector::unsavedFile(const Utf8String &filePath) const
|
||||
{
|
||||
const UnsavedFile &unsavedFile = m_unsavedFiles.unsavedFile(filePath);
|
||||
if (!unsavedFile.filePath().isEmpty())
|
||||
return unsavedFile;
|
||||
|
||||
// Create an unsaved file with the file content from disk.
|
||||
// TODO: Make use of clang_getFileContents() instead of reading from disk.
|
||||
QTextCodec *codec = QTextCodec::codecForName(m_textCodecName);
|
||||
QByteArray fileContent;
|
||||
QString errorString;
|
||||
using namespace Utils;
|
||||
const TextFileFormat::ReadResult readResult
|
||||
= TextFileFormat::readFileUTF8(filePath.toString(), codec, &fileContent, &errorString);
|
||||
if (readResult != TextFileFormat::ReadSuccess) {
|
||||
qWarning() << "Failed to read file" << filePath << ":" << errorString;
|
||||
return UnsavedFile();
|
||||
}
|
||||
|
||||
return UnsavedFile(filePath, Utf8String::fromByteArray(fileContent));
|
||||
}
|
||||
|
||||
Utf8String ToolTipInfoCollector::lineRange(const Utf8String &filePath,
|
||||
unsigned fromLine,
|
||||
unsigned toLine) const
|
||||
{
|
||||
if (toLine < fromLine)
|
||||
return Utf8String();
|
||||
|
||||
const UnsavedFile file = unsavedFile(filePath);
|
||||
if (file.fileContent().isEmpty())
|
||||
return Utf8String();
|
||||
|
||||
return file.lineRange(fromLine, toLine);
|
||||
}
|
||||
|
||||
ToolTipInfo ToolTipInfoCollector::collect(uint line, uint column) const
|
||||
{
|
||||
const Cursor cursor = clang_getCursor(m_cxTranslationUnit, toCXSourceLocation(line, column));
|
||||
if (!cursor.isValid())
|
||||
return ToolTipInfo(); // E.g. cursor on ifdeffed out range
|
||||
|
||||
const Cursor referenced = referencedCursor(cursor);
|
||||
QTC_CHECK(referenced.isValid());
|
||||
|
||||
ToolTipInfo info;
|
||||
info.setText(text(cursor, referenced));
|
||||
info.setBriefComment(referenced.briefComment());
|
||||
|
||||
{
|
||||
ToolTipInfo qDocToolTipInfo = qDocInfo(referenced);
|
||||
info.setQdocIdCandidates(qDocToolTipInfo.qdocIdCandidates());
|
||||
info.setQdocMark(qDocToolTipInfo.qdocMark());
|
||||
info.setQdocCategory(qDocToolTipInfo.qdocCategory());
|
||||
}
|
||||
|
||||
info.setSizeInBytes(sizeInBytes(cursor));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ToolTipInfo collectToolTipInfo(UnsavedFiles &unsavedFiles,
|
||||
const Utf8String &textCodecName,
|
||||
const Utf8String &mainFilePath,
|
||||
CXTranslationUnit cxTranslationUnit,
|
||||
uint line,
|
||||
uint column)
|
||||
{
|
||||
ToolTipInfoCollector collector(unsavedFiles, textCodecName, mainFilePath, cxTranslationUnit);
|
||||
const ToolTipInfo info = collector.collect(line, column);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace ClangBackEnd
|
||||
Reference in New Issue
Block a user