1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-04-23 14:42:11 +02:00

Bug 480238 - Parse QML Object Literal and Members

Acorn-qml can now parse a large subset of QML syntax.  The only two
things left to do at this point are to allow QML Object Literals in
Property Bindings and to allow QML contextual keywords such as 'signal'
as property binding identifiers.  Both of these require lookahead which
acorn does not ship with at the moment, so this may be a bit of an
undertaking.  Also, added a whole bunch of tests to parse the new
syntax.

Change-Id: I0950fa29265c8337b5c9bfc0a1ec0c3ba8267426
Signed-off-by: Matthew Bastien <mbastien@blackberry.com>
This commit is contained in:
Matthew Bastien 2015-10-26 16:43:43 -04:00 committed by Gerrit Code Review @ Eclipse.org
parent cf7cb39de6
commit a9730dc194
2 changed files with 2957 additions and 491 deletions

View file

@ -11,32 +11,63 @@
'use strict';
module.exports = function(acorn) {
// Define QML token types
// Acorn token types
var tt = acorn.tokTypes;
// QML token types
var qtt = {};
var keywords = {};
/*
* Shorthand for defining keywords in tt (acorn.tokTypes). Creates a new key in
* tt with the label _<keywordName>.
* Shorthand for defining keywords in the 'keywords' variable with the following
* format:
* keywords[name].isPrimitive : if this is a primitive type
* keywords[name].isQMLContextual : if this is a contextual keyword for QML
*
* Also stores the token's name in qtt._<keyword> for easy referencing later. None
* of these keywords will be tokenized and, as such, are allowed to be used in
* JavaScript expressions by acorn. The 'isQMLContextual' boolean in keywords refers
* to those contextual keywords that are also contextual in QML's parser rules such
* as 'color', 'list', 'alias', etc.
*/
function kw(name, options) {
if (options === undefined)
options = {};
options.keyword = name;
tt["_" + name] = new acorn.TokenType(name, options);
qtt["_" + name] = name;
keywords[name] = {};
keywords[name].isPrimitive = options.isPrimitive ? true : false;
keywords[name].isQMLContextual = options.isQMLContextual ? true : false;
}
// Define QML keywords
kw("property");
kw("readonly");
kw("color");
// QML keywords
kw("import");
kw("pragma");
kw("property", { isQMLContextual : true });
kw("readonly", { isQMLContextual : true });
kw("signal", { isQMLContextual : true });
kw("as");
kw("boolean", { isPrimitive: true });
kw("double", { isPrimitive: true });
kw("int", { isPrimitive: true });
kw("alias", { isQMLContextual: true });
kw("list", { isPrimitive: true, isQMLContextual: true });
kw("color", { isPrimitive: true, isQMLContextual: true });
kw("real", { isPrimitive: true, isQMLContextual: true });
kw("string", { isPrimitive: true, isQMLContextual: true });
kw("url", { isPrimitive: true, isQMLContextual: true });
// Define QML token contexts
var tc = acorn.tokContexts;
// TODO: Add QML contexts (one such example is so we can parse keywords as identifiers)
// Future reserved words
kw("transient");
kw("synchronized");
kw("abstract");
kw("volatile");
kw("native");
kw("goto");
kw("byte");
kw("long");
kw("char");
kw("short");
kw("float");
// QML parser methods
var pp = acorn.Parser.prototype;
@ -51,21 +82,21 @@ module.exports = function(acorn) {
var loop = true;
while (loop) {
switch (this.type) {
case tt._import:
var qmlImport = this.qml_parseImportStatement();
node.statements.push(qmlImport);
break;
case tt._pragma:
var qmlPragma = this.qml_parsePragmaStatement();
node.statements.push(qmlPragma);
break;
default:
loop = false;
if (this.type === tt._import || 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");
if (node.statements.length > 0) {
return this.finishNode(node, "QMLHeaderStatements");
}
return undefined;
}
/*
@ -105,7 +136,7 @@ module.exports = function(acorn) {
}
// Parse the qualifier, if any
if (this.type === tt._as) {
if (this.isContextual(qtt._as)) {
node.qualifier = this.qml_parseQualifier();
}
@ -158,10 +189,210 @@ module.exports = function(acorn) {
pp.qml_parseQualifier = function() {
var node = this.startNode();
this.next();
node.identifier = this.parseIdent(false);
node.identifier = 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
*/
pp.qml_parseObjectLiteral = function(node) {
if (!node) {
node = this.startNode();
}
if (!node.qualifiedId) {
node.qualifiedId = this.qml_parseQualifiedId();
}
node.block = this.qml_parseMemberBlock();
return this.finishNode(node, "QMLObjectLiteral");
}
/*
* Parses a QML Member Block of the form:
* { <QMLMember>* }
*/
pp.qml_parseMemberBlock = function() {
var node = this.startNode();
this.expect(tt.braceL);
node.members = [];
while (!this.eat(tt.braceR)) {
node.members.push(this.qml_parseMember());
}
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
*/
pp.qml_parseMember = function() {
var node = this.startNode();
if (this.type === tt._default
|| this.isContextual(qtt._default)
|| this.isContextual(qtt._readonly)
|| this.isContextual(qtt._property)) {
return this.qml_parsePropertyDeclaration(node);
} else if (this.isContextual(qtt._signal)) {
return this.qml_parseSignalDefinition(node);
} else if (this.type === tt._function) {
return this.parseFunctionStatement(node);
}
node.qualifiedId = this.qml_parseQualifiedId();
switch(this.type) {
case tt.braceL:
return this.qml_parseObjectLiteral(node);
case tt.colon:
return this.qml_parseProperty(node);
}
this.unexpected();
}
/*
* Parses a QML Property of the form:
* <QMLQualifiedID> <QMLBinding>
*/
pp.qml_parseProperty = function(node) {
if (!node) {
node = this.startNode();
}
if (!node.qualifiedId) {
node.qualifiedId = this.qml_parseQualifiedId();
}
node.binding = this.qml_parseBinding();
return this.finishNode(node, "QMLProperty");
}
/*
* Parses a QML Signal Definition of the form:
* 'signal' <Identifier> [(<Type> <Identifier> [',' <Type> <Identifier>]* )]?
*/
pp.qml_parseSignalDefinition = function(node) {
if (!node) {
node = this.startNode();
}
this.next();
node.identifier = this.qml_parseIdent(false);
node.parameters = [];
if (this.type === tt.parenL) {
this.next();
if (this.type !== tt.parenR) {
do {
var paramNode = this.startNode();
paramNode.type = this.qml_parseIdent(false);
paramNode.identifier = this.qml_parseIdent(false);
node.parameters.push(paramNode);
} while(this.eat(tt.comma));
}
if (!this.eat(tt.parenR)) {
this.unexpected();
}
}
this.semicolon();
return this.finishNode(node, "QMLSignalDefinition");
}
/*
* Parses a QML Property Declaration (or Alias) of the form:
* ['default'|'readonly'] 'property' <QMLType> <Identifier> [<QMLBinding>]
*/
pp.qml_parsePropertyDeclaration = function(node) {
node["default"] = false;
node["readonly"] = false;
if (this.type === tt._default || this.isContextual(qtt._default)) {
node["default"] = true;
this.next();
} else if (this.eatContextual(qtt._readonly)) {
node["readonly"] = true;
}
this.expectContextual(qtt._property);
node.typeInfo = this.qml_parseType();
node.identifier = this.qml_parseIdent(false);
if (this.type !== tt.colon) {
this.semicolon();
} else {
node.binding = this.qml_parseBinding();
}
if (node.typeInfo.type === qtt._alias) {
node.typeInfo = undefined;
return this.finishNode(node, "QMLPropertyAlias");
}
return this.finishNode(node, "QMLPropertyDeclaration");
}
/*
* Parses a QML Binding of the form:
* ':' (<Expression>|<QMLStatementBlock>)
*/
pp.qml_parseBinding = function() {
var node = this.startNode();
this.expect(tt.colon);
// TODO: solve ambiguity where a QML Object Literal starts with a
// Qualified Id that looks very similar to a MemberExpression in
// JavaScript. For now, we just won't parse statements like:
// test: QMLObject { }
// test: QMLObject.QualifiedId { }
if (this.type === tt.braceL) {
node.block = this.qml_parseStatementBlock();
return this.finishNode(node, "QMLBinding");
}
node.expr = this.parseExpression(false);
this.semicolon();
return this.finishNode(node, "QMLBinding");
}
/*
* Parses a QML Statement Block of the form:
* { <JavaScript Statement>* }
*/
pp.qml_parseStatementBlock = function() {
var node = this.startNode();
this.expect(tt.braceL);
node.statements = [];
while(!this.eat(tt.braceR)) {
node.statements.push(this.parseStatement(true, false));
}
return this.finishNode(node, "QMLStatementBlock");
}
/*
* Parses a QML Type which can be either a Qualified ID or a primitive type keyword.
* Returns a node of type qtt._alias if the type keyword parsed was "alias".
*/
pp.qml_parseType = function() {
var node = this.startNode();
if (this.type === tt.name || this.type === tt._var) {
var value = this.value;
if (this.qml_eatPrimitiveType(value)) {
node.isPrimitive = true;
node.primitive = value;
} else if (this.eatContextual(qtt._alias)) {
return this.finishNode(node, qtt._alias);
} else {
node.isPrimitive = false;
node.qualifiedId = this.qml_parseQualifiedId();
}
} else {
this.unexpected();
}
return this.finishNode(node, "QMLType");
}
/*
* Parses a Qualified ID of the form:
* <Identifier> ('.' <Identifier>)*
@ -170,18 +401,20 @@ module.exports = function(acorn) {
var node = this.startNode();
node.parts = [];
if (!this.qml_isIdent(this.type, this.value)) {
this.unexpected();
}
var id = this.value;
node.parts.push(this.value);
this.next();
node.parts.push(id);
while(this.type === tt.dot) {
id += '.';
this.next();
if (this.type === tt.name) {
id += this.value;
node.parts.push(this.value);
} else {
if (!this.qml_isIdent(this.type, this.value)) {
this.unexpected();
}
id += this.value;
node.parts.push(this.value);
this.next();
}
node.raw = id;
@ -190,34 +423,61 @@ module.exports = function(acorn) {
}
/*
* Returns a TokenType that matches the given word or undefined if
* no such TokenType could be found. This method only matches
* QML-specific keywords.
*
* Uses contextual information to determine whether or not a keyword
* such as 'color' is being used as an identifier. If this is found
* to be the case, tt.name is returned.
* 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.
*/
pp.qml_getTokenType = function(word) {
// TODO: use context to determine if this is an identifier or
// a keyword (color, real, etc. can be used as identifiers)
switch(word) {
case "property":
return tt._property;
case "readonly":
return tt._readonly;
case "import":
// Make sure that 'import' is recognized as a keyword
// regardless of the ecma version set in acorn.
return tt._import;
case "color":
return tt._color;
case "pragma":
return tt._pragma;
case "as":
return tt._as;
pp.qml_parseIdent = function(liberal) {
// Check for non-contextual QML keywords
if (this.type === tt.name) {
for (var key in keywords) {
if (!keywords[key].isQMLContextual && this.isContextual(key)) {
this.unexpected();
}
}
}
return undefined;
return this.parseIdent(liberal);
}
/*
* Returns whether or not a given token type and name can be a QML Identifier.
* Uses the 'isQMLContextual' boolean of 'keywords' to determine this.
*/
pp.qml_isIdent = function(type, name) {
if (type === tt.name) {
var key;
if (key = keywords[name]) {
return key.isQMLContextual
}
return true;
}
return false;
}
/*
* 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)) {
this.next();
return true;
}
return false;
}
/*
* Returns whether or not the current token is a QML primitive type.
*/
pp.qml_isPrimitiveType = function(name) {
if (name === "var") {
return true;
}
var key;
if (key = keywords[name]) {
return key.isPrimitive;
}
return false;
}
acorn.plugins.qml = function(instance) {
@ -229,38 +489,27 @@ module.exports = function(acorn) {
// 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.
if (!node.body)
if (!node.body) {
node.body = [];
var headerStmts = this.qml_parseHeaderStatements();
node.body.push(headerStmts);
// TODO: Parse QML object root
// TODO: don't call acorn's parseTopLevel method once the above are working
return nextMethod.call(this, node);
};
});
// Extend acorn's 'readWord' method
instance.extend("readWord", function(nextMethod) {
return function() {
// Parse a word and attempt to match it to a QML keyword
var word = this.readWord1();
var type = this.qml_getTokenType(word);
if (type !== undefined) {
return this.finishToken(type, word);
}
// If we were unable to find a QML keyword, call acorn's implementation
// of the readWord method. Since we don't have access to _tokentype, and
// subsequently _tokentype.keywords, we can't look for JavaScript keyword
// matches ourselves. This is unfortunate because we have to move the parser
// backwards and let readWord call readWord1 a second time for every word
// that is not a QML keyword.
this.pos -= word.length;
return nextMethod.call(this);
var headerStmts = this.qml_parseHeaderStatements();
if (headerStmts !== undefined) {
node.body.push(headerStmts);
}
if (this.type !== tt.eof) {
var objRoot = this.qml_parseObjectLiteral();
if (objRoot !== undefined) {
node.body.push(objRoot);
}
}
if (!this.eat(tt.eof)) {
this.raise(this.pos, "Expected EOF after QML Root Object");
}
return this.finishNode(node, "Program");
};
});
}

File diff suppressed because it is too large Load diff