CppEditor: Move ExtractFunction quickfix to its own files

Change-Id: Iff66f8bc7dc48c9c7d38c77cfe4f43915236fb1c
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Christian Kandeler
2024-05-16 14:41:45 +02:00
parent e634dd769f
commit d30ed4c738
8 changed files with 778 additions and 717 deletions

View File

@@ -110,6 +110,7 @@ add_qtc_plugin(CppEditor
quickfixes/cppquickfixsettingspage.cpp quickfixes/cppquickfixsettingspage.h
quickfixes/cppquickfixsettingswidget.cpp quickfixes/cppquickfixsettingswidget.h
quickfixes/createdeclarationfromuse.cpp quickfixes/createdeclarationfromuse.h
quickfixes/extractfunction.cpp quickfixes/extractfunction.h
quickfixes/insertfunctiondefinition.cpp quickfixes/insertfunctiondefinition.h
quickfixes/logicaloperationquickfixes.cpp quickfixes/logicaloperationquickfixes.h
quickfixes/moveclasstoownfile.cpp quickfixes/moveclasstoownfile.h

View File

@@ -249,6 +249,8 @@ QtcPlugin {
"cppquickfixsettingswidget.h",
"createdeclarationfromuse.cpp",
"createdeclarationfromuse.h",
"extractfunction.cpp",
"extractfunction.h",
"insertfunctiondefinition.cpp",
"insertfunctiondefinition.h",
"logicaloperationquickfixes.cpp",

View File

@@ -1493,115 +1493,6 @@ void QuickfixTest::testAssignToLocalVariableTemplates()
QuickFixOperationTest(testDocuments, &factory);
}
void QuickfixTest::testExtractFunction_data()
{
QTest::addColumn<QByteArray>("original");
QTest::addColumn<QByteArray>("expected");
QTest::newRow("basic")
<< _("// Documentation for f\n"
"void f()\n"
"{\n"
" @{start}g();@{end}\n"
"}\n")
<< _("inline void extracted()\n"
"{\n"
" g();\n"
"}\n"
"\n"
"// Documentation for f\n"
"void f()\n"
"{\n"
" extracted();\n"
"}\n");
QTest::newRow("class function")
<< _("class Foo\n"
"{\n"
"private:\n"
" void bar();\n"
"};\n\n"
"void Foo::bar()\n"
"{\n"
" @{start}g();@{end}\n"
"}\n")
<< _("class Foo\n"
"{\n"
"public:\n"
" void extracted();\n\n"
"private:\n"
" void bar();\n"
"};\n\n"
"inline void Foo::extracted()\n"
"{\n"
" g();\n"
"}\n\n"
"void Foo::bar()\n"
"{\n"
" extracted();\n"
"}\n");
QTest::newRow("class in namespace")
<< _("namespace NS {\n"
"class C {\n"
" void f(C &c);\n"
"};\n"
"}\n"
"void NS::C::f(NS::C &c)\n"
"{\n"
" @{start}C *c2 = &c;@{end}\n"
"}\n")
<< _("namespace NS {\n"
"class C {\n"
" void f(C &c);\n"
"\n"
"public:\n"
" void extracted(NS::C &c);\n" // TODO: Remove non-required qualification
"};\n"
"}\n"
"inline void NS::C::extracted(NS::C &c)\n"
"{\n"
" C *c2 = &c;\n"
"}\n"
"\n"
"void NS::C::f(NS::C &c)\n"
"{\n"
" extracted(c);\n"
"}\n");
QTest::newRow("if-block")
<< _("inline void func()\n"
"{\n"
" int dummy = 0;\n"
" @{start}if@{end} (dummy < 10) {\n"
" ++dummy;\n"
" }\n"
"}\n")
<< _("inline void extracted(int dummy)\n"
"{\n"
" if (dummy < 10) {\n"
" ++dummy;\n"
" }\n"
"}\n\n"
"inline void func()\n"
"{\n"
" int dummy = 0;\n"
" extracted(dummy);\n"
"}\n");
}
void QuickfixTest::testExtractFunction()
{
QFETCH(QByteArray, original);
QFETCH(QByteArray, expected);
QList<TestDocumentPtr> testDocuments;
testDocuments << CppTestDocument::create("file.h", original, expected);
ExtractFunction factory([]() { return QLatin1String("extracted"); });
QuickFixOperationTest(testDocuments, &factory);
}
void QuickfixTest::testExtractLiteralAsParameterTypeDeduction_data()
{
QTest::addColumn<QByteArray>("typeString");

View File

@@ -99,9 +99,6 @@ private slots:
void testAssignToLocalVariableTemplates();
void testExtractFunction_data();
void testExtractFunction();
void testExtractLiteralAsParameterTypeDeduction_data();
void testExtractLiteralAsParameterTypeDeduction();
void testExtractLiteralAsParameterFreeFunctionSeparateFiles();

View File

@@ -23,6 +23,7 @@
#include "convertqt4connect.h"
#include "convertstringliteral.h"
#include "createdeclarationfromuse.h"
#include "extractfunction.h"
#include "insertfunctiondefinition.h"
#include "logicaloperationquickfixes.h"
#include "moveclasstoownfile.h"
@@ -867,595 +868,6 @@ void CompleteSwitchCaseStatement::doMatch(const CppQuickFixInterface &interface,
}
}
namespace {
class ExtractFunctionOptions
{
public:
static bool isValidFunctionName(const QString &name)
{
return !name.isEmpty() && isValidIdentifier(name);
}
bool hasValidFunctionName() const
{
return isValidFunctionName(funcName);
}
QString funcName;
InsertionPointLocator::AccessSpec access = InsertionPointLocator::Public;
};
class ExtractFunctionOperation : public CppQuickFixOperation
{
public:
ExtractFunctionOperation(const CppQuickFixInterface &interface,
int extractionStart,
int extractionEnd,
FunctionDefinitionAST *refFuncDef,
Symbol *funcReturn,
QList<QPair<QString, QString> > relevantDecls,
ExtractFunction::FunctionNameGetter functionNameGetter
= ExtractFunction::FunctionNameGetter())
: CppQuickFixOperation(interface)
, m_extractionStart(extractionStart)
, m_extractionEnd(extractionEnd)
, m_refFuncDef(refFuncDef)
, m_funcReturn(funcReturn)
, m_relevantDecls(relevantDecls)
, m_functionNameGetter(functionNameGetter)
{
setDescription(Tr::tr("Extract Function"));
}
void perform() override
{
QTC_ASSERT(!m_funcReturn || !m_relevantDecls.isEmpty(), return);
CppRefactoringChanges refactoring(snapshot());
CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath());
ExtractFunctionOptions options;
if (m_functionNameGetter)
options.funcName = m_functionNameGetter();
else
options = getOptions();
if (!options.hasValidFunctionName())
return;
const QString &funcName = options.funcName;
Function *refFunc = m_refFuncDef->symbol;
// We don't need to rewrite the type for declarations made inside the reference function,
// since their scope will remain the same. Then we preserve the original spelling style.
// However, we must do so for the return type in the definition.
SubstitutionEnvironment env;
env.setContext(context());
env.switchScope(refFunc);
ClassOrNamespace *targetCoN = context().lookupType(refFunc->enclosingScope());
if (!targetCoN)
targetCoN = context().globalNamespace();
UseMinimalNames subs(targetCoN);
env.enter(&subs);
Overview printer = CppCodeStyleSettings::currentProjectCodeStyleOverview();
Control *control = context().bindings()->control().get();
QString funcDef;
QString funcDecl; // We generate a declaration only in the case of a member function.
QString funcCall;
Class *matchingClass = isMemberFunction(context(), refFunc);
// Write return type.
if (!m_funcReturn) {
funcDef.append(QLatin1String("void "));
if (matchingClass)
funcDecl.append(QLatin1String("void "));
} else {
const FullySpecifiedType &fullType = rewriteType(m_funcReturn->type(), &env, control);
funcDef.append(printer.prettyType(fullType) + QLatin1Char(' '));
funcDecl.append(printer.prettyType(m_funcReturn->type()) + QLatin1Char(' '));
}
// Write class qualification, if any.
if (matchingClass) {
const Scope *current = matchingClass;
QVector<const Name *> classes{matchingClass->name()};
while (current->enclosingScope()->asClass()) {
current = current->enclosingScope()->asClass();
classes.prepend(current->name());
}
while (current->enclosingScope() && current->enclosingScope()->asNamespace()) {
current = current->enclosingScope()->asNamespace();
if (current->name())
classes.prepend(current->name());
}
for (const Name *n : classes) {
const Name *name = rewriteName(n, &env, control);
funcDef.append(printer.prettyName(name));
funcDef.append(QLatin1String("::"));
}
}
// Write the extracted function itself and its call.
funcDef.append(funcName);
if (matchingClass)
funcDecl.append(funcName);
funcCall.append(funcName);
funcDef.append(QLatin1Char('('));
if (matchingClass)
funcDecl.append(QLatin1Char('('));
funcCall.append(QLatin1Char('('));
for (int i = m_funcReturn ? 1 : 0; i < m_relevantDecls.length(); ++i) {
QPair<QString, QString> p = m_relevantDecls.at(i);
funcCall.append(p.first);
funcDef.append(p.second);
if (matchingClass)
funcDecl.append(p.second);
if (i < m_relevantDecls.length() - 1) {
funcCall.append(QLatin1String(", "));
funcDef.append(QLatin1String(", "));
if (matchingClass)
funcDecl.append(QLatin1String(", "));
}
}
funcDef.append(QLatin1Char(')'));
if (matchingClass)
funcDecl.append(QLatin1Char(')'));
funcCall.append(QLatin1Char(')'));
if (refFunc->isConst()) {
funcDef.append(QLatin1String(" const"));
funcDecl.append(QLatin1String(" const"));
}
funcDef.append(QLatin1String("\n{\n"));
QString extract = currentFile->textOf(m_extractionStart, m_extractionEnd);
extract.replace(QChar::ParagraphSeparator, QLatin1String("\n"));
if (!extract.endsWith(QLatin1Char('\n')) && m_funcReturn)
extract.append(QLatin1Char('\n'));
funcDef.append(extract);
if (matchingClass)
funcDecl.append(QLatin1String(";\n"));
if (m_funcReturn) {
funcDef.append(QLatin1String("\nreturn ")
+ m_relevantDecls.at(0).first
+ QLatin1Char(';'));
funcCall.prepend(m_relevantDecls.at(0).second + QLatin1String(" = "));
}
funcDef.append(QLatin1String("\n}\n\n"));
funcDef.replace(QChar::ParagraphSeparator, QLatin1String("\n"));
funcDef.prepend(inlinePrefix(currentFile->filePath()));
funcCall.append(QLatin1Char(';'));
// Do not insert right between the function and an associated comment.
int position = currentFile->startOf(m_refFuncDef);
const QList<Token> functionDoc = commentsForDeclaration(
m_refFuncDef->symbol, m_refFuncDef, *currentFile->document(),
currentFile->cppDocument());
if (!functionDoc.isEmpty()) {
position = currentFile->cppDocument()->translationUnit()->getTokenPositionInDocument(
functionDoc.first(), currentFile->document());
}
ChangeSet change;
change.insert(position, funcDef);
change.replace(m_extractionStart, m_extractionEnd, funcCall);
currentFile->setChangeSet(change);
currentFile->apply();
// Write declaration, if necessary.
if (matchingClass) {
InsertionPointLocator locator(refactoring);
const FilePath filePath = FilePath::fromUtf8(matchingClass->fileName());
const InsertionLocation &location =
locator.methodDeclarationInClass(filePath, matchingClass, options.access);
CppRefactoringFilePtr declFile = refactoring.cppFile(filePath);
change.clear();
position = declFile->position(location.line(), location.column());
change.insert(position, location.prefix() + funcDecl + location.suffix());
declFile->setChangeSet(change);
declFile->apply();
}
}
ExtractFunctionOptions getOptions() const
{
QDialog dlg(Core::ICore::dialogParent());
dlg.setWindowTitle(Tr::tr("Extract Function Refactoring"));
auto layout = new QFormLayout(&dlg);
auto funcNameEdit = new FancyLineEdit;
funcNameEdit->setValidationFunction([](FancyLineEdit *edit, QString *) {
return ExtractFunctionOptions::isValidFunctionName(edit->text());
});
layout->addRow(Tr::tr("Function name"), funcNameEdit);
auto accessCombo = new QComboBox;
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::Public),
InsertionPointLocator::Public);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::PublicSlot),
InsertionPointLocator::PublicSlot);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::Protected),
InsertionPointLocator::Protected);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::ProtectedSlot),
InsertionPointLocator::ProtectedSlot);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::Private),
InsertionPointLocator::Private);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::PrivateSlot),
InsertionPointLocator::PrivateSlot);
layout->addRow(Tr::tr("Access"), accessCombo);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
QObject::connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
QObject::connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
QPushButton *ok = buttonBox->button(QDialogButtonBox::Ok);
ok->setEnabled(false);
QObject::connect(funcNameEdit, &Utils::FancyLineEdit::validChanged,
ok, &QPushButton::setEnabled);
layout->addWidget(buttonBox);
if (dlg.exec() == QDialog::Accepted) {
ExtractFunctionOptions options;
options.funcName = funcNameEdit->text();
options.access = static_cast<InsertionPointLocator::AccessSpec>(accessCombo->
currentData().toInt());
return options;
}
return ExtractFunctionOptions();
}
int m_extractionStart;
int m_extractionEnd;
FunctionDefinitionAST *m_refFuncDef;
Symbol *m_funcReturn;
QList<QPair<QString, QString> > m_relevantDecls;
ExtractFunction::FunctionNameGetter m_functionNameGetter;
};
QPair<QString, QString> assembleDeclarationData(const QString &specifiers, DeclaratorAST *decltr,
const CppRefactoringFilePtr &file,
const Overview &printer)
{
QTC_ASSERT(decltr, return (QPair<QString, QString>()));
if (decltr->core_declarator
&& decltr->core_declarator->asDeclaratorId()
&& decltr->core_declarator->asDeclaratorId()->name) {
QString decltrText = file->textOf(file->startOf(decltr),
file->endOf(decltr->core_declarator));
if (!decltrText.isEmpty()) {
const QString &name = printer.prettyName(
decltr->core_declarator->asDeclaratorId()->name->name);
QString completeDecl = specifiers;
if (!decltrText.contains(QLatin1Char(' ')))
completeDecl.append(QLatin1Char(' ') + decltrText);
else
completeDecl.append(decltrText);
return {name, completeDecl};
}
}
return QPair<QString, QString>();
}
class FunctionExtractionAnalyser : public ASTVisitor
{
public:
FunctionExtractionAnalyser(TranslationUnit *unit,
const int selStart,
const int selEnd,
const CppRefactoringFilePtr &file,
const Overview &printer)
: ASTVisitor(unit)
, m_done(false)
, m_failed(false)
, m_selStart(selStart)
, m_selEnd(selEnd)
, m_extractionStart(0)
, m_extractionEnd(0)
, m_file(file)
, m_printer(printer)
{}
bool operator()(FunctionDefinitionAST *refFunDef)
{
accept(refFunDef);
if (!m_failed && m_extractionStart == m_extractionEnd)
m_failed = true;
return !m_failed;
}
bool preVisit(AST *) override
{
return !m_done;
}
void statement(StatementAST *stmt)
{
if (!stmt)
return;
const int stmtStart = m_file->startOf(stmt);
const int stmtEnd = m_file->endOf(stmt);
if (stmtStart >= m_selEnd
|| (m_extractionStart && stmtEnd > m_selEnd)) {
m_done = true;
return;
}
if (stmtStart >= m_selStart && !m_extractionStart)
m_extractionStart = stmtStart;
if (stmtEnd > m_extractionEnd && m_extractionStart)
m_extractionEnd = stmtEnd;
accept(stmt);
}
bool visit(CaseStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(CompoundStatementAST *stmt) override
{
for (StatementListAST *it = stmt->statement_list; it; it = it->next) {
statement(it->value);
if (m_done)
break;
}
return false;
}
bool visit(DoStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(ForeachStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(RangeBasedForStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(ForStatementAST *stmt) override
{
statement(stmt->initializer);
if (!m_done)
statement(stmt->statement);
return false;
}
bool visit(IfStatementAST *stmt) override
{
statement(stmt->statement);
if (!m_done)
statement(stmt->else_statement);
return false;
}
bool visit(TryBlockStatementAST *stmt) override
{
statement(stmt->statement);
for (CatchClauseListAST *it = stmt->catch_clause_list; it; it = it->next) {
statement(it->value);
if (m_done)
break;
}
return false;
}
bool visit(WhileStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(DeclarationStatementAST *declStmt) override
{
// We need to collect the declarations we see before the extraction or even inside it.
// They might need to be used as either a parameter or return value. Actually, we could
// still obtain their types from the local uses, but it's good to preserve the original
// typing style.
if (declStmt
&& declStmt->declaration
&& declStmt->declaration->asSimpleDeclaration()) {
SimpleDeclarationAST *simpleDecl = declStmt->declaration->asSimpleDeclaration();
if (simpleDecl->decl_specifier_list
&& simpleDecl->declarator_list) {
const QString &specifiers =
m_file->textOf(m_file->startOf(simpleDecl),
m_file->endOf(simpleDecl->decl_specifier_list->lastValue()));
for (DeclaratorListAST *decltrList = simpleDecl->declarator_list;
decltrList;
decltrList = decltrList->next) {
const QPair<QString, QString> p =
assembleDeclarationData(specifiers, decltrList->value, m_file, m_printer);
if (!p.first.isEmpty())
m_knownDecls.insert(p.first, p.second);
}
}
}
return false;
}
bool visit(ReturnStatementAST *) override
{
if (m_extractionStart) {
m_done = true;
m_failed = true;
}
return false;
}
bool m_done;
bool m_failed;
const int m_selStart;
const int m_selEnd;
int m_extractionStart;
int m_extractionEnd;
QHash<QString, QString> m_knownDecls;
CppRefactoringFilePtr m_file;
const Overview &m_printer;
};
} // anonymous namespace
ExtractFunction::ExtractFunction(FunctionNameGetter functionNameGetter)
: m_functionNameGetter(functionNameGetter)
{
}
void ExtractFunction::doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result)
{
const CppRefactoringFilePtr file = interface.currentFile();
// TODO: Fix upstream and uncomment; see QTCREATORBUG-28030.
// if (CppModelManager::usesClangd(file->editor()->textDocument())
// && file->cppDocument()->languageFeatures().cxxEnabled) {
// return;
// }
QTextCursor cursor = file->cursor();
if (!cursor.hasSelection())
return;
const QList<AST *> &path = interface.path();
FunctionDefinitionAST *refFuncDef = nullptr; // The "reference" function, which we will extract from.
for (int i = path.size() - 1; i >= 0; --i) {
refFuncDef = path.at(i)->asFunctionDefinition();
if (refFuncDef)
break;
}
if (!refFuncDef
|| !refFuncDef->function_body
|| !refFuncDef->function_body->asCompoundStatement()
|| !refFuncDef->function_body->asCompoundStatement()->statement_list
|| !refFuncDef->symbol
|| !refFuncDef->symbol->name()
|| refFuncDef->symbol->enclosingScope()->asTemplate() /* TODO: Templates... */) {
return;
}
// Adjust selection ends.
int selStart = cursor.selectionStart();
int selEnd = cursor.selectionEnd();
if (selStart > selEnd)
std::swap(selStart, selEnd);
Overview printer;
// Analyze the content to be extracted, which consists of determining the statements
// which are complete and collecting the declarations seen.
FunctionExtractionAnalyser analyser(interface.semanticInfo().doc->translationUnit(),
selStart, selEnd,
file,
printer);
if (!analyser(refFuncDef))
return;
// We also need to collect the declarations of the parameters from the reference function.
QSet<QString> refFuncParams;
if (refFuncDef->declarator->postfix_declarator_list
&& refFuncDef->declarator->postfix_declarator_list->value
&& refFuncDef->declarator->postfix_declarator_list->value->asFunctionDeclarator()) {
FunctionDeclaratorAST *funcDecltr =
refFuncDef->declarator->postfix_declarator_list->value->asFunctionDeclarator();
if (funcDecltr->parameter_declaration_clause
&& funcDecltr->parameter_declaration_clause->parameter_declaration_list) {
for (ParameterDeclarationListAST *it =
funcDecltr->parameter_declaration_clause->parameter_declaration_list;
it;
it = it->next) {
ParameterDeclarationAST *paramDecl = it->value->asParameterDeclaration();
if (paramDecl->declarator) {
const QString &specifiers =
file->textOf(file->startOf(paramDecl),
file->endOf(paramDecl->type_specifier_list->lastValue()));
const QPair<QString, QString> &p =
assembleDeclarationData(specifiers, paramDecl->declarator,
file, printer);
if (!p.first.isEmpty()) {
analyser.m_knownDecls.insert(p.first, p.second);
refFuncParams.insert(p.first);
}
}
}
}
}
// Identify what would be parameters for the new function and its return value, if any.
Symbol *funcReturn = nullptr;
QList<QPair<QString, QString> > relevantDecls;
const SemanticInfo::LocalUseMap localUses = interface.semanticInfo().localUses;
for (auto it = localUses.cbegin(), end = localUses.cend(); it != end; ++it) {
bool usedBeforeExtraction = false;
bool usedAfterExtraction = false;
bool usedInsideExtraction = false;
const QList<SemanticInfo::Use> &uses = it.value();
for (const SemanticInfo::Use &use : uses) {
if (use.isInvalid())
continue;
const int position = file->position(use.line, use.column);
if (position < analyser.m_extractionStart)
usedBeforeExtraction = true;
else if (position >= analyser.m_extractionEnd)
usedAfterExtraction = true;
else
usedInsideExtraction = true;
}
const QString &name = printer.prettyName(it.key()->name());
if ((usedBeforeExtraction && usedInsideExtraction)
|| (usedInsideExtraction && refFuncParams.contains(name))) {
QTC_ASSERT(analyser.m_knownDecls.contains(name), return);
relevantDecls.push_back({name, analyser.m_knownDecls.value(name)});
}
// We assume that the first use of a local corresponds to its declaration.
if (usedInsideExtraction && usedAfterExtraction && !usedBeforeExtraction) {
if (!funcReturn) {
QTC_ASSERT(analyser.m_knownDecls.contains(name), return);
// The return, if any, is stored as the first item in the list.
relevantDecls.push_front({name, analyser.m_knownDecls.value(name)});
funcReturn = it.key();
} else {
// Would require multiple returns. (Unless we do fancy things, as pointed below.)
return;
}
}
}
// The current implementation doesn't try to be too smart since it preserves the original form
// of the declarations. This might be or not the desired effect. An improvement would be to
// let the user somehow customize the function interface.
result << new ExtractFunctionOperation(interface,
analyser.m_extractionStart,
analyser.m_extractionEnd,
refFuncDef, funcReturn, relevantDecls,
m_functionNameGetter);
}
namespace {
struct ReplaceLiteralsResult
@@ -2549,7 +1961,6 @@ void createCppQuickFixes()
new ApplyDeclDefLinkChanges;
new ConvertFromAndToPointer;
new ExtractFunction;
new ExtractLiteralAsParameter;
new AssignToLocalVariable;
@@ -2567,6 +1978,7 @@ void createCppQuickFixes()
registerLogicalOperationQuickfixes();
registerRewriteControlStatementQuickfixes();
registerRewriteCommentQuickfixes();
registerExtractFunctionQuickfix();
new ExtraRefactoringOperations;

View File

@@ -125,21 +125,6 @@ private:
void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override;
};
/*!
Extracts the selected code and puts it to a function
*/
class ExtractFunction : public CppQuickFixFactory
{
public:
using FunctionNameGetter = std::function<QString()>;
ExtractFunction(FunctionNameGetter functionNameGetter = FunctionNameGetter());
void doMatch(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result) override;
private:
FunctionNameGetter m_functionNameGetter; // For tests to avoid GUI pop-up.
};
/*!
Extracts the selected constant and converts it to a parameter of the current function.

View File

@@ -0,0 +1,766 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "extractfunction.h"
#include "../cppcodestylesettings.h"
#include "../cppeditortr.h"
#include "../cpprefactoringchanges.h"
#include "../insertionpointlocator.h"
#include "cppquickfix.h"
#include "cppquickfixhelpers.h"
#include <coreplugin/icore.h>
#include <cplusplus/CppRewriter.h>
#include <cplusplus/declarationcomments.h>
#include <cplusplus/Overview.h>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QPushButton>
#include <functional>
#ifdef WITH_TESTS
#include "cppquickfix_test.h"
#include <QTest>
#endif
using namespace CPlusPlus;
using namespace Utils;
namespace CppEditor::Internal {
namespace {
using FunctionNameGetter = std::function<QString()>;
class ExtractFunctionOptions
{
public:
static bool isValidFunctionName(const QString &name)
{
return !name.isEmpty() && isValidIdentifier(name);
}
bool hasValidFunctionName() const
{
return isValidFunctionName(funcName);
}
QString funcName;
InsertionPointLocator::AccessSpec access = InsertionPointLocator::Public;
};
class ExtractFunctionOperation : public CppQuickFixOperation
{
public:
ExtractFunctionOperation(
const CppQuickFixInterface &interface,
int extractionStart,
int extractionEnd,
FunctionDefinitionAST *refFuncDef,
Symbol *funcReturn,
QList<QPair<QString, QString>> relevantDecls,
FunctionNameGetter functionNameGetter = {})
: CppQuickFixOperation(interface)
, m_extractionStart(extractionStart)
, m_extractionEnd(extractionEnd)
, m_refFuncDef(refFuncDef)
, m_funcReturn(funcReturn)
, m_relevantDecls(relevantDecls)
, m_functionNameGetter(functionNameGetter)
{
setDescription(Tr::tr("Extract Function"));
}
void perform() override
{
QTC_ASSERT(!m_funcReturn || !m_relevantDecls.isEmpty(), return);
CppRefactoringChanges refactoring(snapshot());
CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath());
ExtractFunctionOptions options;
if (m_functionNameGetter)
options.funcName = m_functionNameGetter();
else
options = getOptions();
if (!options.hasValidFunctionName())
return;
const QString &funcName = options.funcName;
Function *refFunc = m_refFuncDef->symbol;
// We don't need to rewrite the type for declarations made inside the reference function,
// since their scope will remain the same. Then we preserve the original spelling style.
// However, we must do so for the return type in the definition.
SubstitutionEnvironment env;
env.setContext(context());
env.switchScope(refFunc);
ClassOrNamespace *targetCoN = context().lookupType(refFunc->enclosingScope());
if (!targetCoN)
targetCoN = context().globalNamespace();
UseMinimalNames subs(targetCoN);
env.enter(&subs);
Overview printer = CppCodeStyleSettings::currentProjectCodeStyleOverview();
Control *control = context().bindings()->control().get();
QString funcDef;
QString funcDecl; // We generate a declaration only in the case of a member function.
QString funcCall;
Class *matchingClass = isMemberFunction(context(), refFunc);
// Write return type.
if (!m_funcReturn) {
funcDef.append(QLatin1String("void "));
if (matchingClass)
funcDecl.append(QLatin1String("void "));
} else {
const FullySpecifiedType &fullType = rewriteType(m_funcReturn->type(), &env, control);
funcDef.append(printer.prettyType(fullType) + QLatin1Char(' '));
funcDecl.append(printer.prettyType(m_funcReturn->type()) + QLatin1Char(' '));
}
// Write class qualification, if any.
if (matchingClass) {
const Scope *current = matchingClass;
QVector<const Name *> classes{matchingClass->name()};
while (current->enclosingScope()->asClass()) {
current = current->enclosingScope()->asClass();
classes.prepend(current->name());
}
while (current->enclosingScope() && current->enclosingScope()->asNamespace()) {
current = current->enclosingScope()->asNamespace();
if (current->name())
classes.prepend(current->name());
}
for (const Name *n : classes) {
const Name *name = rewriteName(n, &env, control);
funcDef.append(printer.prettyName(name));
funcDef.append(QLatin1String("::"));
}
}
// Write the extracted function itself and its call.
funcDef.append(funcName);
if (matchingClass)
funcDecl.append(funcName);
funcCall.append(funcName);
funcDef.append(QLatin1Char('('));
if (matchingClass)
funcDecl.append(QLatin1Char('('));
funcCall.append(QLatin1Char('('));
for (int i = m_funcReturn ? 1 : 0; i < m_relevantDecls.length(); ++i) {
QPair<QString, QString> p = m_relevantDecls.at(i);
funcCall.append(p.first);
funcDef.append(p.second);
if (matchingClass)
funcDecl.append(p.second);
if (i < m_relevantDecls.length() - 1) {
funcCall.append(QLatin1String(", "));
funcDef.append(QLatin1String(", "));
if (matchingClass)
funcDecl.append(QLatin1String(", "));
}
}
funcDef.append(QLatin1Char(')'));
if (matchingClass)
funcDecl.append(QLatin1Char(')'));
funcCall.append(QLatin1Char(')'));
if (refFunc->isConst()) {
funcDef.append(QLatin1String(" const"));
funcDecl.append(QLatin1String(" const"));
}
funcDef.append(QLatin1String("\n{\n"));
QString extract = currentFile->textOf(m_extractionStart, m_extractionEnd);
extract.replace(QChar::ParagraphSeparator, QLatin1String("\n"));
if (!extract.endsWith(QLatin1Char('\n')) && m_funcReturn)
extract.append(QLatin1Char('\n'));
funcDef.append(extract);
if (matchingClass)
funcDecl.append(QLatin1String(";\n"));
if (m_funcReturn) {
funcDef.append(QLatin1String("\nreturn ")
+ m_relevantDecls.at(0).first
+ QLatin1Char(';'));
funcCall.prepend(m_relevantDecls.at(0).second + QLatin1String(" = "));
}
funcDef.append(QLatin1String("\n}\n\n"));
funcDef.replace(QChar::ParagraphSeparator, QLatin1String("\n"));
funcDef.prepend(inlinePrefix(currentFile->filePath()));
funcCall.append(QLatin1Char(';'));
// Do not insert right between the function and an associated comment.
int position = currentFile->startOf(m_refFuncDef);
const QList<Token> functionDoc = commentsForDeclaration(
m_refFuncDef->symbol, m_refFuncDef, *currentFile->document(),
currentFile->cppDocument());
if (!functionDoc.isEmpty()) {
position = currentFile->cppDocument()->translationUnit()->getTokenPositionInDocument(
functionDoc.first(), currentFile->document());
}
ChangeSet change;
change.insert(position, funcDef);
change.replace(m_extractionStart, m_extractionEnd, funcCall);
currentFile->setChangeSet(change);
currentFile->apply();
// Write declaration, if necessary.
if (matchingClass) {
InsertionPointLocator locator(refactoring);
const FilePath filePath = FilePath::fromUtf8(matchingClass->fileName());
const InsertionLocation &location =
locator.methodDeclarationInClass(filePath, matchingClass, options.access);
CppRefactoringFilePtr declFile = refactoring.cppFile(filePath);
change.clear();
position = declFile->position(location.line(), location.column());
change.insert(position, location.prefix() + funcDecl + location.suffix());
declFile->setChangeSet(change);
declFile->apply();
}
}
ExtractFunctionOptions getOptions() const
{
QDialog dlg(Core::ICore::dialogParent());
dlg.setWindowTitle(Tr::tr("Extract Function Refactoring"));
auto layout = new QFormLayout(&dlg);
auto funcNameEdit = new FancyLineEdit;
funcNameEdit->setValidationFunction([](FancyLineEdit *edit, QString *) {
return ExtractFunctionOptions::isValidFunctionName(edit->text());
});
layout->addRow(Tr::tr("Function name"), funcNameEdit);
auto accessCombo = new QComboBox;
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::Public),
InsertionPointLocator::Public);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::PublicSlot),
InsertionPointLocator::PublicSlot);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::Protected),
InsertionPointLocator::Protected);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::ProtectedSlot),
InsertionPointLocator::ProtectedSlot);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::Private),
InsertionPointLocator::Private);
accessCombo->addItem(
InsertionPointLocator::accessSpecToString(InsertionPointLocator::PrivateSlot),
InsertionPointLocator::PrivateSlot);
layout->addRow(Tr::tr("Access"), accessCombo);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
QObject::connect(buttonBox, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
QObject::connect(buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
QPushButton *ok = buttonBox->button(QDialogButtonBox::Ok);
ok->setEnabled(false);
QObject::connect(funcNameEdit, &Utils::FancyLineEdit::validChanged,
ok, &QPushButton::setEnabled);
layout->addWidget(buttonBox);
if (dlg.exec() == QDialog::Accepted) {
ExtractFunctionOptions options;
options.funcName = funcNameEdit->text();
options.access = static_cast<InsertionPointLocator::AccessSpec>(accessCombo->
currentData().toInt());
return options;
}
return ExtractFunctionOptions();
}
int m_extractionStart;
int m_extractionEnd;
FunctionDefinitionAST *m_refFuncDef;
Symbol *m_funcReturn;
QList<QPair<QString, QString> > m_relevantDecls;
FunctionNameGetter m_functionNameGetter;
};
static QPair<QString, QString> assembleDeclarationData(
const QString &specifiers,
DeclaratorAST *decltr,
const CppRefactoringFilePtr &file,
const Overview &printer)
{
QTC_ASSERT(decltr, return (QPair<QString, QString>()));
if (decltr->core_declarator
&& decltr->core_declarator->asDeclaratorId()
&& decltr->core_declarator->asDeclaratorId()->name) {
QString decltrText = file->textOf(file->startOf(decltr),
file->endOf(decltr->core_declarator));
if (!decltrText.isEmpty()) {
const QString &name = printer.prettyName(
decltr->core_declarator->asDeclaratorId()->name->name);
QString completeDecl = specifiers;
if (!decltrText.contains(QLatin1Char(' ')))
completeDecl.append(QLatin1Char(' ') + decltrText);
else
completeDecl.append(decltrText);
return {name, completeDecl};
}
}
return QPair<QString, QString>();
}
class FunctionExtractionAnalyser : public ASTVisitor
{
public:
FunctionExtractionAnalyser(TranslationUnit *unit,
const int selStart,
const int selEnd,
const CppRefactoringFilePtr &file,
const Overview &printer)
: ASTVisitor(unit)
, m_done(false)
, m_failed(false)
, m_selStart(selStart)
, m_selEnd(selEnd)
, m_extractionStart(0)
, m_extractionEnd(0)
, m_file(file)
, m_printer(printer)
{}
bool operator()(FunctionDefinitionAST *refFunDef)
{
accept(refFunDef);
if (!m_failed && m_extractionStart == m_extractionEnd)
m_failed = true;
return !m_failed;
}
bool preVisit(AST *) override
{
return !m_done;
}
void statement(StatementAST *stmt)
{
if (!stmt)
return;
const int stmtStart = m_file->startOf(stmt);
const int stmtEnd = m_file->endOf(stmt);
if (stmtStart >= m_selEnd
|| (m_extractionStart && stmtEnd > m_selEnd)) {
m_done = true;
return;
}
if (stmtStart >= m_selStart && !m_extractionStart)
m_extractionStart = stmtStart;
if (stmtEnd > m_extractionEnd && m_extractionStart)
m_extractionEnd = stmtEnd;
accept(stmt);
}
bool visit(CaseStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(CompoundStatementAST *stmt) override
{
for (StatementListAST *it = stmt->statement_list; it; it = it->next) {
statement(it->value);
if (m_done)
break;
}
return false;
}
bool visit(DoStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(ForeachStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(RangeBasedForStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(ForStatementAST *stmt) override
{
statement(stmt->initializer);
if (!m_done)
statement(stmt->statement);
return false;
}
bool visit(IfStatementAST *stmt) override
{
statement(stmt->statement);
if (!m_done)
statement(stmt->else_statement);
return false;
}
bool visit(TryBlockStatementAST *stmt) override
{
statement(stmt->statement);
for (CatchClauseListAST *it = stmt->catch_clause_list; it; it = it->next) {
statement(it->value);
if (m_done)
break;
}
return false;
}
bool visit(WhileStatementAST *stmt) override
{
statement(stmt->statement);
return false;
}
bool visit(DeclarationStatementAST *declStmt) override
{
// We need to collect the declarations we see before the extraction or even inside it.
// They might need to be used as either a parameter or return value. Actually, we could
// still obtain their types from the local uses, but it's good to preserve the original
// typing style.
if (declStmt
&& declStmt->declaration
&& declStmt->declaration->asSimpleDeclaration()) {
SimpleDeclarationAST *simpleDecl = declStmt->declaration->asSimpleDeclaration();
if (simpleDecl->decl_specifier_list
&& simpleDecl->declarator_list) {
const QString &specifiers =
m_file->textOf(m_file->startOf(simpleDecl),
m_file->endOf(simpleDecl->decl_specifier_list->lastValue()));
for (DeclaratorListAST *decltrList = simpleDecl->declarator_list;
decltrList;
decltrList = decltrList->next) {
const QPair<QString, QString> p =
assembleDeclarationData(specifiers, decltrList->value, m_file, m_printer);
if (!p.first.isEmpty())
m_knownDecls.insert(p.first, p.second);
}
}
}
return false;
}
bool visit(ReturnStatementAST *) override
{
if (m_extractionStart) {
m_done = true;
m_failed = true;
}
return false;
}
bool m_done;
bool m_failed;
const int m_selStart;
const int m_selEnd;
int m_extractionStart;
int m_extractionEnd;
QHash<QString, QString> m_knownDecls;
CppRefactoringFilePtr m_file;
const Overview &m_printer;
};
//! Extracts the selected code and puts it to a function
class ExtractFunction : public CppQuickFixFactory
{
public:
ExtractFunction(FunctionNameGetter functionNameGetter = FunctionNameGetter())
: m_functionNameGetter(functionNameGetter)
{}
#ifdef WITH_TESTS
static QObject *createTest();
#endif
private:
void doMatch(const CppQuickFixInterface &interface, QuickFixOperations &result) override
{
const CppRefactoringFilePtr file = interface.currentFile();
// TODO: Fix upstream and uncomment; see QTCREATORBUG-28030.
// if (CppModelManager::usesClangd(file->editor()->textDocument())
// && file->cppDocument()->languageFeatures().cxxEnabled) {
// return;
// }
QTextCursor cursor = file->cursor();
if (!cursor.hasSelection())
return;
const QList<AST *> &path = interface.path();
FunctionDefinitionAST *refFuncDef = nullptr; // The "reference" function, which we will extract from.
for (int i = path.size() - 1; i >= 0; --i) {
refFuncDef = path.at(i)->asFunctionDefinition();
if (refFuncDef)
break;
}
if (!refFuncDef
|| !refFuncDef->function_body
|| !refFuncDef->function_body->asCompoundStatement()
|| !refFuncDef->function_body->asCompoundStatement()->statement_list
|| !refFuncDef->symbol
|| !refFuncDef->symbol->name()
|| refFuncDef->symbol->enclosingScope()->asTemplate() /* TODO: Templates... */) {
return;
}
// Adjust selection ends.
int selStart = cursor.selectionStart();
int selEnd = cursor.selectionEnd();
if (selStart > selEnd)
std::swap(selStart, selEnd);
Overview printer;
// Analyze the content to be extracted, which consists of determining the statements
// which are complete and collecting the declarations seen.
FunctionExtractionAnalyser analyser(interface.semanticInfo().doc->translationUnit(),
selStart, selEnd,
file,
printer);
if (!analyser(refFuncDef))
return;
// We also need to collect the declarations of the parameters from the reference function.
QSet<QString> refFuncParams;
if (refFuncDef->declarator->postfix_declarator_list
&& refFuncDef->declarator->postfix_declarator_list->value
&& refFuncDef->declarator->postfix_declarator_list->value->asFunctionDeclarator()) {
FunctionDeclaratorAST *funcDecltr =
refFuncDef->declarator->postfix_declarator_list->value->asFunctionDeclarator();
if (funcDecltr->parameter_declaration_clause
&& funcDecltr->parameter_declaration_clause->parameter_declaration_list) {
for (ParameterDeclarationListAST *it =
funcDecltr->parameter_declaration_clause->parameter_declaration_list;
it;
it = it->next) {
ParameterDeclarationAST *paramDecl = it->value->asParameterDeclaration();
if (paramDecl->declarator) {
const QString &specifiers =
file->textOf(file->startOf(paramDecl),
file->endOf(paramDecl->type_specifier_list->lastValue()));
const QPair<QString, QString> &p =
assembleDeclarationData(specifiers, paramDecl->declarator,
file, printer);
if (!p.first.isEmpty()) {
analyser.m_knownDecls.insert(p.first, p.second);
refFuncParams.insert(p.first);
}
}
}
}
}
// Identify what would be parameters for the new function and its return value, if any.
Symbol *funcReturn = nullptr;
QList<QPair<QString, QString> > relevantDecls;
const SemanticInfo::LocalUseMap localUses = interface.semanticInfo().localUses;
for (auto it = localUses.cbegin(), end = localUses.cend(); it != end; ++it) {
bool usedBeforeExtraction = false;
bool usedAfterExtraction = false;
bool usedInsideExtraction = false;
const QList<SemanticInfo::Use> &uses = it.value();
for (const SemanticInfo::Use &use : uses) {
if (use.isInvalid())
continue;
const int position = file->position(use.line, use.column);
if (position < analyser.m_extractionStart)
usedBeforeExtraction = true;
else if (position >= analyser.m_extractionEnd)
usedAfterExtraction = true;
else
usedInsideExtraction = true;
}
const QString &name = printer.prettyName(it.key()->name());
if ((usedBeforeExtraction && usedInsideExtraction)
|| (usedInsideExtraction && refFuncParams.contains(name))) {
QTC_ASSERT(analyser.m_knownDecls.contains(name), return);
relevantDecls.push_back({name, analyser.m_knownDecls.value(name)});
}
// We assume that the first use of a local corresponds to its declaration.
if (usedInsideExtraction && usedAfterExtraction && !usedBeforeExtraction) {
if (!funcReturn) {
QTC_ASSERT(analyser.m_knownDecls.contains(name), return);
// The return, if any, is stored as the first item in the list.
relevantDecls.push_front({name, analyser.m_knownDecls.value(name)});
funcReturn = it.key();
} else {
// Would require multiple returns. (Unless we do fancy things, as pointed below.)
return;
}
}
}
// The current implementation doesn't try to be too smart since it preserves the original form
// of the declarations. This might be or not the desired effect. An improvement would be to
// let the user somehow customize the function interface.
result << new ExtractFunctionOperation(interface,
analyser.m_extractionStart,
analyser.m_extractionEnd,
refFuncDef, funcReturn, relevantDecls,
m_functionNameGetter);
}
private:
FunctionNameGetter m_functionNameGetter; // For tests to avoid GUI pop-up.
};
#ifdef WITH_TESTS
using namespace Tests;
class ExtractFunctionTest : public QObject
{
Q_OBJECT
private slots:
void test_data()
{
QTest::addColumn<QByteArray>("original");
QTest::addColumn<QByteArray>("expected");
QTest::newRow("basic")
<< QByteArray("// Documentation for f\n"
"void f()\n"
"{\n"
" @{start}g();@{end}\n"
"}\n")
<< QByteArray("inline void extracted()\n"
"{\n"
" g();\n"
"}\n"
"\n"
"// Documentation for f\n"
"void f()\n"
"{\n"
" extracted();\n"
"}\n");
QTest::newRow("class function")
<< QByteArray("class Foo\n"
"{\n"
"private:\n"
" void bar();\n"
"};\n\n"
"void Foo::bar()\n"
"{\n"
" @{start}g();@{end}\n"
"}\n")
<< QByteArray("class Foo\n"
"{\n"
"public:\n"
" void extracted();\n\n"
"private:\n"
" void bar();\n"
"};\n\n"
"inline void Foo::extracted()\n"
"{\n"
" g();\n"
"}\n\n"
"void Foo::bar()\n"
"{\n"
" extracted();\n"
"}\n");
QTest::newRow("class in namespace")
<< QByteArray("namespace NS {\n"
"class C {\n"
" void f(C &c);\n"
"};\n"
"}\n"
"void NS::C::f(NS::C &c)\n"
"{\n"
" @{start}C *c2 = &c;@{end}\n"
"}\n")
<< QByteArray("namespace NS {\n"
"class C {\n"
" void f(C &c);\n"
"\n"
"public:\n"
" void extracted(NS::C &c);\n" // TODO: Remove non-required qualification
"};\n"
"}\n"
"inline void NS::C::extracted(NS::C &c)\n"
"{\n"
" C *c2 = &c;\n"
"}\n"
"\n"
"void NS::C::f(NS::C &c)\n"
"{\n"
" extracted(c);\n"
"}\n");
QTest::newRow("if-block")
<< QByteArray("inline void func()\n"
"{\n"
" int dummy = 0;\n"
" @{start}if@{end} (dummy < 10) {\n"
" ++dummy;\n"
" }\n"
"}\n")
<< QByteArray("inline void extracted(int dummy)\n"
"{\n"
" if (dummy < 10) {\n"
" ++dummy;\n"
" }\n"
"}\n\n"
"inline void func()\n"
"{\n"
" int dummy = 0;\n"
" extracted(dummy);\n"
"}\n");
}
void test()
{
QFETCH(QByteArray, original);
QFETCH(QByteArray, expected);
QList<TestDocumentPtr> testDocuments;
testDocuments << CppTestDocument::create("file.h", original, expected);
ExtractFunction factory([]() { return QLatin1String("extracted"); });
QuickFixOperationTest(testDocuments, &factory);
}
};
QObject *ExtractFunction::createTest() { return new ExtractFunctionTest; }
#endif // WITH_TESTS
} // namespace
void registerExtractFunctionQuickfix()
{
CppQuickFixFactory::registerFactory<ExtractFunction>();
}
} // namespace CppEditor::Internal
#ifdef WITH_TESTS
#include <extractfunction.moc>
#endif

View File

@@ -0,0 +1,7 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
namespace CppEditor::Internal {
void registerExtractFunctionQuickfix();
} // namespace CppEditor::Internal