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

@@ -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

View File

@@ -0,0 +1,140 @@
/**************************************************************************
**
** 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));
}

View File

@@ -0,0 +1,73 @@
/**************************************************************************
**
** 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

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {