From 138066792e61925b827d9d722669b8d96bc0a87b Mon Sep 17 00:00:00 2001 From: Nikolai Kosjar <nikolai.kosjar@digia.com> Date: Mon, 14 Jan 2013 14:45:36 +0100 Subject: [PATCH] C++: Introduce PointerDeclarationFormatter For a given AST, CppRefactoringFile and Overview this will create a ChangeSet for rewriting the pointer or reference declaration according to the Overview. Task-number: QTCREATORBUG-6169 Change-Id: If6f824c1ea5e9f53a11a58ec8b6d696d01f0723e Reviewed-by: Erik Verbruggen <erik.verbruggen@digia.com> --- src/libs/3rdparty/cplusplus/Bind.cpp | 7 + src/libs/3rdparty/cplusplus/Bind.h | 1 + src/libs/cplusplus/CppDocument.cpp | 2 + src/libs/cplusplus/Overview.h | 3 - .../cpppointerdeclarationformatter.cpp | 417 +++++++++++++ .../cpptools/cpppointerdeclarationformatter.h | 122 ++++ .../cpppointerdeclarationformatter_test.cpp | 577 ++++++++++++++++++ src/plugins/cpptools/cpptools.pro | 9 +- src/plugins/cpptools/cpptools.qbs | 5 +- src/plugins/cpptools/cpptoolsplugin.h | 12 +- 10 files changed, 1146 insertions(+), 9 deletions(-) create mode 100644 src/plugins/cpptools/cpppointerdeclarationformatter.cpp create mode 100644 src/plugins/cpptools/cpppointerdeclarationformatter.h create mode 100644 src/plugins/cpptools/cpppointerdeclarationformatter_test.cpp diff --git a/src/libs/3rdparty/cplusplus/Bind.cpp b/src/libs/3rdparty/cplusplus/Bind.cpp index 909ef24e78e..98310f9ee8b 100644 --- a/src/libs/3rdparty/cplusplus/Bind.cpp +++ b/src/libs/3rdparty/cplusplus/Bind.cpp @@ -181,6 +181,13 @@ void Bind::operator()(DeclarationAST *ast, Scope *scope) (void) switchScope(previousScope); } +void Bind::operator()(StatementAST *ast, Scope *scope) +{ + Scope *previousScope = switchScope(scope); + statement(ast); + (void) switchScope(previousScope); +} + FullySpecifiedType Bind::operator()(ExpressionAST *ast, Scope *scope) { Scope *previousScope = switchScope(scope); diff --git a/src/libs/3rdparty/cplusplus/Bind.h b/src/libs/3rdparty/cplusplus/Bind.h index 93022b9af77..ca2c6084ba1 100644 --- a/src/libs/3rdparty/cplusplus/Bind.h +++ b/src/libs/3rdparty/cplusplus/Bind.h @@ -34,6 +34,7 @@ public: void operator()(TranslationUnitAST *ast, Namespace *globalNamespace); void operator()(DeclarationAST *ast, Scope *scope); + void operator()(StatementAST *ast, Scope *scope); FullySpecifiedType operator()(ExpressionAST *ast, Scope *scope); FullySpecifiedType operator()(NewTypeIdAST *ast, Scope *scope); diff --git a/src/libs/cplusplus/CppDocument.cpp b/src/libs/cplusplus/CppDocument.cpp index 2069dd9dc3e..ed8b0a6699e 100644 --- a/src/libs/cplusplus/CppDocument.cpp +++ b/src/libs/cplusplus/CppDocument.cpp @@ -595,6 +595,8 @@ void Document::check(CheckMode mode) if (TranslationUnitAST *ast = _translationUnit->ast()->asTranslationUnit()) semantic(ast, _globalNamespace); + else if (StatementAST *ast = _translationUnit->ast()->asStatement()) + semantic(ast, _globalNamespace); else if (ExpressionAST *ast = _translationUnit->ast()->asExpression()) semantic(ast, _globalNamespace); else if (DeclarationAST *ast = translationUnit()->ast()->asDeclaration()) diff --git a/src/libs/cplusplus/Overview.h b/src/libs/cplusplus/Overview.h index 339a6d8fc7a..405bbb2b461 100644 --- a/src/libs/cplusplus/Overview.h +++ b/src/libs/cplusplus/Overview.h @@ -49,9 +49,6 @@ namespace CPlusPlus { class CPLUSPLUS_EXPORT Overview { - Overview(const Overview &other); - void operator =(const Overview &other); - public: Overview(); diff --git a/src/plugins/cpptools/cpppointerdeclarationformatter.cpp b/src/plugins/cpptools/cpppointerdeclarationformatter.cpp new file mode 100644 index 00000000000..ba0fa4c60fa --- /dev/null +++ b/src/plugins/cpptools/cpppointerdeclarationformatter.cpp @@ -0,0 +1,417 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + + +#include "cpppointerdeclarationformatter.h" + +#include <AST.h> + +#include <QTextCursor> + +#define DEBUG_OUTPUT 0 +#define CHECK_RV(cond, err, r) if (!(cond)) { if (DEBUG_OUTPUT) qDebug() << (err); return r; } +#define CHECK_R(cond, err) if (!(cond)) { if (DEBUG_OUTPUT) qDebug() << (err); return; } +#define CHECK_C(cond, err) if (!(cond)) { if (DEBUG_OUTPUT) qDebug() << (err); continue; } + +using namespace CppTools; + +/*! + \brief Skip not type relevant specifiers and return the index of the + first specifier token which is not followed by __attribute__ + ((T___ATTRIBUTE__)). + + This is used to get 'correct' start of the activation range in + simple declarations. + + Consider these cases: + + static char *s = 0; + typedef char *s cp; + __attribute__((visibility("default"))) char *f(); + + For all cases we want to skip all the not type relevant specifer + (since these are not part of the type and thus are not rewritten). + + \param list The specifier list to iterate + \param translationUnit The TranslationUnit + \param endToken Do not check further than this token + \param found Output parameter, must not be 0. + */ +static unsigned firstTypeSpecifierWithoutFollowingAttribute( + SpecifierListAST *list, TranslationUnit *translationUnit, unsigned endToken, bool *found) +{ + *found = false; + if (! list || ! translationUnit || ! endToken) + return 0; + + for (SpecifierListAST *it = list; it; it = it->next) { + SpecifierAST *specifier = it->value; + CHECK_RV(specifier, "No specifier", 0); + const unsigned index = specifier->firstToken(); + CHECK_RV(index < endToken, "EndToken reached", 0); + + const int tokenKind = translationUnit->tokenKind(index); + switch (tokenKind) { + case T_VIRTUAL: + case T_INLINE: + case T_FRIEND: + case T_REGISTER: + case T_STATIC: + case T_EXTERN: + case T_MUTABLE: + case T_TYPEDEF: + case T_CONSTEXPR: + case T___ATTRIBUTE__: + continue; + default: + // Check if attributes follow + for (unsigned i = index; i <= endToken; ++i) { + const int tokenKind = translationUnit->tokenKind(i); + if (tokenKind == T___ATTRIBUTE__) + return 0; + } + *found = true; + return index; + } + } + + return 0; +} + +PointerDeclarationFormatter::PointerDeclarationFormatter( + const CppRefactoringFilePtr refactoringFile, + const Overview &overview, + CursorHandling cursorHandling) + : ASTVisitor(refactoringFile->cppDocument()->translationUnit()) + , m_cppRefactoringFile(refactoringFile) + , m_overview(overview) + , m_cursorHandling(cursorHandling) +{} + +/*! + Handle + (1) Simple declarations like in "char *s, *t, *int foo();" + (2) Return types of function declarations. + */ +bool PointerDeclarationFormatter::visit(SimpleDeclarationAST *ast) +{ + CHECK_RV(ast, "Invalid AST", true); + + DeclaratorListAST *declaratorList = ast->declarator_list; + CHECK_RV(declaratorList, "No declarator list", true); + DeclaratorAST *firstDeclarator = declaratorList->value; + CHECK_RV(firstDeclarator, "No declarator", true); + CHECK_RV(ast->symbols, "No Symbols", true); + CHECK_RV(ast->symbols->value, "No Symbol", true); + + List<Symbol *> *sit = ast->symbols; + DeclaratorListAST *dit = declaratorList; + for (; sit && dit; sit = sit->next, dit = dit->next) { + DeclaratorAST *declarator = dit->value; + Symbol *symbol = sit->value; + + const bool isFirstDeclarator = declarator == firstDeclarator; + + // If were not handling the first declarator, we need to remove + // characters from the beginning since our rewritten declaration + // will contain all type specifiers. + int charactersToRemove = 0; + if (! isFirstDeclarator) { + const int startAST = m_cppRefactoringFile->startOf(ast); + const int startFirstDeclarator = m_cppRefactoringFile->startOf(firstDeclarator); + CHECK_RV(startAST < startFirstDeclarator, "No specifier", true); + charactersToRemove = startFirstDeclarator - startAST; + } + + // Specify activation range + int lastActivationToken = 0; + Range range; + // (2) Handle function declaration's return type + if (symbol->type()->asFunctionType()) { + PostfixDeclaratorListAST *pfDeclaratorList = declarator->postfix_declarator_list; + CHECK_RV(pfDeclaratorList, "No postfix declarator list", true); + PostfixDeclaratorAST *pfDeclarator = pfDeclaratorList->value; + CHECK_RV(pfDeclarator, "No postfix declarator", true); + FunctionDeclaratorAST *functionDeclarator = pfDeclarator->asFunctionDeclarator(); + CHECK_RV(functionDeclarator, "No function declarator", true); + // End the activation range before the '(' token. + lastActivationToken = functionDeclarator->lparen_token - 1; + + SpecifierListAST *specifierList = isFirstDeclarator + ? ast->decl_specifier_list + : declarator->attribute_list; + + unsigned firstActivationToken = 0; + bool foundBegin = false; + firstActivationToken = firstTypeSpecifierWithoutFollowingAttribute( + specifierList, + m_cppRefactoringFile->cppDocument()->translationUnit(), + lastActivationToken, + &foundBegin); + if (! foundBegin) { + CHECK_RV(! isFirstDeclarator, "Declaration without attributes not supported", true); + firstActivationToken = declarator->firstToken(); + } + + range.start = m_cppRefactoringFile->startOf(firstActivationToken); + + // (1) Handle 'normal' declarations. + } else { + if (isFirstDeclarator) { + bool foundBegin = false; + unsigned firstActivationToken = firstTypeSpecifierWithoutFollowingAttribute( + ast->decl_specifier_list, + m_cppRefactoringFile->cppDocument()->translationUnit(), + declarator->firstToken(), + &foundBegin); + CHECK_RV(foundBegin, "Declaration without attributes not supported", true); + range.start = m_cppRefactoringFile->startOf(firstActivationToken); + } else { + range.start = m_cppRefactoringFile->startOf(declarator); + } + lastActivationToken = declarator->equal_token + ? declarator->equal_token - 1 + : declarator->lastToken() - 1; + } + range.end = m_cppRefactoringFile->endOf(lastActivationToken); + + checkAndRewrite(symbol, range, charactersToRemove); + } + return true; +} + +/*! Handle return types of function definitions */ +bool PointerDeclarationFormatter::visit(FunctionDefinitionAST *ast) +{ + DeclaratorAST *declarator = ast->declarator; + CHECK_RV(declarator, "No declarator", true); + CHECK_RV(declarator->ptr_operator_list, "No Pointer or references", true); + Symbol *symbol = ast->symbol; + + PostfixDeclaratorListAST *pfDeclaratorList = declarator->postfix_declarator_list; + CHECK_RV(pfDeclaratorList, "No postfix declarator list", true); + PostfixDeclaratorAST *pfDeclarator = pfDeclaratorList->value; + CHECK_RV(pfDeclarator, "No postfix declarator", true); + FunctionDeclaratorAST *functionDeclarator = pfDeclarator->asFunctionDeclarator(); + CHECK_RV(functionDeclarator, "No function declarator", true); + + // Specify activation range + bool foundBegin = false; + const unsigned lastActivationToken = functionDeclarator->lparen_token - 1; + const unsigned firstActivationToken = firstTypeSpecifierWithoutFollowingAttribute( + ast->decl_specifier_list, + m_cppRefactoringFile->cppDocument()->translationUnit(), + lastActivationToken, + &foundBegin); + CHECK_RV(foundBegin, "Declaration without attributes not supported", true); + Range range(m_cppRefactoringFile->startOf(firstActivationToken), + m_cppRefactoringFile->endOf(lastActivationToken)); + + checkAndRewrite(symbol, range); + return true; +} + +/*! Handle parameters in function declarations and definitions */ +bool PointerDeclarationFormatter::visit(ParameterDeclarationAST *ast) +{ + DeclaratorAST *declarator = ast->declarator; + CHECK_RV(declarator, "No declarator", true); + CHECK_RV(declarator->ptr_operator_list, "No Pointer or references", true); + Symbol *symbol = ast->symbol; + + // Specify activation range + const int lastActivationToken = ast->equal_token + ? ast->equal_token - 1 + : ast->lastToken() - 1; + Range range(m_cppRefactoringFile->startOf(ast), + m_cppRefactoringFile->endOf(lastActivationToken)); + + checkAndRewrite(symbol, range); + return true; +} + +/*! Handle declaration in foreach statement */ +bool PointerDeclarationFormatter::visit(ForeachStatementAST *ast) +{ + DeclaratorAST *declarator = ast->declarator; + CHECK_RV(declarator, "No declarator", true); + CHECK_RV(declarator->ptr_operator_list, "No Pointer or references", true); + CHECK_RV(ast->type_specifier_list, "No type specifier", true); + SpecifierAST *firstSpecifier = ast->type_specifier_list->value; + CHECK_RV(firstSpecifier, "No first type specifier", true); + CHECK_RV(ast->symbol, "No symbol", true); + Symbol *symbol = ast->symbol->memberAt(0); + + // Specify activation range + const int lastActivationToken = declarator->equal_token + ? declarator->equal_token - 1 + : declarator->lastToken() - 1; + Range range(m_cppRefactoringFile->startOf(firstSpecifier), + m_cppRefactoringFile->endOf(lastActivationToken)); + + checkAndRewrite(symbol, range); + return true; +} + +bool PointerDeclarationFormatter::visit(IfStatementAST *ast) +{ + processIfWhileForStatement(ast->condition, ast->symbol); + return true; +} + +bool PointerDeclarationFormatter::visit(WhileStatementAST *ast) +{ + processIfWhileForStatement(ast->condition, ast->symbol); + return true; +} + +bool PointerDeclarationFormatter::visit(ForStatementAST *ast) +{ + processIfWhileForStatement(ast->condition, ast->symbol); + return true; +} + +/*! Handle declaration in if, while and for statements */ +void PointerDeclarationFormatter::processIfWhileForStatement(ExpressionAST *expression, + Symbol *statementSymbol) +{ + CHECK_R(expression, "No expression"); + CHECK_R(statementSymbol, "No symbol"); + + ConditionAST *condition = expression->asCondition(); + CHECK_R(condition, "No condition"); + DeclaratorAST *declarator = condition->declarator; + CHECK_R(declarator, "No declarator"); + CHECK_R(declarator->ptr_operator_list, "No Pointer or references"); + CHECK_R(declarator->equal_token, "No equal token"); + Block *block = statementSymbol->asBlock(); + CHECK_R(block, "No block"); + CHECK_R(block->memberCount() > 0, "No block members"); + + // Get the right symbol + // + // This is especially important for e.g. + // + // for (char *s = 0; char *t = 0;) {} + // + // The declaration for 's' will be handled in visit(SimpleDeclarationAST *ast), + // so handle declaration for 't' here. + Scope::iterator it = block->lastMember() - 1; + Symbol *symbol = *it; + if (symbol && symbol->asScope()) { // True if there is a "{ ... }" following. + --it; + symbol = *it; + } + + // Specify activation range + Range range(m_cppRefactoringFile->startOf(condition), + m_cppRefactoringFile->endOf(declarator->equal_token - 1)); + + checkAndRewrite(symbol, range); +} + +/*! + \brief Do some further checks and rewrite the symbol's type and + name into the given range + + \param symbol the symbol to be rewritten + \param range the substitution range in the file + */ +void PointerDeclarationFormatter::checkAndRewrite(Symbol *symbol, Range range, + unsigned charactersToRemove) +{ + CHECK_R(range.start >= 0 && range.end > 0, "Range invalid"); + CHECK_R(range.start < range.end, "Range invalid"); + CHECK_R(symbol, "No symbol"); + + // Check range with respect to cursor position / selection + if (m_cursorHandling == RespectCursor) { + const QTextCursor cursor = m_cppRefactoringFile->cursor(); + if (cursor.hasSelection()) { + CHECK_R(cursor.selectionStart() <= range.start, "Change not in selection range"); + CHECK_R(range.end <= cursor.selectionEnd(), "Change not in selection range"); + } else { + CHECK_R(range.start <= cursor.selectionStart(), "Cursor before activation range"); + CHECK_R(cursor.selectionEnd() <= range.end, "Cursor after activation range"); + } + } + + FullySpecifiedType type = symbol->type(); + if (Function *function = type->asFunctionType()) + type = function->returnType(); + + // Check if pointers or references are involved + const QString originalDeclaration = m_cppRefactoringFile->textOf(range); + CHECK_R(originalDeclaration.contains(QLatin1Char('&')) + || originalDeclaration.contains(QLatin1Char('*')), "No pointer or references"); + + // Does the rewritten declaration (part) differs from the original source (part)? + QString rewrittenDeclaration = rewriteDeclaration(type, symbol->name()); + rewrittenDeclaration.remove(0, charactersToRemove); + CHECK_R(originalDeclaration != rewrittenDeclaration, "Rewritten is same as original"); + + if (DEBUG_OUTPUT) { + qDebug("Rewritten: \"%s\" --> \"%s\"", originalDeclaration.toLatin1().constData(), + rewrittenDeclaration.toLatin1().constData()); + } + + // Creating the replacement in the changeset may fail due to operations + // in the changeset that overlap with the current range. + // + // Consider this case: + // + // void (*foo)(char * s) = 0; + // + // First visit(SimpleDeclarationAST *ast) will be called. It creates a + // replacement that also includes the parameter. + // Next visit(ParameterDeclarationAST *ast) is called with the + // original source. It tries to create an replacement operation + // at this position and fails due to overlapping ranges (the + // simple declaration range includes parameter declaration range). + ChangeSet change(m_changeSet); + if (change.replace(range, rewrittenDeclaration)) + m_changeSet = change; + else if (DEBUG_OUTPUT) + qDebug() << "Replacement operation failed"; +} + +/*! Rewrite/format the given type and name. */ +QString PointerDeclarationFormatter::rewriteDeclaration(FullySpecifiedType type, const Name *name) + const +{ + CHECK_RV(type.isValid(), "Invalid type", QString()); + + const char *identifier = 0; + if (const Name *declarationName = name) { + if (const Identifier *id = declarationName->identifier()) + identifier = id->chars(); + } + + return m_overview.prettyType(type, QLatin1String(identifier)); +} diff --git a/src/plugins/cpptools/cpppointerdeclarationformatter.h b/src/plugins/cpptools/cpppointerdeclarationformatter.h new file mode 100644 index 00000000000..214e72396c3 --- /dev/null +++ b/src/plugins/cpptools/cpppointerdeclarationformatter.h @@ -0,0 +1,122 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef CPPPOINTERFORMATTER_H +#define CPPPOINTERFORMATTER_H + +#include "cpptools_global.h" + +#include <AST.h> +#include <ASTVisitor.h> +#include <Overview.h> +#include <Symbols.h> + +#include <cpptools/cpprefactoringchanges.h> +#include <utils/changeset.h> + +namespace CppTools { + +using namespace CPlusPlus; +using namespace CppTools; +using Utils::ChangeSet; + +typedef Utils::ChangeSet::Range Range; + +/*! + \class CppTools::PointerDeclarationFormatter + + \brief Rewrite pointer or reference declarations accordingly to an Overview. + + The following constructs are supported: + \list + \o Simple declarations + \o Parameters and return types of function declarations and definitions + \o Control flow statements like if, while, for, foreach + \endlist +*/ + +class CPPTOOLS_EXPORT PointerDeclarationFormatter: protected ASTVisitor +{ +public: + /*! + \enum PointerDeclarationFormatter::CursorHandling + + This simplifies the QuickFix implementation. + + \value RespectCursor + Consider the cursor position / selection of the CppRefactoringFile + for rejecting edit operation candidates for the resulting ChangeSet. + If there is a selection, the range of the edit operation candidate + should be inside the selection. If there is no selection, the cursor + position should be within the range of the edit operation candidate. + \value IgnoreCursor + Cursor position or selection of the CppRefactoringFile will + _not_ be considered for aborting. + */ + enum CursorHandling { RespectCursor, IgnoreCursor }; + + explicit PointerDeclarationFormatter(const CppRefactoringFilePtr refactoringFile, + const Overview &overview, + CursorHandling cursorHandling = IgnoreCursor); + + /*! + Returns a ChangeSet for applying the formatting changes. + The ChangeSet is empty if it was not possible to rewrite anything. + */ + ChangeSet format(AST *ast) + { + if (ast) + accept(ast); + return m_changeSet; + } + +protected: + bool visit(SimpleDeclarationAST *ast); + bool visit(FunctionDefinitionAST *ast); + bool visit(ParameterDeclarationAST *ast); + bool visit(IfStatementAST *ast); + bool visit(WhileStatementAST *ast); + bool visit(ForStatementAST *ast); + bool visit(ForeachStatementAST *ast); + +private: + void processIfWhileForStatement(ExpressionAST *expression, Symbol *symbol); + void checkAndRewrite(Symbol *symbol, Range range, unsigned charactersToRemove = 0); + QString rewriteDeclaration(FullySpecifiedType type, const Name *name) const; + + const CppRefactoringFilePtr m_cppRefactoringFile; + const Overview &m_overview; + const CursorHandling m_cursorHandling; + + ChangeSet m_changeSet; +}; + +} // namespace CppTools + +#endif // CPPPOINTERFORMATTER_H diff --git a/src/plugins/cpptools/cpppointerdeclarationformatter_test.cpp b/src/plugins/cpptools/cpppointerdeclarationformatter_test.cpp new file mode 100644 index 00000000000..dbffa14c68e --- /dev/null +++ b/src/plugins/cpptools/cpppointerdeclarationformatter_test.cpp @@ -0,0 +1,577 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "cpptoolsplugin.h" + +#include <AST.h> +#include <CppDocument.h> +#include <Symbols.h> +#include <TranslationUnit.h> +#include <pp.h> + +#include <cpptools/cpppointerdeclarationformatter.h> +#include <cpptools/cpprefactoringchanges.h> +#include <cpptools/cpptoolsplugin.h> +#include <texteditor/plaintexteditor.h> +#include <utils/changeset.h> +#include <utils/fileutils.h> + +#include <QDebug> +#include <QDir> +#include <QTextCursor> +#include <QTextDocument> +#include <QtTest> + +using namespace CPlusPlus; +using namespace CppTools; +using namespace CppTools::Internal; + +using Utils::ChangeSet; +typedef Utils::ChangeSet::Range Range; + +Q_DECLARE_METATYPE(Overview) + +static QString stripCursor(const QString &source) +{ + QString copy(source); + const int pos = copy.indexOf(QLatin1Char('@')); + if (pos != -1) + copy.remove(pos, 1); + else + qDebug() << Q_FUNC_INFO << "Warning: No cursor marker to strip."; + return copy; +} + +struct TestEnvironment +{ + QByteArray source; + Snapshot snapshot; + CppRefactoringFilePtr cppRefactoringFile; + TextEditor::BaseTextEditorWidget *editor; + Document::Ptr document; + QTextDocument *textDocument; + TranslationUnit *translationUnit; + Environment env; + + TestEnvironment(const QByteArray &source, Document::ParseMode parseMode) + { + // Find cursor position and remove cursor marker '@' + int cursorPosition = 0; + QString sourceWithoutCursorMarker = QLatin1String(source); + const int pos = sourceWithoutCursorMarker.indexOf(QLatin1Char('@')); + if (pos != -1) { + sourceWithoutCursorMarker.remove(pos, 1); + cursorPosition = pos; + } + + // Write source to temprorary file + const QString filePath = QDir::tempPath() + QLatin1String("/file.h"); + document = Document::create(filePath); + Utils::FileSaver documentSaver(document->fileName()); + documentSaver.write(sourceWithoutCursorMarker.toLatin1()); + documentSaver.finalize(); + + // Preprocess source + Preprocessor preprocess(0, &env); + const QByteArray preprocessedSource = preprocess.run(filePath, sourceWithoutCursorMarker); + + document->setUtf8Source(preprocessedSource); + document->parse(parseMode); + document->check(); + translationUnit = document->translationUnit(); + snapshot.insert(document); + editor = new TextEditor::PlainTextEditorWidget(0); + QString error; + editor->open(&error, document->fileName(), document->fileName()); + + // Set cursor position + QTextCursor cursor = editor->textCursor(); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, cursorPosition); + editor->setTextCursor(cursor); + + textDocument = editor->document(); + cppRefactoringFile = CppRefactoringChanges::file(editor, document); + } + + void applyFormatting(AST *ast, PointerDeclarationFormatter::CursorHandling cursorHandling) + { + Overview overview; + overview.showReturnTypes = true; + overview.showArgumentNames = true; + overview.starBindFlags = Overview::StarBindFlags(0); + + // Run the formatter + PointerDeclarationFormatter formatter(cppRefactoringFile, overview, cursorHandling); + ChangeSet change = formatter.format(ast); // ChangeSet may be empty. + + // Apply change + QTextCursor cursor(textDocument); + change.apply(&cursor); + } +}; + +void CppToolsPlugin::test_format_pointerdeclaration_in_simpledeclarations() +{ + QFETCH(QString, source); + QFETCH(QString, reformattedSource); + + TestEnvironment env(source.toLatin1(), Document::ParseDeclaration); + AST *ast = env.translationUnit->ast(); + QVERIFY(ast); + + env.applyFormatting(ast, PointerDeclarationFormatter::RespectCursor); + + QCOMPARE(env.textDocument->toPlainText(), reformattedSource); +} + +void CppToolsPlugin::test_format_pointerdeclaration_in_simpledeclarations_data() +{ + QTest::addColumn<QString>("source"); + QTest::addColumn<QString>("reformattedSource"); + + QString source; + + // + // Naming scheme for the test cases: <description>_(in|out)-(start|middle|end) + // in/out: + // Denotes whether the cursor is in or out of the quickfix activation range + // start/middle/end: + // Denotes whether is cursor is near to the range start, middle or end + // + + QTest::newRow("basic_in-start") + << "@char *s;" + << "char * s;"; + + source = QLatin1String("char *s;@"); + QTest::newRow("basic_out-end") + << source << stripCursor(source); + + QTest::newRow("basic_in-end") + << "char *s@;" + << "char * s;"; + + QTest::newRow("basic-with-ws_in-start") + << "\n @char *s; " // Add some whitespace to check position detection. + << "\n char * s; "; + + source = QLatin1String("\n @ char *s; "); + QTest::newRow("basic-with-ws_out-start") + << source << stripCursor(source); + + QTest::newRow("basic-with-const_in-start") + << "@const char *s;" + << "const char * s;"; + + QTest::newRow("basic-with-const-and-init-value_in-end") + << "const char *s@ = 0;" + << "const char * s = 0;"; + + source = QLatin1String("const char *s @= 0;"); + QTest::newRow("basic-with-const-and-init-value_out-end") + << source << stripCursor(source); + + QTest::newRow("first-declarator_in-start") + << "@const char *s, *t;" + << "const char * s, *t;"; + + QTest::newRow("first-declarator_in-end") + << "const char *s@, *t;" + << "const char * s, *t;"; + + source = QLatin1String("const char *s,@ *t;"); + QTest::newRow("first-declarator_out-end") + << source << stripCursor(source); + + QTest::newRow("function-declaration-param_in-start") + << "int f(@char *s);" + << "int f(char * s);"; + + QTest::newRow("function-declaration-param_in-end") + << "int f(char *s@);" + << "int f(char * s);"; + + QTest::newRow("function-declaration-param_in-end") + << "int f(char *s@ = 0);" + << "int f(char * s = 0);"; + + QTest::newRow("function-declaration-param-multiple-params_in-end") + << "int f(char *s@, int);" + << "int f(char * s, int);"; + + source = QLatin1String("int f(char *s)@;"); + QTest::newRow("function-declaration-param_out-end") + << source << stripCursor(source); + + source = QLatin1String("int f@(char *s);"); + QTest::newRow("function-declaration-param_out-start") + << source << stripCursor(source); + + source = QLatin1String("int f(char *s =@ 0);"); + QTest::newRow("function-declaration-param_out-end") + << source << stripCursor(source); + + // Function definitions are handled by the same code as function + // declarations, so just check a minimal example. + QTest::newRow("function-definition-param_in-middle") + << "int f(char @*s) {}" + << "int f(char * s) {}"; + + QTest::newRow("function-declaration-returntype_in-start") + << "@char *f();" + << "char * f();"; + + QTest::newRow("function-declaration-returntype_in-end") + << "char *f@();" + << "char * f();"; + + source = QLatin1String("char *f(@);"); + QTest::newRow("function-declaration-returntype_out-end") + << source << stripCursor(source); + + QTest::newRow("function-definition-returntype_in-end") + << "char *f@() {}" + << "char * f() {}"; + + QTest::newRow("function-definition-returntype_in-start") + << "@char *f() {}" + << "char * f() {}"; + + source = QLatin1String("char *f(@) {}"); + QTest::newRow("function-definition-returntype_out-end") + << source << stripCursor(source); + + source = QLatin1String("inline@ char *f() {}"); + QTest::newRow("function-definition-returntype-inline_out-start") + << source << stripCursor(source); + + // Same code is also used for for other specifiers like virtual, inline, friend, ... + QTest::newRow("function-definition-returntype-static-inline_in-start") + << "static inline @char *f() {}" + << "static inline char * f() {}"; + + QTest::newRow("function-declaration-returntype-static-inline_in-start") + << "static inline @char *f();" + << "static inline char * f();"; + + source = QLatin1String("static inline@ char *f() {}"); + QTest::newRow("function-definition-returntype-static-inline_out-start") + << source << stripCursor(source); + + QTest::newRow("function-declaration-returntype-static-custom-type_in-start") + << "static @CustomType *f();" + << "static CustomType * f();"; + + source = QLatin1String("static@ CustomType *f();"); + QTest::newRow("function-declaration-returntype-static-custom-type_out-start") + << source << stripCursor(source); + + QTest::newRow("function-declaration-returntype-symbolvisibilityattributes_in-start") + << "__attribute__((visibility(\"default\"))) @char *f();" + << "__attribute__((visibility(\"default\"))) char * f();"; + + source = QLatin1String("@__attribute__((visibility(\"default\"))) char *f();"); + QTest::newRow("function-declaration-returntype-symbolvisibilityattributes_out-start") + << source << stripCursor(source); + + // The following two are not supported at the moment. + source = QLatin1String("@char * __attribute__((visibility(\"default\"))) f();"); + QTest::newRow("function-declaration-returntype-symbolvisibilityattributes_out-start") + << source << stripCursor(source); + + source = QLatin1String("@char * __attribute__((visibility(\"default\"))) f() {}"); + QTest::newRow("function-definition-returntype-symbolvisibilityattributes_out-start") + << source << stripCursor(source); + + // NOTE: Since __declspec() is not parsed (but defined to nothing), + // we can't detect it properly. Therefore, we fail on code with + // FOO_EXPORT macros with __declspec() for pointer return types; + + QTest::newRow("typedef_in-start") + << "typedef @char *s;" + << "typedef char * s;"; + + source = QLatin1String("@typedef char *s;"); + QTest::newRow("typedef_out-start") + << source << stripCursor(source); + + QTest::newRow("static_in-start") + << "static @char *s;" + << "static char * s;"; + + QTest::newRow("pointerToFunction_in-start") + << "int (*bar)(@char *s);" + << "int (*bar)(char * s);"; + + source = QLatin1String("int (@*bar)();"); + QTest::newRow("pointerToFunction_in-start") + << source << stripCursor(source); + + source = QLatin1String("int (@*bar)[] = 0;"); + QTest::newRow("pointerToArray_in-start") + << source << stripCursor(source); + + // + // Additional test cases that does not fit into the naming scheme + // + + // The following case is a side effect of the reformatting. Though + // the pointer type is according to the overview, the double space + // before 'char' gets corrected. + QTest::newRow("remove-extra-whitespace") + << "@const char * s = 0;" + << "const char * s = 0;"; + + // Nothing to pretty print since no pointer or references are involved. + source = QLatin1String("@char bla;"); // Two spaces to get sure nothing is reformatted. + QTest::newRow("precondition-fail-no-pointer") + << source << stripCursor(source); +} + +void CppToolsPlugin::test_format_pointerdeclaration_in_controlflowstatements() +{ + QFETCH(QString, source); + QFETCH(QString, reformattedSource); + + TestEnvironment env(source.toLatin1(), Document::ParseStatement); + AST *ast = env.translationUnit->ast(); + QVERIFY(ast); + + env.applyFormatting(ast, PointerDeclarationFormatter::RespectCursor); + + QCOMPARE(env.textDocument->toPlainText(), reformattedSource); +} + +void CppToolsPlugin::test_format_pointerdeclaration_in_controlflowstatements_data() +{ + QTest::addColumn<QString>("source"); + QTest::addColumn<QString>("reformattedSource"); + + QString source; + + // + // Same naming scheme as in test_format_pointerdeclaration_in_simpledeclarations_data() + // + + QTest::newRow("if_in-start") + << "if (@char *s = 0);" + << "if (char * s = 0);"; + + source = QLatin1String("if @(char *s = 0);"); + QTest::newRow("if_out-start") + << source << stripCursor(source); + + QTest::newRow("if_in-end") + << "if (char *s@ = 0);" + << "if (char * s = 0);"; + + source = QLatin1String("if (char *s @= 0);"); + QTest::newRow("if_out-end") + << source << stripCursor(source); + + // if and for statements are handled by the same code, so just + // check minimal examples for these + QTest::newRow("while") + << "while (@char *s = 0);" + << "while (char * s = 0);"; + + QTest::newRow("for") + << "for (;@char *s = 0;);" + << "for (;char * s = 0;);"; + + // Should also work since it's a simple declaration + // for (char *s = 0; true;); + + QTest::newRow("foreach_in-start") + << "foreach (@char *s, list);" + << "foreach (char * s, list);"; + + source = QLatin1String("foreach @(char *s, list);"); + QTest::newRow("foreach-out-start") + << source << stripCursor(source); + + QTest::newRow("foreach_in_end") + << "foreach (const char *s@, list);" + << "foreach (const char * s, list);"; + + source = QLatin1String("foreach (const char *s,@ list);"); + QTest::newRow("foreach_out_end") + << source << stripCursor(source); + + // + // Additional test cases that does not fit into the naming scheme + // + + source = QLatin1String("@if (char s = 0);"); // Two spaces to get sure nothing is reformatted. + QTest::newRow("precondition-fail-no-pointer") << source << stripCursor(source); +} + +void CppToolsPlugin::test_format_pointerdeclaration_multiple_declarators() +{ + QFETCH(QString, source); + QFETCH(QString, reformattedSource); + + TestEnvironment env(source.toLatin1(), Document::ParseDeclaration); + AST *ast = env.translationUnit->ast(); + QVERIFY(ast); + + env.applyFormatting(ast, PointerDeclarationFormatter::RespectCursor); + + QCOMPARE(env.textDocument->toPlainText(), reformattedSource); +} + +void CppToolsPlugin::test_format_pointerdeclaration_multiple_declarators_data() +{ + QTest::addColumn<QString>("source"); + QTest::addColumn<QString>("reformattedSource"); + + QString source; + + QTest::newRow("function-declaration_in-start") + << "char *s = 0, @*f(int i) = 0;" + << "char *s = 0, * f(int i) = 0;"; + + QTest::newRow("non-pointer-before_in-start") + << "char c, @*t;" + << "char c, * t;"; + + QTest::newRow("pointer-before_in-start") + << "char *s, @*t;" + << "char *s, * t;"; + + QTest::newRow("pointer-before_in-end") + << "char *s, *t@;" + << "char *s, * t;"; + + source = QLatin1String("char *s,@ *t;"); + QTest::newRow("md1-out_start") + << source << stripCursor(source); + + source = QLatin1String("char *s, *t;@"); + QTest::newRow("md1-out_end") + << source << stripCursor(source); + + QTest::newRow("non-pointer-after_in-start") + << "char c, @*t, d;" + << "char c, * t, d;"; + + QTest::newRow("pointer-after_in-start") + << "char c, @*t, *d;" + << "char c, * t, *d;"; + + QTest::newRow("function-pointer_in-start") + << "char *s, @*(*foo)(char *s) = 0;" + << "char *s, *(*foo)(char * s) = 0;"; +} + +void CppToolsPlugin::test_format_pointerdeclaration_multiple_matches() +{ + QFETCH(QString, source); + QFETCH(QString, reformattedSource); + + TestEnvironment env(source.toLatin1(), Document::ParseTranlationUnit); + AST *ast = env.translationUnit->ast(); + QVERIFY(ast); + + env.applyFormatting(ast, PointerDeclarationFormatter::IgnoreCursor); + + QCOMPARE(env.textDocument->toPlainText(), reformattedSource); +} + +void CppToolsPlugin::test_format_pointerdeclaration_multiple_matches_data() +{ + QTest::addColumn<QString>("source"); + QTest::addColumn<QString>("reformattedSource"); + + QTest::newRow("rvalue-reference") + << "int g(Bar&&c) {}" + << "int g(Bar && c) {}"; + + QTest::newRow("if2") + << "int g() { if (char *s = 0) { char *t = 0; } }" + << "int g() { if (char * s = 0) { char * t = 0; } }"; + + QTest::newRow("if1") + << "int g() { if (int i = 0) { char *t = 0; } }" + << "int g() { if (int i = 0) { char * t = 0; } }"; + + QTest::newRow("for1") + << "int g() { for (char *s = 0; char *t = 0; s++); }" + << "int g() { for (char * s = 0; char * t = 0; s++); }"; + + QTest::newRow("for2") + << "int g() { for (char *s = 0; char *t = 0; s++) { char *u = 0; } }" + << "int g() { for (char * s = 0; char * t = 0; s++) { char * u = 0; } }"; + + QTest::newRow("for3") + << "int g() { for (char *s; char *t = 0; s++) { char *u = 0; } }" + << "int g() { for (char * s; char * t = 0; s++) { char * u = 0; } }"; + + QTest::newRow("multiple-declarators") + << "const char c, *s, *(*foo)(char *s) = 0;" + << "const char c, * s, *(*foo)(char * s) = 0;"; + + QTest::newRow("complex") + << + "int *foo(const int &b, int*, int *&rpi)\n" + "{\n" + " int*pi = 0;\n" + " int*const*const cpcpi = π\n" + " int*const*pcpi = π\n" + " int**const cppi = π\n" + "\n" + " void (*foo)(char *s) = 0;\n" + " int (*bar)[] = 0;\n" + "\n" + " char *s = 0, *f(int i) = 0;\n" + " const char c, *s, *(*foo)(char *s) = 0;" + "\n" + " for (char *s; char *t = 0; s++) { char *u = 0; }" + "\n" + " return 0;\n" + "}\n" + << + "int * foo(const int & b, int *, int *& rpi)\n" + "{\n" + " int * pi = 0;\n" + " int * const * const cpcpi = π\n" + " int * const * pcpi = π\n" + " int ** const cppi = π\n" + "\n" + " void (*foo)(char * s) = 0;\n" + " int (*bar)[] = 0;\n" + "\n" + " char * s = 0, * f(int i) = 0;\n" + " const char c, * s, *(*foo)(char * s) = 0;" + "\n" + " for (char * s; char * t = 0; s++) { char * u = 0; }" + "\n" + " return 0;\n" + "}\n"; +} diff --git a/src/plugins/cpptools/cpptools.pro b/src/plugins/cpptools/cpptools.pro index 4151ba9b0b9..50ffa5d5626 100644 --- a/src/plugins/cpptools/cpptools.pro +++ b/src/plugins/cpptools/cpptools.pro @@ -47,7 +47,8 @@ HEADERS += completionsettingspage.h \ ModelManagerInterface.h \ TypeHierarchyBuilder.h \ cppindexingsupport.h \ - builtinindexingsupport.h + builtinindexingsupport.h \ + cpppointerdeclarationformatter.h SOURCES += completionsettingspage.cpp \ cppclassesfilter.cpp \ @@ -88,7 +89,8 @@ SOURCES += completionsettingspage.cpp \ ModelManagerInterface.cpp \ TypeHierarchyBuilder.cpp \ cppindexingsupport.cpp \ - builtinindexingsupport.cpp + builtinindexingsupport.cpp \ + cpppointerdeclarationformatter.cpp FORMS += completionsettingspage.ui \ cppfilesettingspage.ui \ @@ -99,7 +101,8 @@ equals(TEST, 1) { cppcodegen_test.cpp \ cppcompletion_test.cpp \ cppmodelmanager_test.cpp \ - modelmanagertesthelper.cpp + modelmanagertesthelper.cpp \ + cpppointerdeclarationformatter_test.cpp HEADERS += \ modelmanagertesthelper.h diff --git a/src/plugins/cpptools/cpptools.qbs b/src/plugins/cpptools/cpptools.qbs index 687182bec9c..dc02251a1f2 100644 --- a/src/plugins/cpptools/cpptools.qbs +++ b/src/plugins/cpptools/cpptools.qbs @@ -74,6 +74,8 @@ QtcPlugin { "cppmodelmanager.h", "cppqtstyleindenter.cpp", "cppqtstyleindenter.h", + "cpppointerdeclarationformatter.cpp", + "cpppointerdeclarationformatter.h", "cpprefactoringchanges.cpp", "cpprefactoringchanges.h", "cppsemanticinfo.cpp", @@ -110,7 +112,8 @@ QtcPlugin { "cppcodegen_test.cpp", "cppcompletion_test.cpp", "cppmodelmanager_test.cpp", - "modelmanagertesthelper.cpp", "modelmanagertesthelper.h" + "modelmanagertesthelper.cpp", "modelmanagertesthelper.h", + "cpppointerdeclarationformatter_test.cpp" ] cpp.defines: outer.concat(['SRCDIR="' + FileInfo.path(filePath) + '"']) diff --git a/src/plugins/cpptools/cpptoolsplugin.h b/src/plugins/cpptools/cpptoolsplugin.h index 903213d26f5..41a7ac4cac5 100644 --- a/src/plugins/cpptools/cpptoolsplugin.h +++ b/src/plugins/cpptools/cpptoolsplugin.h @@ -74,9 +74,8 @@ public: private slots: void switchHeaderSource(); -#ifdef WITH_TESTS - // codegen tests +#ifdef WITH_TESTS void test_codegen_public_in_empty_class(); void test_codegen_public_in_nonempty_class(); void test_codegen_public_before_protected(); @@ -114,6 +113,15 @@ private slots: void test_completion_instantiate_nested_class_when_enclosing_is_template(); void test_completion_instantiate_nested_of_nested_class_when_enclosing_is_template(); + void test_format_pointerdeclaration_in_simpledeclarations(); + void test_format_pointerdeclaration_in_simpledeclarations_data(); + void test_format_pointerdeclaration_in_controlflowstatements(); + void test_format_pointerdeclaration_in_controlflowstatements_data(); + void test_format_pointerdeclaration_multiple_declarators(); + void test_format_pointerdeclaration_multiple_declarators_data(); + void test_format_pointerdeclaration_multiple_matches(); + void test_format_pointerdeclaration_multiple_matches_data(); + void test_modelmanager_paths(); void test_modelmanager_framework_headers(); -- GitLab