mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-08-08 08:45:44 +02:00
Bug 481126 - QML Static Analysis based on Tern.js
Moved the Tern.js work and acorn-qml into the Qt Core plugin rather than in a separate plugin. Added walk.js in order to facilitate walking the QML AST by acorn-qml. Changed a few things in index.js and inject.js for acorn-qml in order to get it working in a browser environment. Added a tern-qml webpage demo which doesn't do much for now. Change-Id: I3c8a3d57c98a4936d0e038774b410bb2a68afb6c Signed-off-by: Matthew Bastien <mbastien@blackberry.com>
This commit is contained in:
parent
ae53f82634
commit
f9143ebfdc
23 changed files with 808 additions and 674 deletions
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<classpath>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
|
|
||||||
<classpathentry kind="src" path="src"/>
|
|
||||||
<classpathentry kind="output" path="bin"/>
|
|
||||||
</classpath>
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<projectDescription>
|
|
||||||
<name>org.eclipse.cdt.qt.core.acorn</name>
|
|
||||||
<comment></comment>
|
|
||||||
<projects>
|
|
||||||
</projects>
|
|
||||||
<buildSpec>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.pde.ManifestBuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.pde.SchemaBuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
</buildSpec>
|
|
||||||
<natures>
|
|
||||||
<nature>org.eclipse.pde.PluginNature</nature>
|
|
||||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
|
||||||
</natures>
|
|
||||||
</projectDescription>
|
|
|
@ -1,7 +0,0 @@
|
||||||
eclipse.preferences.version=1
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
|
||||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
|
|
||||||
org.eclipse.jdt.core.compiler.compliance=1.8
|
|
||||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
|
||||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
|
||||||
org.eclipse.jdt.core.compiler.source=1.8
|
|
|
@ -1,10 +0,0 @@
|
||||||
Manifest-Version: 1.0
|
|
||||||
Bundle-ManifestVersion: 2
|
|
||||||
Bundle-Name: %pluginName
|
|
||||||
Bundle-SymbolicName: org.eclipse.cdt.qt.core.acorn
|
|
||||||
Bundle-Version: 2.0.0.qualifier
|
|
||||||
Bundle-Activator: org.eclipse.cdt.qt.core.acorn.Activator
|
|
||||||
Bundle-Vendor: %providerName
|
|
||||||
Require-Bundle: org.eclipse.core.runtime
|
|
||||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
|
||||||
Bundle-ActivationPolicy: lazy
|
|
|
@ -1,518 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* 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';
|
|
||||||
|
|
||||||
module.exports = function(acorn) {
|
|
||||||
// Acorn token types
|
|
||||||
var tt = acorn.tokTypes;
|
|
||||||
|
|
||||||
// QML token types
|
|
||||||
var qtt = {};
|
|
||||||
var keywords = {};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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 = {};
|
|
||||||
qtt["_" + name] = name;
|
|
||||||
keywords[name] = {};
|
|
||||||
keywords[name].isPrimitive = options.isPrimitive ? true : false;
|
|
||||||
keywords[name].isQMLContextual = options.isQMLContextual ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 });
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parses a set of QML Header Statements which can either be of
|
|
||||||
* the type import or pragma
|
|
||||||
*/
|
|
||||||
pp.qml_parseHeaderStatements = function() {
|
|
||||||
var node = this.startNode()
|
|
||||||
node.statements = [];
|
|
||||||
|
|
||||||
var loop = true;
|
|
||||||
while (loop) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.statements.length > 0) {
|
|
||||||
return this.finishNode(node, "QMLHeaderStatements");
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parses a QML Pragma statement of the form:
|
|
||||||
* 'pragma' <Identifier>
|
|
||||||
*/
|
|
||||||
pp.qml_parsePragmaStatement = function() {
|
|
||||||
var node = this.startNode();
|
|
||||||
this.next();
|
|
||||||
node.identifier = 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
|
|
||||||
*/
|
|
||||||
pp.qml_parseImportStatement = function() {
|
|
||||||
var node = this.startNode();
|
|
||||||
this.next();
|
|
||||||
|
|
||||||
// The type of import varies solely on the next token
|
|
||||||
switch(this.type) {
|
|
||||||
case tt.name:
|
|
||||||
node.module = this.qml_parseModule();
|
|
||||||
break;
|
|
||||||
case tt.string:
|
|
||||||
node.directoryPath = this.parseLiteral(this.value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.unexpected();
|
|
||||||
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:
|
|
||||||
* <QualifiedId> <VersionLiteral>
|
|
||||||
*/
|
|
||||||
pp.qml_parseModule = function() {
|
|
||||||
var node = this.startNode();
|
|
||||||
|
|
||||||
node.qualifiedId = this.qml_parseQualifiedId();
|
|
||||||
if (this.type === tt.num) {
|
|
||||||
node.version = this.qml_parseVersionLiteral();
|
|
||||||
} else {
|
|
||||||
this.unexpected();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.finishNode(node, "QMLModule");
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parses a QML Version Literal which consists of a major and minor
|
|
||||||
* version separated by a '.'
|
|
||||||
*/
|
|
||||||
pp.qml_parseVersionLiteral = function() {
|
|
||||||
var node = this.startNode();
|
|
||||||
|
|
||||||
node.raw = this.input.slice(this.start, this.end);
|
|
||||||
node.value = this.value;
|
|
||||||
var matches;
|
|
||||||
if (matches = /(\d+)\.(\d+)/.exec(node.raw)) {
|
|
||||||
node.major = parseInt(matches[1]);
|
|
||||||
node.minor = parseInt(matches[2]);
|
|
||||||
} else {
|
|
||||||
this.raise(this.start, "QML module must specify major and minor version");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.next();
|
|
||||||
return this.finishNode(node, "QMLVersionLiteral");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Parses a QML Qualifier of the form:
|
|
||||||
* 'as' <Identifier>
|
|
||||||
*/
|
|
||||||
pp.qml_parseQualifier = function() {
|
|
||||||
var node = this.startNode();
|
|
||||||
this.next();
|
|
||||||
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>)*
|
|
||||||
*/
|
|
||||||
pp.qml_parseQualifiedId = function() {
|
|
||||||
var node = this.startNode();
|
|
||||||
|
|
||||||
node.parts = [];
|
|
||||||
if (!this.qml_isIdent(this.type, this.value)) {
|
|
||||||
this.unexpected();
|
|
||||||
}
|
|
||||||
var id = this.value;
|
|
||||||
this.next();
|
|
||||||
node.parts.push(id);
|
|
||||||
while(this.type === tt.dot) {
|
|
||||||
id += '.';
|
|
||||||
this.next();
|
|
||||||
if (!this.qml_isIdent(this.type, this.value)) {
|
|
||||||
this.unexpected();
|
|
||||||
}
|
|
||||||
id += this.value;
|
|
||||||
node.parts.push(this.value);
|
|
||||||
this.next();
|
|
||||||
}
|
|
||||||
node.raw = id;
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
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 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) {
|
|
||||||
|
|
||||||
// Extend acorn's 'parseTopLevel' method
|
|
||||||
instance.extend("parseTopLevel", function(nextMethod) {
|
|
||||||
return function(node) {
|
|
||||||
// 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.
|
|
||||||
if (!node.body) {
|
|
||||||
node.body = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return acorn;
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
source.. = src/
|
|
||||||
output.. = bin/
|
|
||||||
bin.includes = META-INF/,\
|
|
||||||
.
|
|
|
@ -1,8 +0,0 @@
|
||||||
# Copyright (c) 2013 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
|
|
||||||
|
|
||||||
pluginName=C/C++ Qt Acorn QML Parser
|
|
||||||
providerName=Eclipse CDT
|
|
|
@ -1,51 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
|
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<parent>
|
|
||||||
<groupId>org.eclipse.cdt</groupId>
|
|
||||||
<artifactId>cdt-parent</artifactId>
|
|
||||||
<version>8.8.0-SNAPSHOT</version>
|
|
||||||
<relativePath>../../pom.xml</relativePath>
|
|
||||||
</parent>
|
|
||||||
|
|
||||||
<version>2.0.0-SNAPSHOT</version>
|
|
||||||
<artifactId>org.eclipse.cdt.qt.core.acorn</artifactId>
|
|
||||||
<packaging>eclipse-plugin</packaging>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>generate-parsers</id>
|
|
||||||
<phase>generate-sources</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>run</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<target>
|
|
||||||
<ant antfile="${basedir}/build.xml" target="build"/>
|
|
||||||
</target>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
<execution>
|
|
||||||
<id>clean-parsers</id>
|
|
||||||
<phase>clean</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>run</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<target>
|
|
||||||
<ant antfile="${basedir}/build.xml" target="clean"/>
|
|
||||||
</target>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
|
@ -1,40 +0,0 @@
|
||||||
/*******************************************************************************
|
|
||||||
* 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
|
|
||||||
*******************************************************************************/
|
|
||||||
package org.eclipse.cdt.qt.core.acorn;
|
|
||||||
|
|
||||||
import org.osgi.framework.BundleActivator;
|
|
||||||
import org.osgi.framework.BundleContext;
|
|
||||||
|
|
||||||
public class Activator implements BundleActivator {
|
|
||||||
|
|
||||||
private static BundleContext context;
|
|
||||||
|
|
||||||
static BundleContext getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* (non-Javadoc)
|
|
||||||
* @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
|
|
||||||
*/
|
|
||||||
public void start(BundleContext bundleContext) throws Exception {
|
|
||||||
Activator.context = bundleContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* (non-Javadoc)
|
|
||||||
* @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
|
|
||||||
*/
|
|
||||||
public void stop(BundleContext bundleContext) throws Exception {
|
|
||||||
Activator.context = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -10,4 +10,15 @@
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = require('./inject')(require('acorn'));
|
// This will only be visible globally if we are in a browser environment
|
||||||
|
var acornQML;
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
return module.exports = mod(require("./inject.js"), require("acorn"));
|
||||||
|
if (typeof define == "function" && define.amd) // AMD
|
||||||
|
return define(["./inject.js", "acorn/dist/acorn"], mod);
|
||||||
|
acornQML = mod(injectQML, acorn); // Plain browser env
|
||||||
|
})(function(injectQML, acorn) {
|
||||||
|
return injectQML(acorn);
|
||||||
|
})
|
529
qt/org.eclipse.cdt.qt.core/acorn-qml/inject.js
Normal file
529
qt/org.eclipse.cdt.qt.core/acorn-qml/inject.js
Normal file
|
@ -0,0 +1,529 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* 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 injectQML;
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
return module.exports = mod();
|
||||||
|
if (typeof define == "function" && define.amd) // AMD
|
||||||
|
return define([], mod);
|
||||||
|
injectQML = mod(); // Plain browser env
|
||||||
|
})(function() {
|
||||||
|
return function(acorn) {
|
||||||
|
// Acorn token types
|
||||||
|
var tt = acorn.tokTypes;
|
||||||
|
|
||||||
|
// QML token types
|
||||||
|
var qtt = {};
|
||||||
|
var keywords = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 = {};
|
||||||
|
qtt["_" + name] = name;
|
||||||
|
keywords[name] = {};
|
||||||
|
keywords[name].isPrimitive = options.isPrimitive ? true : false;
|
||||||
|
keywords[name].isQMLContextual = options.isQMLContextual ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parses a set of QML Header Statements which can either be of
|
||||||
|
* the type import or pragma
|
||||||
|
*/
|
||||||
|
pp.qml_parseHeaderStatements = function() {
|
||||||
|
var node = this.startNode()
|
||||||
|
node.statements = [];
|
||||||
|
|
||||||
|
var loop = true;
|
||||||
|
while (loop) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.statements.length > 0) {
|
||||||
|
return this.finishNode(node, "QMLHeaderStatements");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parses a QML Pragma statement of the form:
|
||||||
|
* 'pragma' <Identifier>
|
||||||
|
*/
|
||||||
|
pp.qml_parsePragmaStatement = function() {
|
||||||
|
var node = this.startNode();
|
||||||
|
this.next();
|
||||||
|
node.identifier = 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
|
||||||
|
*/
|
||||||
|
pp.qml_parseImportStatement = function() {
|
||||||
|
var node = this.startNode();
|
||||||
|
this.next();
|
||||||
|
|
||||||
|
// The type of import varies solely on the next token
|
||||||
|
switch(this.type) {
|
||||||
|
case tt.name:
|
||||||
|
node.module = this.qml_parseModule();
|
||||||
|
break;
|
||||||
|
case tt.string:
|
||||||
|
node.directoryPath = this.parseLiteral(this.value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.unexpected();
|
||||||
|
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:
|
||||||
|
* <QualifiedId> <VersionLiteral>
|
||||||
|
*/
|
||||||
|
pp.qml_parseModule = function() {
|
||||||
|
var node = this.startNode();
|
||||||
|
|
||||||
|
node.qualifiedId = this.qml_parseQualifiedId();
|
||||||
|
if (this.type === tt.num) {
|
||||||
|
node.version = this.qml_parseVersionLiteral();
|
||||||
|
} else {
|
||||||
|
this.unexpected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.finishNode(node, "QMLModule");
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parses a QML Version Literal which consists of a major and minor
|
||||||
|
* version separated by a '.'
|
||||||
|
*/
|
||||||
|
pp.qml_parseVersionLiteral = function() {
|
||||||
|
var node = this.startNode();
|
||||||
|
|
||||||
|
node.raw = this.input.slice(this.start, this.end);
|
||||||
|
node.value = this.value;
|
||||||
|
var matches;
|
||||||
|
if (matches = /(\d+)\.(\d+)/.exec(node.raw)) {
|
||||||
|
node.major = parseInt(matches[1]);
|
||||||
|
node.minor = parseInt(matches[2]);
|
||||||
|
} else {
|
||||||
|
this.raise(this.start, "QML module must specify major and minor version");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.next();
|
||||||
|
return this.finishNode(node, "QMLVersionLiteral");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parses a QML Qualifier of the form:
|
||||||
|
* 'as' <Identifier>
|
||||||
|
*/
|
||||||
|
pp.qml_parseQualifier = function() {
|
||||||
|
var node = this.startNode();
|
||||||
|
this.next();
|
||||||
|
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>)*
|
||||||
|
*/
|
||||||
|
pp.qml_parseQualifiedId = function() {
|
||||||
|
var node = this.startNode();
|
||||||
|
|
||||||
|
node.parts = [];
|
||||||
|
if (!this.qml_isIdent(this.type, this.value)) {
|
||||||
|
this.unexpected();
|
||||||
|
}
|
||||||
|
var id = this.value;
|
||||||
|
this.next();
|
||||||
|
node.parts.push(id);
|
||||||
|
while(this.type === tt.dot) {
|
||||||
|
id += '.';
|
||||||
|
this.next();
|
||||||
|
if (!this.qml_isIdent(this.type, this.value)) {
|
||||||
|
this.unexpected();
|
||||||
|
}
|
||||||
|
id += this.value;
|
||||||
|
node.parts.push(this.value);
|
||||||
|
this.next();
|
||||||
|
}
|
||||||
|
node.raw = id;
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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 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) {
|
||||||
|
|
||||||
|
// Extend acorn's 'parseTopLevel' method
|
||||||
|
instance.extend("parseTopLevel", function(nextMethod) {
|
||||||
|
return function(node) {
|
||||||
|
// 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.
|
||||||
|
if (!node.body) {
|
||||||
|
node.body = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return acorn;
|
||||||
|
};
|
||||||
|
})
|
76
qt/org.eclipse.cdt.qt.core/acorn-qml/walk/walk.js
Normal file
76
qt/org.eclipse.cdt.qt.core/acorn-qml/walk/walk.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
return mod(require("acorn/walk"));
|
||||||
|
if (typeof define == "function" && define.amd) // AMD
|
||||||
|
return define([ "acorn/dist/walk" ], mod);
|
||||||
|
mod(acorn.walk); // Plain browser env
|
||||||
|
})(function(walk) {
|
||||||
|
function skipThrough(node, st, c) { c(node, st) }
|
||||||
|
function ignore(node, st, c) {}
|
||||||
|
|
||||||
|
var base = walk.base;
|
||||||
|
base["Program"] = function(node, st, c) {
|
||||||
|
for (var i = 0; i < node.body.length; i++) {
|
||||||
|
var nodeBody = node.body[i];
|
||||||
|
if (node.body[i].type === "QMLObjectLiteral") {
|
||||||
|
c(node.body[i], st, "QMLRootObject");
|
||||||
|
} else {
|
||||||
|
c(node.body[i], st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base["QMLHeaderStatements"] = function(node, st, c) {
|
||||||
|
for (var i = 0; i < node.statements.length; i++) {
|
||||||
|
c(node.statements[i], st, "QMLHeaderStatement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base["QMLHeaderStatement"] = skipThrough;
|
||||||
|
base["QMLImportStatement"] = function(node, st, c) {
|
||||||
|
c(node.module, st);
|
||||||
|
}
|
||||||
|
base["QMLModule"] = ignore;
|
||||||
|
base["QMLPragmaStatement"] = ignore;
|
||||||
|
base["QMLRootObject"] = skipThrough;
|
||||||
|
base["QMLObjectLiteral"] = function(node, st, c) {
|
||||||
|
c(node.block, st);
|
||||||
|
}
|
||||||
|
base["QMLMemberBlock"] = function(node, st, c) {
|
||||||
|
for (var i = 0; i < node.members.length; i++) {
|
||||||
|
c(node.members[i], st, "QMLMember");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base["QMLMember"] = skipThrough;
|
||||||
|
base["QMLPropertyDeclaration"] = function(node, st, c) {
|
||||||
|
c(node.identifier, st, "Pattern");
|
||||||
|
c(node.binding, st);
|
||||||
|
}
|
||||||
|
base["QMLSignalDefinition"] = ignore;
|
||||||
|
base["QMLProperty"] = function(node, st, c) {
|
||||||
|
// c(node.qualifiedId, st)
|
||||||
|
c(node.binding, st);
|
||||||
|
}
|
||||||
|
base["QMLBinding"] = function(node, st, c) {
|
||||||
|
if (node.block) {
|
||||||
|
c(node.block, st);
|
||||||
|
} else {
|
||||||
|
c(node.expr, st, "Expression");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base["QMLStatementBlock"] = function(node, st, c) {
|
||||||
|
for (var i = 0; i < node.statements.length; i++) {
|
||||||
|
c(node.statements[i], st, "Statement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
4
qt/org.eclipse.cdt.qt.core/tern-qml/.gitignore
vendored
Normal file
4
qt/org.eclipse.cdt.qt.core/tern-qml/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/node_modules
|
||||||
|
/.settings
|
||||||
|
.project
|
||||||
|
.tern-project
|
5
qt/org.eclipse.cdt.qt.core/tern-qml/.npmignore
Normal file
5
qt/org.eclipse.cdt.qt.core/tern-qml/.npmignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/test
|
||||||
|
/node_modules
|
||||||
|
/.settings
|
||||||
|
.project
|
||||||
|
.tern-project
|
97
qt/org.eclipse.cdt.qt.core/tern-qml/demo/qml-demo.html
Normal file
97
qt/org.eclipse.cdt.qt.core/tern-qml/demo/qml-demo.html
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>QML Tern Demo</title>
|
||||||
|
|
||||||
|
<!-- CodeMirror -->
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css">
|
||||||
|
<script src="../node_modules/codemirror/lib/codemirror.js"></script>
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror/theme/eclipse.css">
|
||||||
|
<script src="../node_modules/codemirror/addon/hint/show-hint.js"></script>
|
||||||
|
<script src="../node_modules/codemirror/addon/edit/closetag.js"></script>
|
||||||
|
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
|
||||||
|
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>
|
||||||
|
<script src="../node_modules/codemirror/addon/selection/active-line.js"></script>
|
||||||
|
<script src="../node_modules/codemirror/mode/javascript/javascript.js"></script>
|
||||||
|
|
||||||
|
<!-- Acorn -->
|
||||||
|
<script src="../node_modules/acorn/dist/acorn.js"></script>
|
||||||
|
<script src="../node_modules/acorn/dist/acorn_loose.js"></script>
|
||||||
|
<script src="../node_modules/acorn/dist/walk.js"></script>
|
||||||
|
<script src="../node_modules/acorn-qml/inject.js"></script>
|
||||||
|
<script src="../node_modules/acorn-qml/index.js"></script>
|
||||||
|
<script src="../node_modules/acorn-qml/walk/walk.js"></script>
|
||||||
|
|
||||||
|
<!-- Tern JS -->
|
||||||
|
<script src="../node_modules/tern/lib/signal.js"></script>
|
||||||
|
<script src="../node_modules/tern/lib/tern.js"></script>
|
||||||
|
<script src="../node_modules/tern/lib/def.js"></script>
|
||||||
|
<script src="../node_modules/tern/lib/comment.js"></script>
|
||||||
|
<script src="../node_modules/tern/lib/infer.js"></script>
|
||||||
|
<script src="../qml.js"></script>
|
||||||
|
|
||||||
|
<!-- Official CodeMirror Tern addon -->
|
||||||
|
<script src="../node_modules/codemirror/addon/tern/tern.js"></script>
|
||||||
|
|
||||||
|
<!-- Extension of CodeMirror Tern addon -->
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror-javascript/addon/hint/tern/tern-extension.css">
|
||||||
|
<script src="../node_modules/codemirror-javascript/addon/hint/tern/tern-extension.js"></script>
|
||||||
|
<script src="defs/ecma5.json.js"></script>
|
||||||
|
<script src="defs/browser.json.js"></script>
|
||||||
|
|
||||||
|
<!-- CodeMirror Extension -->
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror-extension/addon/hint/show-hint-eclipse.css">
|
||||||
|
<script src="../node_modules/codemirror-extension/addon/hint/show-context-info.js"></script>
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror-extension/addon/hint/show-context-info.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror-extension/addon/hint/templates-hint.css">
|
||||||
|
<script src="../node_modules/codemirror-extension/addon/hint/templates-hint.js"></script>
|
||||||
|
|
||||||
|
<!-- CodeMirror Javascript -->
|
||||||
|
<script src="../node_modules/codemirror-javascript/addon/hint/javascript/javascript-templates.js"></script>
|
||||||
|
|
||||||
|
<!-- Tern Hover -->
|
||||||
|
<link rel="stylesheet" href="../node_modules/codemirror-extension/addon/hover/text-hover.css">
|
||||||
|
<script src="../node_modules/codemirror-extension/addon/hover/text-hover.js"></script>
|
||||||
|
<script src="../node_modules/codemirror-javascript/addon/hint/tern/tern-hover.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Demo with QML Tern plugin </h1>
|
||||||
|
<form><textarea id="code" name="code">import QtQuick 2.3 Window { 	prop: { 		Qt.quit(); 		 	} } </textarea></form>
|
||||||
|
<script type="text/javascript">
|
||||||
|
function passAndHint(cm) {
|
||||||
|
setTimeout(function() {cm.execCommand("autocomplete");}, 100);
|
||||||
|
return CodeMirror.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
function myHint(cm) {
|
||||||
|
return CodeMirror.showHint(cm, CodeMirror.ternHint, {async: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.commands.autocomplete = function(cm) {
|
||||||
|
CodeMirror.showHint(cm, myHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
|
||||||
|
mode: 'text/javascript',
|
||||||
|
theme : "eclipse",
|
||||||
|
styleActiveLine: true,
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
extraKeys: {
|
||||||
|
"'.'": passAndHint,
|
||||||
|
"Ctrl-Space": "autocomplete",
|
||||||
|
"Ctrl-I": function(cm) { CodeMirror.tern.showType(cm); },
|
||||||
|
"Ctrl-B": function(cm) { CodeMirror.tern.jumpToDef(cm); },
|
||||||
|
"Alt-,": function(cm) { CodeMirror.tern.jumpBack(cm); },
|
||||||
|
"Ctrl-Q": function(cm) { CodeMirror.tern.rename(cm); }
|
||||||
|
},
|
||||||
|
gutters: ["CodeMirror-linenumbers"],
|
||||||
|
ternWith: { plugins: { "qml" : true } }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
qt/org.eclipse.cdt.qt.core/tern-qml/package.json
Normal file
16
qt/org.eclipse.cdt.qt.core/tern-qml/package.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "tern-qt",
|
||||||
|
"description": "Tern Qt Plugin",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tern": "^0.16.0",
|
||||||
|
"acorn": "^2.4.0",
|
||||||
|
"acorn-qml": "../acorn-qml"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"test": ">=0.0.5",
|
||||||
|
"codemirror": "^5.6.0",
|
||||||
|
"codemirror-extension": "^0.1.0",
|
||||||
|
"codemirror-javascript": "^0.1.0"
|
||||||
|
}
|
||||||
|
}
|
69
qt/org.eclipse.cdt.qt.core/tern-qml/qml.js
Normal file
69
qt/org.eclipse.cdt.qt.core/tern-qml/qml.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
return mod(require("tern/lib/infer"), require("tern/lib/tern"));
|
||||||
|
if (typeof define == "function" && define.amd) // AMD
|
||||||
|
return define([ "tern/lib/infer", "tern/lib/tern" ], mod);
|
||||||
|
mod(tern, tern); // Plain browser env
|
||||||
|
})(function(infer, tern) {
|
||||||
|
// Define a few shorthand variables/functions
|
||||||
|
var Scope = infer.Scope;
|
||||||
|
function skipThrough(node, st, c) { c(node, st) }
|
||||||
|
function ignore(node, st, c) {}
|
||||||
|
|
||||||
|
// Register the QML plugin in Tern
|
||||||
|
tern.registerPlugin("qml", function(server) {
|
||||||
|
extendTernScopeGatherer(infer.scopeGatherer);
|
||||||
|
extendTernInferWrapper(infer.inferWrapper);
|
||||||
|
extendTernTypeFinder(infer.typeFinder);
|
||||||
|
extendTernSearchVisitor(infer.searchVisitor);
|
||||||
|
server.on("preParse", preParse);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preParse(text, options) {
|
||||||
|
var plugins = options.plugins;
|
||||||
|
if (!plugins) plugins = options.plugins = {};
|
||||||
|
plugins["qml"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendTernScopeGatherer(scopeGatherer) {
|
||||||
|
scopeGatherer["QMLModule"] = function(node, scope, c) {
|
||||||
|
scope.defProp(node.qualifiedId.raw, node.qualifiedId);
|
||||||
|
}
|
||||||
|
scopeGatherer["QMLMemberBlock"] = function(node, scope, c) {
|
||||||
|
var inner = node.scope = new Scope(scope, node);
|
||||||
|
for (var i = 0; i < node.members.length; i++) {
|
||||||
|
c(node.members[i], inner, "QMLMember");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scopeGatherer["QMLStatementBlock"] = function(node, scope, c) {
|
||||||
|
var inner = node.scope = new Scope(scope, node);
|
||||||
|
for (var i = 0; i < node.statements.length; i++) {
|
||||||
|
c(node.statements[i], inner, "Statement");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendTernInferWrapper(inferWrapper) {
|
||||||
|
// TODO: Implement the AST walk methods for inferWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendTernTypeFinder(typeFinder) {
|
||||||
|
// TODO: Implement the AST walk methods for typeFinder
|
||||||
|
}
|
||||||
|
|
||||||
|
function extendTernSearchVisitor(searchVisitor) {
|
||||||
|
// TODO: Implement the AST walk methods for searchVisitor
|
||||||
|
}
|
||||||
|
})
|
Loading…
Add table
Reference in a new issue