diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,7 @@ pythonparsejob.cpp pythonhighlighting.cpp pythondebug.cpp + pythonstylechecking.cpp # config pages: docfilekcm/docfilewizard.cpp @@ -103,5 +104,6 @@ install(DIRECTORY documentation_files DESTINATION ${DATA_INSTALL_DIR}/kdevpythonsupport) install(DIRECTORY correction_files DESTINATION ${DATA_INSTALL_DIR}/kdevpythonsupport) +install(FILES codestyle.py DESTINATION ${DATA_INSTALL_DIR}/kdevpythonsupport) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/codestyle.py b/codestyle.py new file mode 100755 --- /dev/null +++ b/codestyle.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +from pycodestyle import Checker +from contextlib import redirect_stdout +from io import StringIO +from sys import stdin, stdout, stderr + +while True: + size = stdin.read(10) + size = int(size) + if not size > 0: + continue + buf = str() + print("waiting for buf of size", size, file=stderr) + while len(buf) < size: + buf += stdin.read(size-len(buf)) + print("buf:", buf, file=stderr) + lines = buf.splitlines() + opts, text = lines[:3], [l + "\n" for l in lines[3:]] + + c = Checker(lines=text) + output = StringIO() + with redirect_stdout(output): + # writes directly to stdout, so catch it ... + c.check_all() + output = output.getvalue() + + stdout.write("{0:>10}".format(len(output))) + stdout.write(output) + stdout.flush() diff --git a/duchain/helpers.h b/duchain/helpers.h --- a/duchain/helpers.h +++ b/duchain/helpers.h @@ -225,6 +225,8 @@ static Declaration* declarationForName(const QualifiedIdentifier& identifier, const RangeInRevision& nodeRange, DUChainPointer context); + + static QString getPythonExecutablePath(IProject* project); }; } diff --git a/duchain/helpers.cpp b/duchain/helpers.cpp --- a/duchain/helpers.cpp +++ b/duchain/helpers.cpp @@ -361,7 +361,7 @@ return absolutePath; } -QString getPythonExecutablePath(IProject* project) +QString Helper::getPythonExecutablePath(IProject* project) { if ( project ) { auto interpreter = project->projectConfiguration()->group("pythonsupport").readEntry("interpreter"); diff --git a/pythonlanguagesupport.h b/pythonlanguagesupport.h --- a/pythonlanguagesupport.h +++ b/pythonlanguagesupport.h @@ -24,7 +24,10 @@ #include #include #include -#include +#include + +#include +#include namespace KDevelop { @@ -38,6 +41,7 @@ class Highlighting; class Refactoring; +class StyleChecking; class LanguageSupport : public KDevelop::IPlugin @@ -79,12 +83,17 @@ int perProjectConfigPages() const override; KDevelop::ConfigPage* perProjectConfigPage(int number, const KDevelop::ProjectConfigOptions& options, QWidget* parent) override; -public slots: +private: + QProcess m_checkerProcess; + +public Q_SLOTS: void documentOpened(KDevelop::IDocument*); + void updateStyleChecking(KDevelop::ReferencedTopDUContext top); private: Highlighting* m_highlighting; - Refactoring *m_refactoring; + Refactoring* m_refactoring; + StyleChecking* m_styleChecking; static LanguageSupport* m_self; }; diff --git a/pythonlanguagesupport.cpp b/pythonlanguagesupport.cpp --- a/pythonlanguagesupport.cpp +++ b/pythonlanguagesupport.cpp @@ -55,8 +55,11 @@ #include "pep8kcm/kcm_pep8.h" #include "projectconfig/projectconfigpage.h" #include "docfilekcm/kcm_docfiles.h" +#include "pythonstylechecking.h" +#include "helpers.h" #include +#include #include "pythondebug.h" using namespace KDevelop; @@ -85,6 +88,7 @@ , KDevelop::ILanguageSupport() , m_highlighting( new Highlighting( this ) ) , m_refactoring( new Refactoring( this ) ) + , m_styleChecking( new StyleChecking( this ) ) { m_self = this; @@ -106,9 +110,14 @@ } DUChainReadLocker lock; - TopDUContextPointer topContext = TopDUContextPointer(DUChain::self()->chainForDocument(doc->url())); + ReferencedTopDUContext top = DUChain::self()->chainForDocument(doc->url()); lock.unlock(); - ParseJob::eventuallyDoPEP8Checking(IndexedString(doc->url()), topContext.data()); + updateStyleChecking(top); +} + +void LanguageSupport::updateStyleChecking(KDevelop::ReferencedTopDUContext top) +{ + m_styleChecking->updateStyleChecking(top); } LanguageSupport::~LanguageSupport() @@ -216,7 +225,6 @@ return nullptr; } - } #include "pythonlanguagesupport.moc" diff --git a/pythonparsejob.h b/pythonparsejob.h --- a/pythonparsejob.h +++ b/pythonparsejob.h @@ -54,7 +54,7 @@ virtual CodeAst* ast() const; bool wasReadFromDisk() const; - static void eventuallyDoPEP8Checking(const IndexedString document, TopDUContext* topContext); + void eventuallyDoPEP8Checking(TopDUContext* topContext); ControlFlowGraph* controlFlowGraph() override; DataAccessRepository* dataAccessInformation() override; diff --git a/pythonparsejob.cpp b/pythonparsejob.cpp --- a/pythonparsejob.cpp +++ b/pythonparsejob.cpp @@ -56,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -258,8 +259,8 @@ } // If enabled, and if the document is open, do PEP8 checking. - eventuallyDoPEP8Checking(document(), m_duContext); - + eventuallyDoPEP8Checking(m_duContext); + if ( minimumFeatures() & TopDUContext::AST ) { DUChainWriteLocker lock; m_currentSession->ast = m_ast; @@ -280,88 +281,17 @@ return nullptr; } -void ParseJob::eventuallyDoPEP8Checking(const IndexedString document, TopDUContext* topContext) +void ParseJob::eventuallyDoPEP8Checking(TopDUContext* topContext) { - IDocument* idoc = ICore::self()->documentController()->documentForUrl(document.toUrl()); - if ( ! idoc || ! topContext || ! idoc->textDocument() || topContext->features() & PEP8Checking ) { - return; - } - KConfig config("kdevpythonsupportrc"); KConfigGroup configGroup = config.group("pep8"); if ( ! PEP8KCModule::isPep8Enabled(configGroup) ) { return; } - { - DUChainWriteLocker lock; - topContext->setFeatures((TopDUContext::Features) ( topContext->features() | PEP8Checking )); - } - qDebug() << "doing pep8 checking"; - // TODO that's not very elegant, better would be making pep8 read from stdin -- but it doesn't support that atm - QTemporaryFile tempfile; - tempfile.open(); - tempfile.write(idoc->textDocument()->text().toUtf8()); - tempfile.close(); - QString url = PEP8KCModule::pep8Path(configGroup); QString arguments = PEP8KCModule::pep8Arguments(configGroup); - QFileInfo f(url); - bool error = false; - if ( url.isEmpty() || ! f.isExecutable() ) { - error = true; - return; // don't bother executing an invalid executable - } - // create a string that contains the command to call pep8 with the given arguments - QStringList commandArgs = (QStringList() << tempfile.fileName() << KShell::splitArgs(arguments)); - QProcess process; - process.setProcessChannelMode(QProcess::MergedChannels); - // call the pep8 command - process.start(url, commandArgs); - process.waitForFinished(1000); - if ( process.state() != QProcess::NotRunning || ( process.exitCode() != 0 && process.exitCode() != 1 ) ) { - process.kill(); - error = true; - } - else { - QByteArray data = process.readAll(); - QList errors = data.split('\n'); - QRegExp errorFormat("(.*):(\\d*):(\\d*): (.*)", Qt::CaseInsensitive, QRegExp::RegExp2); - DUChainWriteLocker lock; - foreach ( const QByteArray& error, errors ) { - if ( errorFormat.exactMatch(error.data()) ) { - const QStringList texts = errorFormat.capturedTexts(); - bool lineno_ok = false; - bool colno_ok = false; - int lineno = texts.at(2).toInt(&lineno_ok); - int colno = texts.at(3).toInt(&colno_ok); - if ( ! lineno_ok || ! colno_ok ) { - qDebug() << "invalid line / col number:" << texts; - continue; - } - QString error = texts.at(4); - KDevelop::Problem *p = new KDevelop::Problem(); - p->setFinalLocation(DocumentRange(document, KTextEditor::Range(lineno - 1, qMax(colno - 4, 0), - lineno - 1, colno + 4))); - p->setSource(KDevelop::IProblem::Preprocessor); - p->setSeverity(error.startsWith('W') ? KDevelop::IProblem::Hint : KDevelop::IProblem::Warning); - p->setDescription(i18n("PEP8 checker error: %1", error)); - ProblemPointer ptr(p); - topContext->addProblem(ptr); - } - else { - qDebug() << "invalid pep8 error line:" << error; - } - } - } - if ( error ) { - DUChainWriteLocker lock; - KDevelop::Problem *p = new KDevelop::Problem(); - p->setFinalLocation(DocumentRange(document, KTextEditor::Range(0, 0, 0, 0))); - p->setSource(KDevelop::IProblem::Preprocessor); - p->setSeverity(KDevelop::IProblem::Warning); - p->setDescription(i18n("The selected PEP8 syntax checker \"%1\" does not seem to work correctly.", url)); - ProblemPointer ptr(p); - topContext->addProblem(ptr); - } + + auto ls = static_cast(languageSupport()); + QMetaObject::invokeMethod(ls, "updateStyleChecking", Q_ARG(KDevelop::ReferencedTopDUContext, topContext)); } } diff --git a/pythonstylechecking.h b/pythonstylechecking.h new file mode 100644 --- /dev/null +++ b/pythonstylechecking.h @@ -0,0 +1,57 @@ +/* + * + * Copyright 2016 Sven Brauch + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef PYTHONSTYLECHECKING_H +#define PYTHONSTYLECHECKING_H + +#include +#include + +#include + +namespace Python { + +class StyleChecking : public QObject +{ +Q_OBJECT +public: + StyleChecking(QObject* parent=nullptr); + ~StyleChecking(); + void startChecker(const QString& text, const QString& select={}, + const QString& ignore={}, const int maxLineLength=80); + +public Q_SLOTS: + void updateStyleChecking(const KDevelop::ReferencedTopDUContext& top); + void addErrorsToContext(const QVector& errors); + +private Q_SLOTS: + void processOutputStarted(); + +private: + QProcess m_checkerProcess; + KDevelop::ReferencedTopDUContext m_currentlyChecking; + QMutex m_mutex; +}; + +}; + +#endif // PYTHONSTYLECHECKING_H diff --git a/pythonstylechecking.cpp b/pythonstylechecking.cpp new file mode 100644 --- /dev/null +++ b/pythonstylechecking.cpp @@ -0,0 +1,180 @@ +/* + * Copyright 2016 Sven Brauch + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "pythonstylechecking.h" + +#include +#include + +#include +#include +#include + +#include "pythonparsejob.h" +#include "helpers.h" + +namespace Python { + +StyleChecking::StyleChecking(QObject* parent) + : QObject(parent) +{ + qRegisterMetaType("KDevelop::ReferencedTopDUContext"); + connect(&m_checkerProcess, &QProcess::readyReadStandardOutput, + this, &StyleChecking::processOutputStarted); +} + +StyleChecking::~StyleChecking() +{ + if ( m_checkerProcess.state() == QProcess::Running ) { + m_checkerProcess.terminate(); + m_checkerProcess.waitForFinished(100); + } +} + +void StyleChecking::startChecker(const QString& text, const QString& select, + const QString& ignore, const int maxLineLength) +{ + // start up the server + if ( m_checkerProcess.state() == QProcess::NotRunning ) { + auto python = Helper::getPythonExecutablePath(nullptr); + auto serverPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kdevpythonsupport/codestyle.py"); + if ( serverPath.isEmpty() ) { + qWarning() << "setup problem: codestyle.py not found"; + return; + } + m_checkerProcess.start(python, {serverPath}); + m_checkerProcess.waitForStarted(30); + if ( m_checkerProcess.state() != QProcess::Running ) { + qWarning() << "failed to start code checker process"; + return; + } + } + + // send input + QByteArray data = text.toUtf8(); + QByteArray header; + header.append(select.toUtf8()); + header.append("\n"); + header.append(ignore.toUtf8()); + header.append("\n"); + header.append(QByteArray::number(maxLineLength)); + header.append("\n"); + // size, always 10 bytes + header.insert(0, QString::number(header.size() + data.size()).leftJustified(10)); + m_checkerProcess.write(header); + m_checkerProcess.write(data); +} + +void StyleChecking::addErrorsToContext(const QVector& errors) +{ + QRegExp errorFormat("(.*):(\\d*):(\\d*): (.*)", Qt::CaseInsensitive, QRegExp::RegExp2); + DUChainWriteLocker lock; + auto document = m_currentlyChecking->url(); + for ( const auto& error : errors ) { + if ( errorFormat.exactMatch(error) ) { + const QStringList texts = errorFormat.capturedTexts(); + bool lineno_ok = false; + bool colno_ok = false; + int lineno = texts.at(2).toInt(&lineno_ok); + int colno = texts.at(3).toInt(&colno_ok); + if ( ! lineno_ok || ! colno_ok ) { + qDebug() << "invalid line / col number:" << texts; + continue; + } + QString error = texts.at(4); + KDevelop::Problem* p = new KDevelop::Problem(); + p->setFinalLocation(DocumentRange(document, KTextEditor::Range(lineno - 1, qMax(colno - 4, 0), + lineno - 1, colno + 4))); + p->setSource(KDevelop::IProblem::Preprocessor); + p->setSeverity(error.startsWith('W') ? KDevelop::IProblem::Hint : KDevelop::IProblem::Warning); + p->setDescription(i18n("PEP8 checker error: %1", error)); + ProblemPointer ptr(p); + m_currentlyChecking->addProblem(ptr); + } + else { + qDebug() << "invalid pep8 error line:" << error; + } + } +// if ( error ) { +// DUChainWriteLocker lock; +// KDevelop::Problem *p = new KDevelop::Problem(); +// p->setFinalLocation(DocumentRange(document, KTextEditor::Range(0, 0, 0, 0))); +// p->setSource(KDevelop::IProblem::Preprocessor); +// p->setSeverity(KDevelop::IProblem::Warning); +// p->setDescription(i18n("The PEP8 syntax checker does not seem to work correctly.")); +// ProblemPointer ptr(p); +// topContext->addProblem(ptr); +// } + + m_currentlyChecking->setFeatures((TopDUContext::Features) ( m_currentlyChecking->features() | ParseJob::PEP8Checking )); +} + +void StyleChecking::processOutputStarted() +{ + // read output size + QByteArray size_d; + size_d = m_checkerProcess.read(10); + bool ok; + auto size = size_d.toInt(&ok); + if ( !ok || size <= 0 ) { + return; + } + + // read and process actual output + QByteArray buf; + QVector errors; + while ( size > 0 ) { + auto d = m_checkerProcess.read(size); + buf.append(d); + size -= d.size(); + + auto ofs = -1; + while ( (ofs = buf.indexOf('\n', ofs+1)) != -1 ) { + errors.append(buf.left(ofs)); + } + } + addErrorsToContext(errors); + + // done, unlock mutex + m_currentlyChecking = nullptr; + m_mutex.unlock(); +} + +void StyleChecking::updateStyleChecking(const KDevelop::ReferencedTopDUContext& top) +{ + if ( !top ) { + return; + } + auto url = top->url(); + IDocument* idoc = ICore::self()->documentController()->documentForUrl(url.toUrl()); + if ( !idoc || !idoc->textDocument() || top->features() & ParseJob::PEP8Checking ) { + return; + } + auto text = idoc->textDocument()->text(); + + if ( !m_mutex.tryLock(1000) ) { + return; + } + m_currentlyChecking = top; + startChecker(text); +} + +};