Add QmlJS semantic checker.

* Add SemanticHighlighter to QmlJSTextEditor to update the semantic info
  in a background thread.
* Add QmlJS::Check to run semantic checks on qml and js documents.
* Add a check for incorrect property names.
* Fix hoverhandler to show tool tips from extra selections over help
  tooltips.
This commit is contained in:
Christian Kamm
2010-02-16 10:36:09 +01:00
parent 98a0757916
commit da3679066e
11 changed files with 571 additions and 62 deletions

View File

@@ -36,6 +36,7 @@
#include <qmljs/qmljsindenter.h>
#include <qmljs/qmljsinterpreter.h>
#include <qmljs/qmljsbind.h>
#include <qmljs/qmljscheck.h>
#include <qmljs/qmljsevaluate.h>
#include <qmljs/qmljsdocument.h>
#include <qmljs/parser/qmljsastvisitor_p.h>
@@ -563,6 +564,11 @@ QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) :
m_methodCombo(0),
m_modelManager(0)
{
qRegisterMetaType<SemanticInfo>("SemanticInfo");
m_semanticHighlighter = new SemanticHighlighter(this);
m_semanticHighlighter->start();
setParenthesesMatchingEnabled(true);
setMarksVisible(true);
setCodeFoldingSupported(true);
@@ -589,10 +595,15 @@ QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) :
connect(m_modelManager, SIGNAL(documentUpdated(QmlJS::Document::Ptr)),
this, SLOT(onDocumentUpdated(QmlJS::Document::Ptr)));
}
connect(m_semanticHighlighter, SIGNAL(changed(SemanticInfo)),
this, SLOT(updateSemanticInfo(SemanticInfo)));
}
QmlJSTextEditor::~QmlJSTextEditor()
{
m_semanticHighlighter->abort();
m_semanticHighlighter->wait();
}
SemanticInfo QmlJSTextEditor::semanticInfo() const
@@ -660,62 +671,9 @@ void QmlJSTextEditor::onDocumentUpdated(QmlJS::Document::Ptr doc)
if (doc->ast()) {
// got a correctly parsed (or recovered) file.
// create the ranges and update the semantic info.
CreateRanges createRanges;
SemanticInfo sem;
sem.snapshot = m_modelManager->snapshot();
sem.document = doc;
sem.ranges = createRanges(document(), doc);
// Refresh the ids
FindIdDeclarations updateIds;
sem.idLocations = updateIds(doc);
if (doc->isParsedCorrectly()) {
FindDeclarations findDeclarations;
sem.declarations = findDeclarations(doc->ast());
QStringList items;
items.append(tr("<Select Symbol>"));
foreach (Declaration decl, sem.declarations)
items.append(decl.text);
m_methodCombo->clear();
m_methodCombo->addItems(items);
updateMethodBoxIndex();
}
m_semanticInfo = sem;
const SemanticHighlighter::Source source = currentSource(/*force = */ true);
m_semanticHighlighter->rehighlight(source);
}
QList<QTextEdit::ExtraSelection> selections;
foreach (const DiagnosticMessage &d, doc->diagnosticMessages()) {
if (d.isWarning())
continue;
const int line = d.loc.startLine;
const int column = qMax(1U, d.loc.startColumn);
QTextEdit::ExtraSelection sel;
QTextCursor c(document()->findBlockByNumber(line - 1));
sel.cursor = c;
sel.cursor.setPosition(c.position() + column - 1);
if (sel.cursor.atBlockEnd())
sel.cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
else
sel.cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
sel.format.setUnderlineColor(Qt::red);
sel.format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
sel.format.setToolTip(d.message);
selections.append(sel);
}
setExtraSelections(CodeWarningsSelection, selections);
}
void QmlJSTextEditor::jumpToMethod(int index)
@@ -1150,3 +1108,196 @@ QString QmlJSTextEditor::insertParagraphSeparator(const QTextCursor &) const
return QLatin1String("}\n");
}
static void appendExtraSelectionsForMessages(
QList<QTextEdit::ExtraSelection> *selections,
const QList<DiagnosticMessage> &messages,
const QTextDocument *document)
{
foreach (const DiagnosticMessage &d, messages) {
if (d.isWarning())
continue;
const int line = d.loc.startLine;
const int column = qMax(1U, d.loc.startColumn);
QTextEdit::ExtraSelection sel;
QTextCursor c(document->findBlockByNumber(line - 1));
sel.cursor = c;
sel.cursor.setPosition(c.position() + column - 1);
if (sel.cursor.atBlockEnd())
sel.cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
else
sel.cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
sel.format.setUnderlineColor(Qt::red);
sel.format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
sel.format.setToolTip(d.message);
selections->append(sel);
}
}
void QmlJSTextEditor::semanticRehighlight()
{
m_semanticHighlighter->rehighlight(currentSource());
}
void QmlJSTextEditor::updateSemanticInfo(const SemanticInfo &semanticInfo)
{
if (semanticInfo.revision() != document()->revision()) {
// got outdated semantic info
semanticRehighlight();
return;
}
m_semanticInfo = semanticInfo;
Document::Ptr doc = semanticInfo.document;
// create the ranges
CreateRanges createRanges;
m_semanticInfo.ranges = createRanges(document(), doc);
// Refresh the ids
FindIdDeclarations updateIds;
m_semanticInfo.idLocations = updateIds(doc);
if (doc->isParsedCorrectly()) {
FindDeclarations findDeclarations;
m_semanticInfo.declarations = findDeclarations(doc->ast());
QStringList items;
items.append(tr("<Select Symbol>"));
foreach (Declaration decl, m_semanticInfo.declarations)
items.append(decl.text);
m_methodCombo->clear();
m_methodCombo->addItems(items);
updateMethodBoxIndex();
}
// update warning/error extra selections
QList<QTextEdit::ExtraSelection> selections;
appendExtraSelectionsForMessages(&selections, doc->diagnosticMessages(), document());
appendExtraSelectionsForMessages(&selections, m_semanticInfo.semanticMessages, document());
setExtraSelections(CodeWarningsSelection, selections);
}
SemanticHighlighter::Source QmlJSTextEditor::currentSource(bool force)
{
int line = 0, column = 0;
convertPosition(position(), &line, &column);
const Snapshot snapshot = m_modelManager->snapshot();
const QString fileName = file()->fileName();
QString code;
if (force || m_semanticInfo.revision() != document()->revision())
code = toPlainText(); // get the source code only when needed.
const unsigned revision = document()->revision();
SemanticHighlighter::Source source(snapshot, fileName, code,
line, column, revision);
source.force = force;
return source;
}
SemanticHighlighter::SemanticHighlighter(QObject *parent)
: QThread(parent),
m_done(false)
{
}
SemanticHighlighter::~SemanticHighlighter()
{
}
void SemanticHighlighter::abort()
{
QMutexLocker locker(&m_mutex);
m_done = true;
m_condition.wakeOne();
}
void SemanticHighlighter::rehighlight(const Source &source)
{
QMutexLocker locker(&m_mutex);
m_source = source;
m_condition.wakeOne();
}
bool SemanticHighlighter::isOutdated()
{
QMutexLocker locker(&m_mutex);
const bool outdated = ! m_source.fileName.isEmpty() || m_done;
return outdated;
}
void SemanticHighlighter::run()
{
setPriority(QThread::IdlePriority);
forever {
m_mutex.lock();
while (! (m_done || ! m_source.fileName.isEmpty()))
m_condition.wait(&m_mutex);
const bool done = m_done;
const Source source = m_source;
m_source.clear();
m_mutex.unlock();
if (done)
break;
const SemanticInfo info = semanticInfo(source);
if (! isOutdated()) {
m_mutex.lock();
m_lastSemanticInfo = info;
m_mutex.unlock();
emit changed(info);
}
}
}
SemanticInfo SemanticHighlighter::semanticInfo(const Source &source)
{
m_mutex.lock();
const int revision = m_lastSemanticInfo.revision();
m_mutex.unlock();
Snapshot snapshot;
Document::Ptr doc;
if (! source.force && revision == source.revision) {
m_mutex.lock();
snapshot = m_lastSemanticInfo.snapshot;
doc = m_lastSemanticInfo.document;
m_mutex.unlock();
}
if (! doc) {
snapshot = source.snapshot;
doc = snapshot.documentFromSource(source.code, source.fileName);
// ### This doesn't really work: what if snapshot doesn't have the doc?
if (snapshot.document(source.fileName)->qmlProgram())
doc->parseQml();
else if (snapshot.document(source.fileName)->jsProgram())
doc->parseJavaScript();
}
SemanticInfo semanticInfo;
semanticInfo.snapshot = snapshot;
semanticInfo.document = doc;
Check checker(doc, snapshot);
semanticInfo.semanticMessages = checker();
return semanticInfo;
}

View File

@@ -34,6 +34,10 @@
#include <qmljs/qmljsscanner.h>
#include <texteditor/basetexteditor.h>
#include <QtCore/QWaitCondition>
#include <QtCore/QMutex>
#include <QtCore/QThread>
QT_BEGIN_NAMESPACE
class QComboBox;
class QTimer;
@@ -117,6 +121,75 @@ public: // attributes
QList<Range> ranges;
QHash<QString, QList<QmlJS::AST::SourceLocation> > idLocations;
QList<Declaration> declarations;
// these are in addition to the parser messages in the document
QList<QmlJS::DiagnosticMessage> semanticMessages;
};
class SemanticHighlighter: public QThread
{
Q_OBJECT
public:
SemanticHighlighter(QObject *parent = 0);
virtual ~SemanticHighlighter();
void abort();
struct Source
{
QmlJS::Snapshot snapshot;
QString fileName;
QString code;
int line;
int column;
int revision;
bool force;
Source()
: line(0), column(0), revision(0), force(false)
{ }
Source(const QmlJS::Snapshot &snapshot,
const QString &fileName,
const QString &code,
int line, int column,
int revision)
: snapshot(snapshot), fileName(fileName),
code(code), line(line), column(column),
revision(revision), force(false)
{ }
void clear()
{
snapshot = QmlJS::Snapshot();
fileName.clear();
code.clear();
line = 0;
column = 0;
revision = 0;
force = false;
}
};
void rehighlight(const Source &source);
Q_SIGNALS:
void changed(const SemanticInfo &semanticInfo);
protected:
virtual void run();
private:
bool isOutdated();
SemanticInfo semanticInfo(const Source &source);
private:
QMutex m_mutex;
QWaitCondition m_condition;
bool m_done;
Source m_source;
SemanticInfo m_lastSemanticInfo;
};
class QmlJSTextEditor : public TextEditor::BaseTextEditor
@@ -154,6 +227,9 @@ private slots:
// refactoring ops
void renameIdUnderCursor();
void semanticRehighlight();
void updateSemanticInfo(const SemanticInfo &semanticInfo);
protected:
void contextMenuEvent(QContextMenuEvent *e);
TextEditor::BaseTextEditorEditable *createEditableInterface();
@@ -173,6 +249,8 @@ private:
QString wordUnderCursor() const;
SemanticHighlighter::Source currentSource(bool force = false);
const Context m_context;
QTimer *m_updateDocumentTimer;
@@ -183,6 +261,7 @@ private:
QTextCharFormat m_occurrencesUnusedFormat;
QTextCharFormat m_occurrenceRenameFormat;
SemanticHighlighter *m_semanticHighlighter;
SemanticInfo m_semanticInfo;
};

View File

@@ -165,7 +165,7 @@ void HoverHandler::updateHelpIdAndTooltip(TextEditor::ITextEditor *editor, int p
}
QString symbolName = QLatin1String("<unknown>");
if (m_helpId.isEmpty()) {
if (m_helpId.isEmpty() && m_toolTip.isEmpty()) {
AST::Node *node = semanticInfo.nodeUnderCursor(pos);
if (node && !(AST::cast<AST::StringLiteral *>(node) != 0 || AST::cast<AST::NumericLiteral *>(node) != 0)) {
AST::Node *declaringMember = semanticInfo.declaringMember(pos);