qmllint: parse JS files for methods

Change-Id: I3888231ac82f9babd51e6332af3c5457bf3c9141
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Ulf Hermann 2019-11-08 18:43:31 +01:00
parent 1cd494fbfb
commit 0989e5a2b7
5 changed files with 206 additions and 140 deletions

View File

@ -0,0 +1,3 @@
function foo() {
return "ttt"
}

View File

@ -0,0 +1,6 @@
import QtQml 2.0
import "Methods.js" as Methods
QtObject {
objectName: Methods.foo()
}

View File

@ -159,6 +159,7 @@ void TestQmllint::cleanQmlCode_data()
QTest::newRow("qmldirAndQmltypes") << QStringLiteral("qmldirAndQmltypes.qml"); QTest::newRow("qmldirAndQmltypes") << QStringLiteral("qmldirAndQmltypes.qml");
QTest::newRow("forLoop") << QStringLiteral("forLoop.qml"); QTest::newRow("forLoop") << QStringLiteral("forLoop.qml");
QTest::newRow("esmodule") << QStringLiteral("esmodule.mjs"); QTest::newRow("esmodule") << QStringLiteral("esmodule.mjs");
QTest::newRow("methodsInJavascript") << QStringLiteral("javascriptMethods.qml");
} }
void TestQmllint::cleanQmlCode() void TestQmllint::cleanQmlCode()

View File

@ -40,6 +40,11 @@
#include <QtCore/qdiriterator.h> #include <QtCore/qdiriterator.h>
#include <QtCore/qscopedvaluerollback.h> #include <QtCore/qscopedvaluerollback.h>
static const QString prefixedName(const QString &prefix, const QString &name)
{
return prefix.isEmpty() ? name : (prefix + QLatin1Char('.') + name);
}
static QQmlDirParser createQmldirParserForFile(const QString &filename) static QQmlDirParser createQmldirParserForFile(const QString &filename)
{ {
QFile f(filename); QFile f(filename);
@ -67,6 +72,150 @@ void FindUnqualifiedIDVisitor::leaveEnvironment()
m_currentScope = m_currentScope->parentScope(); m_currentScope = m_currentScope->parentScope();
} }
void FindUnqualifiedIDVisitor::parseHeaders(QQmlJS::AST::UiHeaderItemList *header)
{
using namespace QQmlJS::AST;
while (header) {
if (auto import = cast<UiImport *>(header->headerItem)) {
if (import->version) {
QString path;
auto uri = import->importUri;
while (uri) {
path.append(uri->name);
path.append("/");
uri = uri->next;
}
path.chop(1);
QString prefix = QLatin1String("");
if (import->asToken.isValid()) {
prefix += import->importId + QLatin1Char('.');
}
importHelper(path, prefix, import->version->majorVersion,
import->version->minorVersion);
}
}
header = header->next;
}
}
void FindUnqualifiedIDVisitor::parseMembers(QQmlJS::AST::UiObjectMemberList *member,
ScopeTree *scope)
{
using namespace QQmlJS::AST;
// member should be the sole element
Q_ASSERT(!member->next);
Q_ASSERT(member && member->member->kind == UiObjectMember::Kind_UiObjectDefinition);
auto definition = cast<UiObjectDefinition *>(member->member);
auto qualifiedId = definition->qualifiedTypeNameId;
while (qualifiedId && qualifiedId->next) {
qualifiedId = qualifiedId->next;
}
scope->setSuperclassName(qualifiedId->name.toString());
UiObjectMemberList *initMembers = definition->initializer->members;
while (initMembers) {
switch (initMembers->member->kind) {
case UiObjectMember::Kind_UiArrayBinding: {
// nothing to do
break;
}
case UiObjectMember::Kind_UiEnumDeclaration: {
// nothing to do
break;
}
case UiObjectMember::Kind_UiObjectBinding: {
// nothing to do
break;
}
case UiObjectMember::Kind_UiObjectDefinition: {
// creates nothing accessible
break;
}
case UiObjectMember::Kind_UiPublicMember: {
auto publicMember = cast<UiPublicMember *>(initMembers->member);
switch (publicMember->type) {
case UiPublicMember::Signal: {
UiParameterList *param = publicMember->parameters;
MetaMethod method;
method.setMethodType(MetaMethod::Signal);
method.setMethodName(publicMember->name.toString());
while (param) {
method.addParameter(param->name.toString(), param->type->name.toString());
param = param->next;
}
scope->addMethod(method);
break;
}
case UiPublicMember::Property: {
MetaProperty prop {
publicMember->name.toString(),
publicMember->typeModifier.toString(),
false,
false,
false,
0
};
scope->addProperty(prop);
break;
}
}
break;
}
case UiObjectMember::Kind_UiScriptBinding: {
// does not create anything new, ignore
break;
}
case UiObjectMember::Kind_UiSourceElement: {
auto sourceElement = cast<UiSourceElement *>(initMembers->member);
if (FunctionExpression *fexpr = sourceElement->sourceElement->asFunctionDefinition()) {
MetaMethod method;
method.setMethodName(fexpr->name.toString());
method.setMethodType(MetaMethod::Method);
FormalParameterList *parameters = fexpr->formals;
while (parameters) {
method.addParameter(parameters->element->bindingIdentifier.toString(), "");
parameters = parameters->next;
}
scope->addMethod(method);
} else if (ClassExpression *clexpr =
sourceElement->sourceElement->asClassDefinition()) {
const MetaProperty prop { clexpr->name.toString(), "", false, false, false, 1 };
scope->addProperty(prop);
} else if (cast<VariableStatement *>(sourceElement->sourceElement)) {
// nothing to do
} else {
const auto loc = sourceElement->firstSourceLocation();
m_colorOut.writeUncolored(
"unsupportedd sourceElement at "
+ QString::fromLatin1("%1:%2: ").arg(loc.startLine).arg(loc.startColumn)
+ QString::number(sourceElement->sourceElement->kind));
}
break;
}
default: {
m_colorOut.writeUncolored("unsupported element of kind "
+ QString::number(initMembers->member->kind));
}
}
initMembers = initMembers->next;
}
}
void FindUnqualifiedIDVisitor::parseProgram(QQmlJS::AST::Program *program, ScopeTree *scope)
{
using namespace QQmlJS::AST;
for (auto *statement = program->statements; statement; statement = statement->next) {
if (auto *function = cast<FunctionDeclaration *>(statement->statement)) {
MetaMethod method(function->name.toString());
method.setMethodType(MetaMethod::Method);
for (auto *parameters = function->formals; parameters; parameters = parameters->next)
method.addParameter(parameters->element->bindingIdentifier.toString(), "");
scope->addMethod(method);
}
}
}
enum ImportVersion { FullyVersioned, PartiallyVersioned, Unversioned, BasePath }; enum ImportVersion { FullyVersioned, PartiallyVersioned, Unversioned, BasePath };
QStringList completeImportPaths(const QString &uri, const QString &basePath, int vmaj, int vmin) QStringList completeImportPaths(const QString &uri, const QString &basePath, int vmaj, int vmin)
@ -168,7 +317,7 @@ FindUnqualifiedIDVisitor::Import FindUnqualifiedIDVisitor::readQmldir(const QStr
auto mo = qmlComponents.find(it.key()); auto mo = qmlComponents.find(it.key());
if (mo == qmlComponents.end()) if (mo == qmlComponents.end())
mo = qmlComponents.insert(it.key(), localQmlFile2ScopeTree(filePath)); mo = qmlComponents.insert(it.key(), localFile2ScopeTree(filePath));
(*mo)->addExport( (*mo)->addExport(
it.key(), reader.typeNamespace(), it.key(), reader.typeNamespace(),
@ -203,11 +352,11 @@ void FindUnqualifiedIDVisitor::processImport(const QString &prefix, const FindUn
// add objects // add objects
for (auto it = import.objects.begin(); it != import.objects.end(); ++it) { for (auto it = import.objects.begin(); it != import.objects.end(); ++it) {
const auto &val = it.value(); const auto &val = it.value();
m_exportedName2Scope.insert(prefix + val->className(), val); m_exportedName2Scope.insert(prefixedName(prefix, val->className()), val);
const auto exports = val->exports(); const auto exports = val->exports();
for (const auto &valExport : exports) for (const auto &valExport : exports)
m_exportedName2Scope.insert(prefix + valExport.type(), val); m_exportedName2Scope.insert(prefixedName(prefix, valExport.type()), val);
const auto enums = val->enums(); const auto enums = val->enums();
for (const auto &valEnum : enums) for (const auto &valEnum : enums)
@ -248,11 +397,12 @@ void FindUnqualifiedIDVisitor::importHelper(const QString &module, const QString
} }
} }
ScopeTree *FindUnqualifiedIDVisitor::localQmlFile2ScopeTree(const QString &filePath) ScopeTree *FindUnqualifiedIDVisitor::localFile2ScopeTree(const QString &filePath)
{ {
using namespace QQmlJS::AST; using namespace QQmlJS::AST;
auto scope = new ScopeTree(ScopeType::QMLScope); auto scope = new ScopeTree(ScopeType::QMLScope);
QString baseName = QFileInfo { filePath }.baseName(); const QFileInfo info { filePath };
QString baseName = info.baseName();
scope->setClassName(baseName.endsWith(".ui") ? baseName.chopped(3) : baseName); scope->setClassName(baseName.endsWith(".ui") ? baseName.chopped(3) : baseName);
QFile file(filePath); QFile file(filePath);
if (!file.open(QFile::ReadOnly)) if (!file.open(QFile::ReadOnly))
@ -264,146 +414,48 @@ ScopeTree *FindUnqualifiedIDVisitor::localQmlFile2ScopeTree(const QString &fileP
QQmlJS::Engine engine; QQmlJS::Engine engine;
QQmlJS::Lexer lexer(&engine); QQmlJS::Lexer lexer(&engine);
lexer.setCode(code, 1, true); const QString lowerSuffix = info.suffix().toLower();
const bool isJavaScript = (lowerSuffix == QLatin1String("js") || lowerSuffix == QLatin1String("mjs"));
const bool isESModule = lowerSuffix == QLatin1String("mjs");
lexer.setCode(code, /*line = */ 1, /*qmlMode=*/ !isJavaScript);
QQmlJS::Parser parser(&engine); QQmlJS::Parser parser(&engine);
if (!parser.parse()) {
const bool success = isJavaScript ? (isESModule ? parser.parseModule()
: parser.parseProgram())
: parser.parse();
if (!success)
return scope; return scope;
if (!isJavaScript) {
QQmlJS::AST::UiProgram *program = parser.ast();
parseHeaders(program->headers);
parseMembers(program->members, scope);
} else {
// TODO: Anything special to do with ES modules here?
parseProgram(QQmlJS::AST::cast<QQmlJS::AST::Program *>(parser.rootNode()), scope);
} }
QQmlJS::AST::UiProgram *program = parser.ast();
auto header = program->headers;
while (header) {
if (auto import = cast<UiImport *>(header->headerItem)) {
if (import->version) {
QString path;
auto uri = import->importUri;
while (uri) {
path.append(uri->name);
path.append("/");
uri = uri->next;
}
path.chop(1);
QString prefix = QLatin1String("");
if (import->asToken.isValid()) {
prefix += import->importId + QLatin1Char('.');
}
importHelper(path, prefix, import->version->majorVersion,
import->version->minorVersion);
}
}
header = header->next;
}
auto member = program->members;
// member should be the sole element
Q_ASSERT(!member->next);
Q_ASSERT(member && member->member->kind == UiObjectMember::Kind_UiObjectDefinition);
auto definition = cast<UiObjectDefinition *>(member->member);
auto qualifiedId = definition->qualifiedTypeNameId;
while (qualifiedId && qualifiedId->next) {
qualifiedId = qualifiedId->next;
}
scope->setSuperclassName(qualifiedId->name.toString());
UiObjectMemberList *initMembers = definition->initializer->members;
while (initMembers) {
switch (initMembers->member->kind) {
case UiObjectMember::Kind_UiArrayBinding: {
// nothing to do
break;
}
case UiObjectMember::Kind_UiEnumDeclaration: {
// nothing to do
break;
}
case UiObjectMember::Kind_UiObjectBinding: {
// nothing to do
break;
}
case UiObjectMember::Kind_UiObjectDefinition: {
// creates nothing accessible
break;
}
case UiObjectMember::Kind_UiPublicMember: {
auto publicMember = cast<UiPublicMember *>(initMembers->member);
switch (publicMember->type) {
case UiPublicMember::Signal: {
UiParameterList *param = publicMember->parameters;
MetaMethod method;
method.setMethodType(MetaMethod::Signal);
method.setMethodName(publicMember->name.toString());
while (param) {
method.addParameter(param->name.toString(), param->type->name.toString());
param = param->next;
}
scope->addMethod(method);
break;
}
case UiPublicMember::Property: {
const MetaProperty property {
publicMember->name.toString(),
publicMember->typeModifier.toString(),
false,
false,
false,
0
};
scope->addProperty(property);
break;
}
}
break;
}
case UiObjectMember::Kind_UiScriptBinding: {
// does not create anything new, ignore
break;
}
case UiObjectMember::Kind_UiSourceElement: {
auto sourceElement = cast<UiSourceElement *>(initMembers->member);
if (FunctionExpression *fexpr = sourceElement->sourceElement->asFunctionDefinition()) {
MetaMethod method;
method.setMethodName(fexpr->name.toString());
method.setMethodType(MetaMethod::Method);
FormalParameterList *parameters = fexpr->formals;
while (parameters) {
method.addParameter(parameters->element->bindingIdentifier.toString(), "");
parameters = parameters->next;
}
scope->addMethod(method);
} else if (ClassExpression *clexpr =
sourceElement->sourceElement->asClassDefinition()) {
const MetaProperty prop { clexpr->name.toString(), "", false, false, false, 1 };
scope->addProperty(prop);
} else if (cast<VariableStatement *>(sourceElement->sourceElement)) {
// nothing to do
} else {
const auto loc = sourceElement->firstSourceLocation();
m_colorOut.writeUncolored(
"unsupportedd sourceElement at "
+ QString::fromLatin1("%1:%2: ").arg(loc.startLine).arg(loc.startColumn)
+ QString::number(sourceElement->sourceElement->kind));
}
break;
}
default: {
m_colorOut.writeUncolored("unsupported element of kind "
+ QString::number(initMembers->member->kind));
}
}
initMembers = initMembers->next;
}
return scope; return scope;
} }
void FindUnqualifiedIDVisitor::importDirectory(const QString &directory, const QString &prefix) void FindUnqualifiedIDVisitor::importFileOrDirectory(const QString &fileOrDirectory,
const QString &prefix)
{ {
QString dirname = directory; QString name = fileOrDirectory;
QFileInfo info { dirname };
if (info.isRelative())
dirname = QDir(QFileInfo { m_filePath }.path()).filePath(dirname);
QDirIterator it { dirname, QStringList() << QLatin1String("*.qml"), QDir::NoFilter }; if (QFileInfo(name).isRelative())
name = QDir(QFileInfo { m_filePath }.path()).filePath(name);
if (QFileInfo(name).isFile()) {
m_exportedName2Scope.insert(prefix, ScopeTree::ConstPtr(localFile2ScopeTree(name)));
return;
}
QDirIterator it { name, QStringList() << QLatin1String("*.qml"), QDir::NoFilter };
while (it.hasNext()) { while (it.hasNext()) {
ScopeTree::ConstPtr scope(localQmlFile2ScopeTree(it.next())); ScopeTree::ConstPtr scope(localFile2ScopeTree(it.next()));
if (!scope->className().isEmpty()) if (!scope->className().isEmpty())
m_exportedName2Scope.insert(prefix + scope->className(), scope); m_exportedName2Scope.insert(prefixedName(prefix, scope->className()), scope);
} }
} }
@ -471,7 +523,7 @@ bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiProgram *)
// using an empty ScopeTree // using an empty ScopeTree
m_exportedName2Scope.insert(QFileInfo { m_filePath }.baseName(), {}); m_exportedName2Scope.insert(QFileInfo { m_filePath }.baseName(), {});
importDirectory(".", QString()); importFileOrDirectory(".", QString());
return true; return true;
} }
@ -778,11 +830,11 @@ bool FindUnqualifiedIDVisitor::visit(QQmlJS::AST::UiImport *import)
// construct path // construct path
QString prefix = QLatin1String(""); QString prefix = QLatin1String("");
if (import->asToken.isValid()) { if (import->asToken.isValid()) {
prefix += import->importId + QLatin1Char('.'); prefix += import->importId;
} }
auto dirname = import->fileName.toString(); auto dirname = import->fileName.toString();
if (!dirname.isEmpty()) if (!dirname.isEmpty())
importDirectory(dirname, prefix); importFileOrDirectory(dirname, prefix);
QString path {}; QString path {};
if (!import->importId.isEmpty()) { if (!import->importId.isEmpty()) {

View File

@ -95,11 +95,15 @@ private:
Import readQmldir(const QString &dirname); Import readQmldir(const QString &dirname);
void processImport(const QString &prefix, const Import &import); void processImport(const QString &prefix, const Import &import);
ScopeTree *localQmlFile2ScopeTree(const QString &filePath); ScopeTree *localFile2ScopeTree(const QString &filePath);
void importDirectory(const QString &directory, const QString &prefix); void importFileOrDirectory(const QString &directory, const QString &prefix);
void importExportedNames(const QStringRef &prefix, QString name); void importExportedNames(const QStringRef &prefix, QString name);
void parseHeaders(QQmlJS::AST::UiHeaderItemList *headers);
void parseMembers(QQmlJS::AST::UiObjectMemberList *members, ScopeTree *scope);
void parseProgram(QQmlJS::AST::Program *program, ScopeTree *scope);
void throwRecursionDepthError() override; void throwRecursionDepthError() override;
// start block/scope handling // start block/scope handling