Commit da367906 authored by Christian Kamm's avatar Christian Kamm

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.
parent 98a07579
......@@ -16,7 +16,8 @@ HEADERS += \
$$PWD/qmljsdocument.h \
$$PWD/qmljsscanner.h \
$$PWD/qmljsinterpreter.h \
$$PWD/qmljslink.h
$$PWD/qmljslink.h \
$$PWD/qmljscheck.h
SOURCES += \
$$PWD/qmljsbind.cpp \
......@@ -25,7 +26,8 @@ SOURCES += \
$$PWD/qmljsscanner.cpp \
$$PWD/qmljsinterpreter.cpp \
$$PWD/qmljsmetatypesystem.cpp \
$$PWD/qmljslink.cpp
$$PWD/qmljslink.cpp \
$$PWD/qmljscheck.cpp
contains(QT_CONFIG, declarative) {
QT += declarative
......
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** 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.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/
#include "qmljscheck.h"
#include "qmljsbind.h"
#include "qmljsinterpreter.h"
#include "parser/qmljsast_p.h"
#include <QtCore/QDebug>
using namespace QmlJS;
using namespace QmlJS::AST;
using namespace QmlJS::Interpreter;
Check::Check(Document::Ptr doc, const Snapshot &snapshot)
: _doc(doc)
, _snapshot(snapshot)
, _context(&_engine)
, _link(&_context, doc, snapshot)
{
}
Check::~Check()
{
}
QList<DiagnosticMessage> Check::operator()()
{
_messages.clear();
Node::accept(_doc->ast(), this);
return _messages;
}
bool Check::visit(UiProgram *ast)
{
// build the initial scope chain
if (ast->members && ast->members->member)
_link.scopeChainAt(_doc, ast->members->member);
return true;
}
bool Check::visit(UiObjectDefinition *ast)
{
const ObjectValue *oldScopeObject = _context.qmlScopeObject();
_context.setQmlScopeObject(_doc->bind()->findQmlObject(ast));
Node::accept(ast->initializer, this);
_context.setQmlScopeObject(oldScopeObject);
return false;
}
bool Check::visit(UiObjectBinding *ast)
{
checkScopeObjectMember(ast->qualifiedId);
const ObjectValue *oldScopeObject = _context.qmlScopeObject();
_context.setQmlScopeObject(_doc->bind()->findQmlObject(ast));
Node::accept(ast->initializer, this);
_context.setQmlScopeObject(oldScopeObject);
return false;
}
bool Check::visit(UiScriptBinding *ast)
{
checkScopeObjectMember(ast->qualifiedId);
return true;
}
bool Check::visit(UiArrayBinding *ast)
{
checkScopeObjectMember(ast->qualifiedId);
return true;
}
void Check::checkScopeObjectMember(const AST::UiQualifiedId *id)
{
const ObjectValue *scopeObject = _context.qmlScopeObject();
if (! id)
return; // ### error?
const QString propertyName = id->name->asString();
if (propertyName == QLatin1String("id") && ! id->next)
return;
// attached properties
if (! propertyName.isEmpty() && propertyName[0].isUpper())
scopeObject = _context.typeEnvironment(_doc.data());
const Value *value = scopeObject->lookupMember(propertyName, &_context);
if (!value) {
error(id->identifierToken,
QString("'%1' is not a valid property name").arg(propertyName));
}
// ### check for rest of qualifiedId
}
void Check::error(const AST::SourceLocation &loc, const QString &message)
{
_messages.append(DiagnosticMessage(DiagnosticMessage::Error, loc, message));
}
void Check::warning(const AST::SourceLocation &loc, const QString &message)
{
_messages.append(DiagnosticMessage(DiagnosticMessage::Warning, loc, message));
}
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** 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.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/
#ifndef QMLJSCHECK_H
#define QMLJSCHECK_H
#include <qmljs/qmljsdocument.h>
#include <qmljs/qmljsinterpreter.h>
#include <qmljs/qmljslink.h>
#include <qmljs/parser/qmljsastvisitor_p.h>
namespace QmlJS {
class QMLJS_EXPORT Check: protected AST::Visitor
{
public:
Check(Document::Ptr doc, const Snapshot &snapshot);
virtual ~Check();
QList<DiagnosticMessage> operator()();
protected:
virtual bool visit(AST::UiProgram *ast);
virtual bool visit(AST::UiObjectDefinition *ast);
virtual bool visit(AST::UiObjectBinding *ast);
virtual bool visit(AST::UiScriptBinding *ast);
virtual bool visit(AST::UiArrayBinding *ast);
private:
void checkScopeObjectMember(const AST::UiQualifiedId *id);
void warning(const AST::SourceLocation &loc, const QString &message);
void error(const AST::SourceLocation &loc, const QString &message);
Document::Ptr _doc;
Snapshot _snapshot;
Interpreter::Engine _engine;
Interpreter::Context _context;
Link _link;
QList<DiagnosticMessage> _messages;
};
} // namespace QmlJS
#endif // QMLJSCHECK_H
......@@ -216,6 +216,19 @@ void Snapshot::insert(const Document::Ptr &document)
_documents.insert(document->fileName(), document);
}
Document::Ptr Snapshot::documentFromSource(const QString &code,
const QString &fileName) const
{
Document::Ptr newDoc = Document::create(fileName);
if (Document::Ptr thisDocument = document(fileName)) {
newDoc->_documentRevision = thisDocument->_documentRevision;
}
newDoc->setSource(code);
return newDoc;
}
QList<Document::Ptr> Snapshot::importedDocuments(const Document::Ptr &doc, const QString &importPath) const
{
// ### TODO: maybe we should add all imported documents in the parse Document::parse() method, regardless of whether they're in the path or not.
......
......@@ -41,6 +41,7 @@
namespace QmlJS {
class Bind;
class Snapshot;
class QMLJS_EXPORT Document
{
......@@ -96,6 +97,9 @@ private:
QString _path;
QString _componentName;
QString _source;
// for documentFromSource
friend class Snapshot;
};
class QMLJS_EXPORT Snapshot
......@@ -118,6 +122,9 @@ public:
Document::Ptr document(const QString &fileName) const
{ return _documents.value(fileName); }
Document::Ptr documentFromSource(const QString &code,
const QString &fileName) const;
QList<Document::Ptr> importedDocuments(const Document::Ptr &doc, const QString &importPath) const;
QMap<QString, Document::Ptr> componentsDefinedByImportedDocuments(const Document::Ptr &doc, const QString &importPath) const;
};
......
......@@ -686,7 +686,9 @@ void StringValue::accept(ValueVisitor *visitor) const
Context::Context(Engine *engine)
: _engine(engine),
_lookupMode(JSLookup)
_lookupMode(JSLookup),
_qmlScopeObjectIndex(-1),
_qmlScopeObjectSet(false)
{
}
......@@ -739,6 +741,39 @@ void Context::pushScope(const ObjectValue *object)
void Context::popScope()
{
_scopeChain.removeLast();
if (_scopeChain.length() <= _qmlScopeObjectIndex)
_qmlScopeObjectSet = false;
}
// Marks this to be the location where a scope object can be inserted.
void Context::markQmlScopeObject()
{
_qmlScopeObjectIndex = _scopeChain.length();
}
// Sets or inserts the scope object if scopeObject != 0, removes it otherwise.
void Context::setQmlScopeObject(const ObjectValue *scopeObject)
{
if (_qmlScopeObjectSet) {
if (scopeObject == 0) {
_scopeChain.removeAt(_qmlScopeObjectIndex);
_qmlScopeObjectSet = false;
} else {
_scopeChain[_qmlScopeObjectIndex] = scopeObject;
}
} else if (scopeObject != 0 && _scopeChain.length() >= _qmlScopeObjectIndex) {
_scopeChain.insert(_qmlScopeObjectIndex, scopeObject);
_qmlScopeObjectSet = true;
}
}
// Gets the scope object, if set. Returns 0 otherwise.
const ObjectValue *Context::qmlScopeObject() const
{
if (!_qmlScopeObjectSet)
return 0;
else
return _scopeChain[_qmlScopeObjectIndex];
}
const Value *Context::lookup(const QString &name)
......
......@@ -241,6 +241,10 @@ public:
void pushScope(const ObjectValue *object);
void popScope();
void markQmlScopeObject();
void setQmlScopeObject(const ObjectValue *scopeObject);
const ObjectValue *qmlScopeObject() const;
const Value *lookup(const QString &name);
const ObjectValue *lookupType(const Document *doc, AST::UiQualifiedId *qmlTypeName);
......@@ -253,8 +257,10 @@ private:
Engine *_engine;
LookupMode _lookupMode;
QHash<const ObjectValue *, Properties> _properties;
ScopeChain _scopeChain;
QHash<const Document *, const ObjectValue *> _typeEnvironments;
ScopeChain _scopeChain;
int _qmlScopeObjectIndex;
bool _qmlScopeObjectSet;
};
class QMLJS_EXPORT Reference: public Value
......
......@@ -102,8 +102,11 @@ void Link::pushScopeChainForComponent(Document::Ptr doc, QStringList *linkedDocs
if (bind->rootObjectValue())
_context->pushScope(bind->rootObjectValue());
if (scopeObject && scopeObject != bind->rootObjectValue())
_context->pushScope(scopeObject);
if (scopeObject) {
_context->markQmlScopeObject();
if (scopeObject != bind->rootObjectValue())
_context->setQmlScopeObject(scopeObject);
}
const QStringList &includedScripts = bind->includedScripts();
for (int index = includedScripts.size() - 1; index != -1; --index) {
......
......@@ -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();