1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-08-11 18:25:40 +02:00

Bug 481850 - QML Error Tolerant Parser

Added the loose plugin to acorn-qml which is error-tolerant in its
parsing approach.  Switched the main parser to automatically use strict
mode and to complain if the ECMA Script version is set to anything
higher than 5.  This simplifies the parsing a little bit and keeps us in
sync with Qt which currently uses ECMA Script 5 in its JavaScript
runtime environment.

Updated the test framework to accommodate the loose parser and be less
'magic' in how it runs the tests.  Added new tests to make sure the
loose parser is, in fact, error tolerant.

Change-Id: I670fc01853198d2261fbf9d8b017d225c4612182
Signed-off-by: Matthew Bastien <mbastien@blackberry.com>
This commit is contained in:
Matthew Bastien 2015-11-09 14:45:25 -05:00 committed by Gerrit Code Review @ Eclipse.org
parent 8b32e36ed7
commit 2d6e4f4b8a
8 changed files with 3671 additions and 3374 deletions

View file

@ -25,8 +25,8 @@ var injectQML;
var tt = acorn.tokTypes;
// QML token types
var qtt = {};
var keywords = {};
var qtt = acorn.qmlTokTypes = {};
var keywords = acorn.qmlKeywords = {};
/*
* Shorthand for defining keywords in the 'keywords' variable with the following
@ -59,7 +59,7 @@ var injectQML;
kw("boolean", { isPrimitive: true });
kw("double", { isPrimitive: true });
kw("int", { isPrimitive: true });
kw("alias", { isQMLContextual: true });
kw("alias", { isPrimitive: true, isQMLContextual: true });
kw("list", { isPrimitive: true, isQMLContextual: true });
kw("color", { isPrimitive: true, isQMLContextual: true });
kw("real", { isPrimitive: true, isQMLContextual: true });
@ -92,7 +92,7 @@ var injectQML;
var loop = true;
while (loop) {
if (this.type === tt._import || this.isContextual(qtt._import)) {
if (this.isContextual(qtt._import)) {
var qmlImport = this.qml_parseImportStatement();
node.statements.push(qmlImport);
} else if (this.isContextual(qtt._pragma)) {
@ -128,7 +128,7 @@ var injectQML;
pp.qml_parseImportStatement = function () {
var node = this.startNode();
if (!this.eat(tt._import) && !this.eatContextual(qtt._import)) {
if (!this.eatContextual(qtt._import)) {
this.unexpected();
}
@ -216,7 +216,7 @@ var injectQML;
if (!node.id) {
node.id = this.qml_parseQualifiedId(false);
}
node.block = this.qml_parseMemberBlock();
node.body = this.qml_parseMemberBlock();
return this.finishNode(node, "QMLObjectLiteral");
}
@ -243,7 +243,7 @@ var injectQML;
* - a Signal Definition
*/
pp.qml_parseMember = function () {
if (this.type === tt._default || this.isContextual(qtt._default) || this.isContextual(qtt._readonly) || this.isContextual(qtt._property)) {
if (this.type === tt._default || this.isContextual(qtt._readonly) || this.isContextual(qtt._property)) {
return this.qml_parsePropertyDeclaration();
} else if (this.isContextual(qtt._signal)) {
return this.qml_parseSignalDefinition();
@ -357,7 +357,7 @@ var injectQML;
// Parse 'default' or 'readonly'
node["default"] = false;
node["readonly"] = false;
if (this.eat(tt._default) || this.eatContextual(qtt._default)) {
if (this.eat(tt._default)) {
node["default"] = true;
} else if (this.isContextual(qtt._readonly)) {
// Parse as a qualified id in case this is not a property declaration
@ -451,15 +451,11 @@ var injectQML;
* Returns a node of type qtt._alias if the type keyword parsed was "alias".
*/
pp.qml_parseKind = function () {
if (this.type === tt.name || this.type === tt._var) {
var value = this.value;
if (this.qml_eatPrimitiveType(value)) {
return value;
} else if (this.eatContextual(qtt._alias)) {
return qtt._alias;
} else {
return this.qml_parseQualifiedId(false);
}
var value = this.value;
if (this.qml_eatPrimitiveType(this.type, value)) {
return value;
} else {
return this.qml_parseQualifiedId(false);
}
this.unexpected();
}
@ -499,13 +495,9 @@ var injectQML;
*/
pp.qml_parseIdent = function (liberal) {
// Check for non-contextual QML keywords
if (this.type === tt.name) {
if (!liberal) {
for (var key in keywords) {
if (!keywords[key].isQMLContextual && this.isContextual(key)) {
this.unexpected();
}
}
if (!liberal) {
if (!this.qml_isIdent(this.type, this.value)) {
this.unexpected();
}
}
return this.parseIdent(liberal);
@ -530,8 +522,8 @@ var injectQML;
* Returns whether or not the current token is a QML primitive type and consumes
* it as a side effect if it is.
*/
pp.qml_eatPrimitiveType = function (name) {
if (this.qml_isPrimitiveType(name)) {
pp.qml_eatPrimitiveType = function (type, name) {
if (this.qml_isPrimitiveType(type, name)) {
this.next();
return true;
}
@ -541,14 +533,16 @@ var injectQML;
/*
* Returns whether or not the current token is a QML primitive type.
*/
pp.qml_isPrimitiveType = function (name) {
pp.qml_isPrimitiveType = function (type, name) {
if (name === "var") {
return true;
}
var key;
if (key = keywords[name]) {
return key.isPrimitive;
if (type === tt.name) {
var key;
if (key = keywords[name]) {
return key.isPrimitive;
}
}
return false;
}
@ -558,6 +552,16 @@ var injectQML;
// Extend acorn's 'parseTopLevel' method
instance.extend("parseTopLevel", function (nextMethod) {
return function (node) {
// Make parsing simpler by only allowing ECMA Version 5 or older ('import' is
// not a keyword in this version of ECMA Script). Qt 5.5 runs with ECMA Script
// 5 anyway, so this makes sense.
if (!this.options.ecmaVersion || this.options.ecmaVersion > 5) {
throw new Error("QML only supports ECMA Script Language Specification 5 or older");
}
// Force strict mode
this.strict = true;
// Most of QML's constructs sit at the top-level of the parse tree,
// replacing JavaScripts top-level. Here we are parsing such things
// as the root object literal and header statements of QML. Eventually,

View file

@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2015 QNX Software Systems and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* QNX Software Systems - Initial API and implementation
*******************************************************************************/
'use strict';
// This will only be visible globally if we are in a browser environment
var acornQMLLoose;
(function (mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
return module.exports = mod(require("./inject.js"), require("acorn"), require("acorn/dist/acorn_loose"));
if (typeof define == "function" && define.amd) // AMD
return define(["./inject.js", "acorn", "acorn/dist/acorn_loose"], mod);
acornQMLLoose = mod(injectQMLLoose, acorn, acorn); // Plain browser env
})(function (injectQMLLoose, acorn, acorn_loose) {
return injectQMLLoose(acorn);
})

View file

@ -0,0 +1,568 @@
/*******************************************************************************
* Copyright (c) 2015 QNX Software Systems and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* QNX Software Systems - Initial API and implementation
*******************************************************************************/
'use strict';
// This will only be visible globally if we are in a browser environment
var injectQMLLoose;
(function (mod) {
if (typeof exports === "object" && typeof module === "object") // CommonJS
return module.exports = mod();
if (typeof define === "function" && define.amd) // AMD
return define([], mod);
injectQMLLoose = mod(); // Plain browser env
})(function () {
return function (acorn) {
// Acorn token types
var tt = acorn.tokTypes;
// QML token types
var qtt = acorn.qmlTokTypes;
var keywords = acorn.qmlKeywords;
// QML parser methods
var lp = acorn.LooseParser.prototype;
var pp = acorn.Parser.prototype;
/*
* Parses a set of QML Header Statements which can either be of
* the type import or pragma
*/
lp.qml_parseHeaderStatements = function () {
var node = this.startNode()
node.statements = [];
var loop = true;
while (loop) {
if (this.isContextual(qtt._import)) {
var qmlImport = this.qml_parseImportStatement();
node.statements.push(qmlImport);
} else if (this.isContextual(qtt._pragma)) {
var qmlPragma = this.qml_parsePragmaStatement();
node.statements.push(qmlPragma);
} else {
loop = false;
}
}
return this.finishNode(node, "QMLHeaderStatements");
}
/*
* Parses a QML Pragma statement of the form:
* 'pragma' <Identifier>
*/
lp.qml_parsePragmaStatement = function () {
var node = this.startNode();
this.expectContextual(qtt._pragma);
node.id = this.parseIdent(false);
this.semicolon();
return this.finishNode(node, "QMLPragmaStatement");
}
/*
* Parses a QML Import statement of the form:
* 'import' <ModuleIdentifier> <Version.Number> [as <Qualifier>]
* 'import' <DirectoryPath> [as <Qualifier>]
*
* as specified by http://doc.qt.io/qt-5/qtqml-syntax-imports.html
*/
lp.qml_parseImportStatement = function () {
var node = this.startNode();
this.expectContextual(qtt._import);
switch (this.tok.type) {
case tt.string:
node.module = null;
node.directory = this.parseExprAtom();
break;
default:
node.module = this.qml_parseModule();
node.directory = null;
break;
}
// Parse the qualifier, if any
if (this.isContextual(qtt._as)) {
node.qualifier = this.qml_parseQualifier();
}
this.semicolon();
return this.finishNode(node, "QMLImportStatement");
};
/*
* Parses a QML Module of the form:
* <QMLQualifiedId> <QMLVersionLiteral> ['as' <QMLQualifier>]?
*/
lp.qml_parseModule = function () {
var node = this.startNode();
node.id = this.qml_parseQualifiedId(false);
node.version = this.qml_parseVersionLiteral();
return this.finishNode(node, "QMLModule");
};
/*
* Parses a QML Version Literal which consists of a major and minor
* version separated by a '.'
*/
lp.qml_parseVersionLiteral = function () {
var node = this.startNode();
node.raw = this.input.slice(this.tok.start, this.tok.end);
node.value = this.tok.value;
var matches;
if (this.tok.type === tt.num) {
if (matches = /(\d+)\.(\d+)/.exec(node.raw)) {
node.major = parseInt(matches[1]);
node.minor = parseInt(matches[2]);
this.next();
} else {
node.major = parseInt(node.raw);
node.minor = 0;
this.next();
}
} else {
node.major = 0;
node.minor = 0;
node.value = 0;
node.raw = "0.0";
}
return this.finishNode(node, "QMLVersionLiteral");
}
/*
* Parses a QML Qualifier of the form:
* 'as' <Identifier>
*/
lp.qml_parseQualifier = function () {
var node = this.startNode();
this.expectContextual(qtt._as);
node.id = this.qml_parseIdent(false);
return this.finishNode(node, "QMLQualifier");
}
/*
* Parses a QML Object Literal of the form:
* <QualifiedId> { (<QMLMember>)* }
*
* http://doc.qt.io/qt-5/qtqml-syntax-basics.html#object-declarations
*/
lp.qml_parseObjectLiteral = function () {
var node = this.startNode();
node.id = this.qml_parseQualifiedId(false);
node.body = this.qml_parseMemberBlock();
return this.finishNode(node, "QMLObjectLiteral");
}
/*
* Parses a QML Member Block of the form:
* { <QMLMember>* }
*/
lp.qml_parseMemberBlock = function () {
var node = this.startNode();
this.pushCx();
this.expect(tt.braceL);
var blockIndent = this.curIndent, line = this.curLineStart;
node.members = [];
while (!this.closes(tt.braceR, blockIndent, line, true)) {
var member = this.qml_parseMember();
if (member) {
node.members.push(member);
}
}
this.popCx();
this.eat(tt.braceR);
return this.finishNode(node, "QMLMemberBlock");
}
/*
* Parses a QML Member which can be one of the following:
* - a QML Property Binding
* - a Property Declaration (or Alias)
* - a QML Object Literal
* - a JavaScript Function Declaration
* - a Signal Definition
*/
lp.qml_parseMember = function () {
if (this.tok.type === tt._default || this.isContextual(qtt._readonly) || this.isContextual(qtt._property) || this.qml_isPrimitiveType(this.tok.type, this.tok.value)) {
return this.qml_parsePropertyDeclaration();
} else if (this.isContextual(qtt._signal)) {
return this.qml_parseSignalDefinition();
} else if (this.tok.type === tt._function) {
return this.qml_parseFunctionMember();
} else if (this.qml_isIdent(this.tok.type, this.tok.value) || this.tok.type === tt.dot) {
var la = this.lookAhead(1);
if (this.qml_isIdent(la.type, la.value)) {
// Two identifiers in a row means this is most likely a property declaration
// with the 'property' token missing
return this.qml_parsePropertyDeclaration();
} else {
var node = this.qml_parseObjectLiteralOrPropertyBinding();
if (node) {
return node;
} else {
return this.qml_parsePropertyBinding();
}
}
} else if (this.tok.type === tt.colon) {
return this.qml_parsePropertyBinding();
} else if (this.tok.type === tt.braceL) {
return this.qml_parseObjectLiteral();
}
// ignore the current token if it didn't pass the previous tests
this.next();
}
/*
* Parses a JavaScript function as a member of a QML Object Literal
*/
lp.qml_parseFunctionMember = function () {
var node = this.startNode();
this.expect(tt._function);
return this.qml_parseFunction(node, true);
}
/*
* QML version of 'parseFunction' needed to have proper error tolerant parsing
* for QML member functions versus their JavaScript counterparts. The main
* difference between the two functions is that this implementation will not
* forcefully insert '(' and '{' tokens for the body and parameters. Instead,
* it will silently create an empty parameter list or body and let parsing
* continue normally.
*/
lp.qml_parseFunction = function(node, isStatement) {
this.initFunction(node);
if (this.tok.type === tt.name) node.id = this.parseIdent();
else if (isStatement) node.id = this.dummyIdent();
node.params = this.tok.type === tt.parenL ? this.parseFunctionParams() : [];
if (this.tok.type === tt.braceL) {
node.body = this.parseBlock();
} else {
if (this.options.locations) {
node.body = this.startNodeAt([ this.last.end, this.last.loc.end ])
} else {
node.body = this.startNodeAt(this.last.end);
}
node.body.body = [];
this.finishNode(node.body, "BlockStatement");
}
return this.finishNode(node, isStatement ? "FunctionDeclaration" : "FunctionExpression");
}
/*
* Parses a QML Object Literal or Property Binding depending on the tokens found.
*/
lp.qml_parseObjectLiteralOrPropertyBinding = function () {
var i = 1, la = this.tok;
if (this.qml_isIdent(la.type, la.value)) {
la = this.lookAhead(i++);
}
while (la.type === tt.dot) {
la = this.lookAhead(i++);
if (this.qml_isIdent(la.type, la.value)) {
la = this.lookAhead(i++);
}
}
switch (la.type) {
case tt.braceL:
return this.qml_parseObjectLiteral();
case tt.colon:
return this.qml_parsePropertyBinding();
}
return null;
}
/*
* Parses a QML Property of the form:
* <QMLQualifiedID> <QMLBinding>
*/
lp.qml_parsePropertyBinding = function () {
var node = this.startNode();
node.id = this.qml_parseQualifiedId(false);
this.expect(tt.colon);
node.expr = this.qml_parsePropertyAssignment();
return this.finishNode(node, "QMLPropertyBinding");
}
/*
* Parses a QML Signal Definition of the form:
* 'signal' <Identifier> [(<Type> <Identifier> [',' <Type> <Identifier>]* )]?
*/
lp.qml_parseSignalDefinition = function () {
var node = this.startNode();
// Check if this is an object literal or property binding first
var objOrBind = this.qml_parseObjectLiteralOrPropertyBinding();
if (objOrBind) {
return objOrBind;
}
this.expectContextual(qtt._signal);
node.id = this.qml_parseIdent(false);
this.qml_parseSignalParams(node);
this.semicolon();
return this.finishNode(node, "QMLSignalDefinition");
}
/*
* Checks if the given node is a dummy identifier
*/
function isDummy(node) {
return node.name === "✖"
}
/*
* Parses QML Signal Parameters of the form:
* [(<Type> <Identifier> [',' <Type> <Identifier>]* )]?
*/
lp.qml_parseSignalParams = function (node) {
this.pushCx()
let indent = this.curIndent, line = this.curLineStart
node.params = [];
if (this.eat(tt.parenL)) {
while (!this.closes(tt.parenR, indent + 1, line)) {
var param = this.startNode();
param.kind = this.qml_parseIdent(true);
// Break out of an infinite loop where we continously consume dummy ids
if (isDummy(param.kind) && this.tok.type !== tt.comma) {
break;
}
param.id = this.qml_parseIdent(false);
node.params.push(this.finishNode(param, "QMLParameter"));
// Break out of an infinite loop where we continously consume dummy ids
if (isDummy(param.id) && this.tok.type !== tt.comma) {
break;
}
this.eat(tt.comma);
}
this.popCx()
if (!this.eat(tt.parenR)) {
// If there is no closing brace, make the node span to the start
// of the next token (this is useful for Tern)
this.last.end = this.tok.start
if (this.options.locations) this.last.loc.end = this.tok.loc.start
}
}
}
/*
* Parses a QML Property Declaration (or Alias) of the form:
* ['default'|'readonly'] 'property' <QMLType> <Identifier> [<QMLBinding>]
*/
lp.qml_parsePropertyDeclaration = function () {
var node = this.startNode();
// Parse 'default' or 'readonly'
node["default"] = false;
node["readonly"] = false;
if (this.eat(tt._default)) {
node["default"] = true;
} else if (this.isContextual(qtt._readonly)) {
var objOrBind = this.qml_parseObjectLiteralOrPropertyBinding();
if (objOrBind) {
objOrBind["default"] = undefined;
objOrBind["readonly"] = undefined;
return objOrBind;
}
this.expectContextual(qtt._readonly);
node["readonly"] = true;
}
if (!node["default"] && !node["readonly"]) {
var objOrBind = this.qml_parseObjectLiteralOrPropertyBinding();
if (objOrBind) {
return objOrBind;
}
this.expectContextual(qtt._property);
} else {
this.expectContextual(qtt._property);
}
node.kind = this.qml_parseKind();
node.id = this.qml_parseIdent(false);
if (!this.eat(tt.colon)) {
node.init = null;
this.semicolon();
} else {
node.init = this.qml_parsePropertyAssignment();
}
return this.finishNode(node, "QMLPropertyDeclaration");
}
/*
* Parses one of the following possibilities for a QML Property assignment:
* - JavaScript Expression
* - QML JavaScript Statement Block
* - QML Object Literal
*/
lp.qml_parsePropertyAssignment = function () {
if (this.tok.type === tt.braceL) {
return this.qml_parseStatementBlock();
} else {
// Perform look ahead to determine whether this is an expression or
// a QML Object Literal
var i = 1, la = this.tok;
if (this.qml_isIdent(la.type, la.value)) {
la = this.lookAhead(i++);
}
while (la.type === tt.dot) {
la = this.lookAhead(i++);
if (this.qml_isIdent(la.type, la.value)) {
la = this.lookAhead(i++);
}
}
if (la.type === tt.braceL) {
return this.qml_parseObjectLiteral();
} else {
var node = this.parseExpression(false);
this.semicolon();
return node;
}
}
}
/*
* Parses a QML Statement Block of the form:
* { <JavaScript Statement>* }
*/
lp.qml_parseStatementBlock = function () {
var node = this.startNode();
this.pushCx();
this.expect(tt.braceL);
var blockIndent = this.curIndent,
line = this.curLineStart;
node.statements = [];
while (!this.closes(tt.braceR, blockIndent, line, true)) {
node.statements.push(this.parseStatement(true, false));
}
this.popCx();
this.eat(tt.braceR);
return this.finishNode(node, "QMLStatementBlock");
}
/*
* Parses a QML Type which can be either a Qualified ID or a primitive type keyword.
*/
lp.qml_parseKind = function () {
var value = this.tok.value;
if (this.qml_eatPrimitiveType(this.tok.type, value)) {
return value;
} else {
return this.qml_parseQualifiedId(false);
}
}
/*
* Parses a Qualified ID of the form:
* <Identifier> ('.' <Identifier>)*
*
* If 'liberal' is true then this method will allow non-contextual QML keywords as
* identifiers.
*/
lp.qml_parseQualifiedId = function (liberal) {
var node = this.startNode();
node.parts = [];
node.parts.push(this.qml_parseIdent(liberal));
while (this.eat(tt.dot)) {
node.parts.push(this.qml_parseIdent(liberal));
}
node.name = "";
for (var i = 0; i < node.parts.length; i++) {
node.name += node.parts[i].name;
if (i < node.parts.length - 1) {
node.name += ".";
}
}
return this.finishNode(node, "QMLQualifiedID");
}
/*
* Parses an Identifier in a QML Context. That is, this method uses 'isQMLContextual'
* to throw an error if a non-contextual QML keyword is found.
*
* If 'liberal' is true then this method will allow non-contextual QML keywords as
* identifiers.
*/
lp.qml_parseIdent = function (liberal) {
// Check for non-contextual QML keywords
if (!liberal) {
if (!this.qml_isIdent(this.tok.type, this.tok.value)) {
return this.dummyIdent();
}
}
return this.parseIdent();
}
/*
* Checks the next token to see if it matches the given contextual keyword. If the
* contextual keyword was not found, this function looks ahead at the next two tokens
* and jumps ahead if it was found there. Returns whether or not the keyword was found.
*/
lp.expectContextual = function(name) {
if (this.eatContextual(name)) return true
for (let i = 1; i <= 2; i++) {
if (this.lookAhead(i).type == tt.name && this.lookAhead(i).value === name) {
for (let j = 0; j < i; j++) this.next()
return true
}
}
}
// Functions left un-changed from the main parser
lp.qml_isIdent = pp.qml_isIdent;
lp.qml_eatPrimitiveType = pp.qml_eatPrimitiveType;
lp.qml_isPrimitiveType = pp.qml_isPrimitiveType;
acorn.pluginsLoose.qml = function (instance) {
// Extend acorn's 'parseTopLevel' method
instance.extend("parseTopLevel", function (nextMethod) {
return function () {
// Make parsing simpler by only allowing ECMA Version 5 or older ('import' is
// not a keyword in this version of ECMA Script). Qt 5.5 runs with ECMA Script
// 5 anyway, so this makes sense.
if (!this.options.ecmaVersion || this.options.ecmaVersion > 5) {
throw new Error("QML only supports ECMA Script Language Specification 5 or older");
}
// Most of QML's constructs sit at the top-level of the parse tree,
// replacing JavaScripts top-level. Here we are parsing such things
// as the root object literal and header statements of QML. Eventually,
// these rules will delegate down to JavaScript expressions.
var node = this.startNode();
node.headerStatements = this.qml_parseHeaderStatements();
node.rootObject = null;
if (this.tok.type !== tt.eof) {
node.rootObject = this.qml_parseObjectLiteral();
}
return this.finishNode(node, "Program");
};
});
}
return acorn;
};
})

View file

@ -7,6 +7,6 @@
"test": "node test/run.js"
},
"dependencies": {
"acorn": "^2.5.2"
"acorn": "^2.6.2"
}
}

View file

@ -37,6 +37,7 @@ exports.runTests = function(config, callback) {
testOpts.onToken = [];
}
testOpts.plugins = { qml: true };
testOpts.pluginsLoose = { qml: true };
var ast = parse(test.code, testOpts);
if (test.error) {
if (config.loose) {
@ -63,7 +64,7 @@ exports.runTests = function(config, callback) {
else callback("ok", test.code);
}
} catch(e) {
if (!(e instanceof SyntaxError)) {
if (!(e instanceof SyntaxError || e instanceof Error)) {
throw e;
}
if (test.error) {

View file

@ -31,7 +31,22 @@ function log(title, message) {
var stats, modes = {
Normal: {
config: {
parse: require("..").parse
parse: require("..").parse,
normal: true,
filter: function (test) {
var opts = test.options || {};
return opts.normal !== false;
}
}
},
Loose: {
config: {
parse: require("../loose").parse_dammit,
loose: true,
filter: function (test) {
var opts = test.options || {};
return opts.loose !== false;
}
}
}
};

File diff suppressed because it is too large Load diff

View file

@ -38,7 +38,7 @@
base["QMLPragmaStatement"] = ignore;
base["QMLRootObject"] = skipThrough;
base["QMLObjectLiteral"] = function (node, st, c) {
c(node.block, st);
c(node.body, st);
};
base["QMLMemberBlock"] = function (node, st, c) {
for (var i = 0; i < node.members.length; i++) {