1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-08-07 08:15:48 +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:
Matthew Bastien 2015-10-30 11:27:22 -04:00 committed by Gerrit Code Review @ Eclipse.org
parent ae53f82634
commit f9143ebfdc
23 changed files with 808 additions and 674 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.

View file

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

View file

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

View file

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

View file

@ -10,4 +10,15 @@
*******************************************************************************/
'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);
})

View 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;
};
})

View 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");
}
}
})

View file

@ -0,0 +1,4 @@
/node_modules
/.settings
.project
.tern-project

View file

@ -0,0 +1,5 @@
/test
/node_modules
/.settings
.project
.tern-project

View 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&#10;Window {&#10;&#09;prop: {&#10;&#09;&#09;Qt.quit();&#10;&#09;&#09;&#10;&#09;}&#10;}&#10;</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>

View 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"
}
}

View 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
}
})