1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-04-23 06:32:10 +02:00
cdt/qt/org.eclipse.cdt.qt.core/tern-qml/qml.js
Matthew Bastien 84b5f4bfd2 Bug 480238 - QML AST in Java
Created a set of Interfaces to represent the JavaScript and QML Ast in
plain Java.  Updated acorn-qml to be able to parse the entirety of QML
syntax as specified by the QML grammar.  Also modified the QML AST to
represent the added syntax and modified tern-qml to handle the new AST
elements.

Changed the way that the QMLAnalyzer handles path resolution.  Paths are
now relative to the local file system.

Note: the normal acorn-qml parser cannot parse the full range of QML
syntax due to ambiguities.  However, the loose parser can.  We're still
waiting on Acorn to bring lookahead to the normal parser in order to
resolve this.

Change-Id: I77c820ad46301975b2a91969a656d428ad9409c1
Signed-off-by: Matthew Bastien <mbastien@blackberry.com>
2015-12-14 11:13:52 -05:00

1034 lines
No EOL
33 KiB
JavaScript

/*******************************************************************************
* 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
*******************************************************************************/
(function (root, mod) {
if (typeof exports === "object" && typeof module === "object") // CommonJS
return mod(exports, require("acorn"), require("acorn/dist/acorn_loose"), require("acorn/dist/walk"),
require("acorn-qml"), require("acorn-qml/loose"), require("acorn-qml/walk"), require("tern"),
require("tern/lib/infer"), require("tern/lib/signal"));
if (typeof define === "function" && define.amd) // AMD
return define(["exports", "acorn/dist/acorn", "acorn/dist/acorn_loose", "acorn/dist/walk", "acorn-qml",
"acorn-qml/loose", "acorn-qml/walk", "tern", "tern/lib/infer", "tern/lib/signal"], mod);
mod(root.ternQML || (root.ternQML = {}), acorn, acorn, acorn.walk, acorn, acorn, acorn.walk, tern, tern, tern.signal); // Plain browser env
})(this, function (exports, acorn, acornLoose, walk, acornQML, acornQMLLoose, acornQMLWalk, tern, infer, signal) {
'use strict';
// Grab 'def' from 'infer' (used for jsDefs)
var def = infer.def;
// 'extend' taken from infer.js
function extend(proto, props) {
var obj = Object.create(proto);
if (props) {
for (var prop in props) obj[prop] = props[prop];
}
return obj;
}
// QML Import Handler
var qmlImportHandler = exports.importHandler = null;
var ImportHandler = function (server) {
this.server = server;
this.imports = null;
};
ImportHandler.prototype = {
reset: function () {
this.imports = null;
},
resolveDirectory: function (file, path) {
var impl = this.server.options.resolveDirectory;
if (impl) {
return impl(file, path);
}
// Getting to this point means that we were unable to find an implementation of
// the 'resolveDirectory' method. The only time this should happen is during
// a test case which we expect to have an import of the style "./ ..." and nothing
// else. This method will simply remove the './', add the file's base, and return.
if (!path) {
// If no path was specified, return the base directory of the file
var dir = file.name;
dir = dir.substring(0, dir.lastIndexOf("/") + 1);
return dir;
}
if (path.substring(0, 2) === "./") {
path = file.directory + path.substring(2);
}
if (path.substr(path.length - 1, 1) !== "/") {
path = path + "/";
}
return path;
},
updateDirectoryImportList: function () {
if (!this.imports) {
this.imports = {};
}
var dir, f;
var seenDirs = {};
for (var i = 0; i < this.server.files.length; i++) {
var file = this.server.files[i];
dir = file.directory;
f = file.nameExt;
if (!dir) {
// Resolve the directory name and file name/extension
dir = file.directory = this.resolveDirectory(file, null);
f = file.nameExt = this.getFileNameAndExtension(file);
}
seenDirs[dir] = true;
// No file scope means the file was recently added/changed and we should
// update its import reference
if (!file.scope) {
// Check for a valid QML Object Identifier
if (f.extension === "qml") {
var ch = f.name.charAt(0);
if (ch.toUpperCase() === ch && f.name.indexOf(".") === -1) {
// Create the array for this directory if necessary
if (!this.imports[dir]) {
this.imports[dir] = {};
}
// Create an Obj to represent this import
var obj = new infer.Obj(null, f.name);
obj.origin = file.name;
this.imports[dir][f.name] = obj;
}
}
}
}
for (dir in this.imports) {
if (!(dir in seenDirs)) {
this.imports[dir] = undefined;
}
}
},
getFileNameAndExtension: function (file) {
var fileName = file.name.substring(file.name.lastIndexOf("/") + 1);
var dot = fileName.lastIndexOf(".");
return {
name: dot >= 0 ? fileName.substring(0, dot) : fileName,
extension: fileName.substring(dot + 1)
};
},
resolveObject: function (loc, name) {
return loc[name];
},
defineImport: function (scope, loc, name, obj) {
var prop = scope.defProp(name);
var objRef = new ObjRef(loc, name, this);
prop.objType = objRef;
objRef.propagate(prop);
},
defineImports: function (file, scope) {
scope = scope || file.scope;
// Add any imports from the current directory
var imports = this.imports[file.directory];
var f = file.nameExt;
if (imports) {
for (var name in imports) {
if (f.name !== name) {
this.defineImport(scope, imports, name, imports[name]);
}
}
}
// Walk the AST for any imports
var ih = this;
walk.simple(file.ast, {
QMLImport: function (node) {
var prop = null;
var scope = file.scope;
if (node.qualifier) {
prop = file.scope.defProp(node.qualifier.id.name, node.qualifier.id);
prop.origin = file.name;
var obj = new infer.Obj(null, node.qualifier.id.name);
obj.propagate(prop);
prop.objType = obj;
scope = obj;
}
if (node.directory) {
var dir = ih.resolveDirectory(file, node.directory.value);
var imports = ih.imports[dir];
if (imports) {
for (var name in imports) {
ih.defineImport(scope, imports, name, imports[name]);
}
}
}
}
});
},
createQMLObjectType: function (file, node, isRoot) {
// Find the imported object
var obj = this.getQMLObjectType(file, node.id);
// If this is the root, connect the imported object to the root object
if (isRoot) {
var tmp = this.getRootQMLObjectType(file, node.id);
if (tmp) {
// Hook up the Obj Reference
tmp.proto = obj;
obj = tmp;
obj.originNode = node.id;
// Break any cyclic dependencies
while ((tmp = tmp.proto)) {
if (tmp.resolve() == obj.resolve()) {
tmp.proto = null;
}
}
}
}
return obj;
},
getQMLObjectType: function (file, qid) {
var prop = findProp(qid, file.scope);
if (prop) {
return prop.objType;
}
return new infer.Obj(null, qid.name);
},
getRootQMLObjectType: function (file, qid) {
var f = file.nameExt;
var imports = this.imports[file.directory];
if (imports && imports[f.name]) {
return imports[f.name];
}
return new infer.Obj(null, qid.name);
}
};
// 'isInteger' taken from infer.js
function isInteger(str) {
var c0 = str.charCodeAt(0);
if (c0 >= 48 && c0 <= 57) return !/\D/.test(str);
else return false;
}
/*
* We have to redefine 'hasProp' to make it work with our scoping. The original 'hasProp'
* function checked proto.props instead of using proto.hasProp.
*/
infer.Obj.prototype.hasProp = function (prop, searchProto) {
if (isInteger(prop)) prop = this.normalizeIntegerProp(prop);
var found = this.props[prop];
if (searchProto !== false && this.proto && !found)
found = this.proto.hasProp(prop, true);
return found;
};
// Creating a resolve function on 'infer.Obj' so we can simplify some of our 'ObjRef' logic
infer.Obj.prototype.resolve = function () {
return this;
};
/*
* QML Object Reference
*
* An ObjRef behaves exactly the same as an ordinary 'infer.Obj' object, except that it
* mirrors its internal state to a referenced object. This object is resolved by the QML
* Import Handler each time the ObjRef is accessed (including getting and setting internal
* variables). In theory this means we don't have to know at runtime whether or not an
* object is an ObjRef or an infer.Obj.
*/
var ObjRef = function (loc, lookup, ih) {
// Using underscores for property names so we don't accidentally collide with any
// 'infer.Obj' property names (which would cause a stack overflow if we were to
// try to access them here).
this._loc = loc;
this._objLookup = lookup;
this._ih = ih;
var obj = this.resolve();
// Use Object.defineProperty to setup getter and setter methods that delegate
// to the resolved object's properties. We only need to do this once since all
// 'infer.Obj' objects should have the same set of property names.
for (var propertyName in obj) {
if (!(obj[propertyName] instanceof Function)) {
(function () {
var prop = propertyName;
Object.defineProperty(this, prop, {
enumerable: true,
get: function () {
return this.resolve()[prop];
},
set: function (value) {
this.resolve()[prop] = value;
}
});
}).call(this);
}
}
};
ObjRef.prototype = extend(infer.Type.prototype, {
resolve: function () {
return this._ih.resolveObject(this._loc, this._objLookup);
}
});
(function () {
// Wire up all base functions to use the resolved object's implementation
for (var _func in infer.Obj.prototype) {
if (_func !== "resolve") {
(function () {
var fn = _func;
ObjRef.prototype[fn] = function () {
return this.resolve()[fn](arguments[0], arguments[1], arguments[2], arguments[3]);
};
})();
}
}
})();
/*
* QML Object Scope (inherits methods from infer.Scope)
*
* A QML Object Scope does not contain its own properties. Instead, its properties
* are defined in its given Object Type and resolved from there. Any properties
* defined within the Object Type are visible without qualifier to any downstream
* scopes.
*/
var QMLObjScope = exports.QMLObjScope = function (prev, originNode, objType) {
infer.Scope.call(this, prev, originNode, false);
this.objType = objType;
};
QMLObjScope.prototype = extend(infer.Scope.prototype, {
hasProp: function (prop, searchProto) {
// Search for a property in the Object type.
// Always search the Object Type's prototype as well
var found = this.objType.hasProp(prop, true);
if (found) {
return found;
}
// Search for a property in the prototype (previous scope)
if (this.proto && searchProto !== false) {
return this.proto.hasProp(prop, searchProto);
}
},
defProp: function (prop, originNode) {
return this.objType.defProp(prop, originNode);
},
removeProp: function (prop) {
return this.objType.removeProp(prop);
},
gatherProperties: function (f, depth) {
// Gather properties from the Object Type and its prototype(s)
var obj = this.objType;
var callback = function (prop, obj, d) {
f(prop, obj, depth);
};
while (obj) {
obj.gatherProperties(callback, depth);
obj = obj.proto;
}
// gather properties from the prototype (previous scope)
if (this.proto) {
this.proto.gatherProperties(f, depth + 1);
}
}
});
/*
* QML Member Scope (inherits methods from infer.Scope)
*
* A QML Member Scope is a bit of a special case when it comes to QML scoping. Like
* the QML Object Scope, it does not contain any properties of its own. The reason
* that it is special is it only gathers properties from its immediate predecessor
* that aren't functions (i.e. They don't have the 'isFunction' flag set. The
* 'isFunction' flag is created by QML signal properties and JavaScript functions
* that are QML Members.)
*/
var QMLMemScope = exports.QMLMemScope = function (prev, originNode, fileScope) {
infer.Scope.call(this, prev, originNode, false);
this.fileScope = fileScope;
};
QMLMemScope.prototype = extend(infer.Scope.prototype, {
hasProp: function (prop, searchProto) {
// Search for a property in the prototype
var found = null;
if (this.proto) {
// Don't continue searching after the previous scope
found = this.proto.hasProp(prop, false);
if (found && !found.isFunction) {
return found;
}
}
// Search for a property in the file Scope
if (this.fileScope) {
return this.fileScope.hasProp(prop, searchProto);
}
},
defProp: function (prop, originNode) {
return this.prev.defProp(prop, originNode);
},
removeProp: function (prop) {
return this.prev.removeProp(prop);
},
gatherProperties: function (f, depth) {
// Gather properties from the prototype (previous scope)
var found = null;
if (this.proto) {
this.proto.gatherProperties(function (prop, obj, d) {
// Don't continue passed the predecessor by checking depth
if (d === depth) {
var propObj = obj.hasProp(prop);
if (propObj && !propObj.isFunction) {
f(prop, obj, d);
}
}
}, depth);
}
// Gather properties from the file Scope
this.fileScope.gatherProperties(f, depth);
}
});
/*
* QML JavaScript Scope (inherits methods from infer.Scope)
*
* A QML JavaScript Scope also contains references to the file's ID Scope, the global
* JavaScript Scope, and a possible function parameter scope. Most likely, this
* scope will not contain its own properties. The resolution order for 'getProp' and
* 'hasProp' are:
* 1. The ID Scope
* 2. This Scope's properties
* 3. The Function Scope (if it exists)
* 4. The JavaScript Scope
* 5. The Previous Scope in the chain
*/
var QMLJSScope = exports.QMLJSScope = function (prev, originNode, idScope, jsScope, fnScope) {
infer.Scope.call(this, prev, originNode, false);
this.idScope = idScope;
this.jsScope = jsScope;
this.fnScope = fnScope;
};
QMLJSScope.prototype = extend(infer.Scope.prototype, {
hasProp: function (prop, searchProto) {
if (isInteger(prop)) {
prop = this.normalizeIntegerProp(prop);
}
// Search the ID scope
var found = null;
if (this.idScope) {
found = this.idScope.hasProp(prop, searchProto);
}
// Search the current scope
if (!found) {
found = this.props[prop];
}
// Search the Function Scope
if (!found && this.fnScope) {
found = this.fnScope.hasProp(prop, searchProto);
}
// Search the JavaScript Scope
if (!found && this.jsScope) {
found = this.jsScope.hasProp(prop, searchProto);
}
// Search the prototype (previous scope)
if (!found && this.proto && searchProto !== false) {
found = this.proto.hasProp(prop, searchProto);
}
return found;
},
gatherProperties: function (f, depth) {
// Gather from the ID Scope
if (this.idScope) {
this.idScope.gatherProperties(f, depth);
}
// Gather from the current scope
for (var prop in this.props) {
f(prop, this, depth);
}
// Gather from the Function Scope
if (this.fnScope) {
this.fnScope.gatherProperties(f, depth);
}
// Gather from the JS Scope
if (this.jsScope) {
this.jsScope.gatherProperties(f, depth);
}
// Gather from the prototype (previous scope)
if (this.proto) {
this.proto.gatherProperties(f, depth + 1);
}
}
});
// QML Scope Builder
var ScopeBuilder = function (file, jsDefs) {
// File Scope
this.scope = file.scope;
this.file = file;
// ID Scope
this.idScope = new infer.Scope();
this.idScope.name = "<qml-id>";
// JavaScript Scope
this.jsScope = new infer.Scope();
this.jsScope.name = "<qml-js>";
var curOrigin = infer.cx().curOrigin;
for (var i = 0; i < jsDefs.length; ++i) {
def.load(jsDefs[i], this.jsScope);
}
infer.cx().curOrigin = curOrigin;
};
ScopeBuilder.prototype = {
newObjScope: function (node) {
var obj = qmlImportHandler.createQMLObjectType(this.file, node, !this.rootScope);
var scope = new QMLObjScope(this.rootScope || this.scope, node, obj);
scope.name = "<qml-obj>";
if (!this.rootScope) {
this.rootScope = scope;
}
return scope;
},
getIDScope: function () {
return this.idScope;
},
newMemberScope: function (objScope, node) {
var memScope = new QMLMemScope(objScope, node, this.scope);
memScope.name = "<qml-member>";
return memScope;
},
newJSScope: function (scope, node, fnScope) {
var jsScope = new QMLJSScope(scope, node, this.idScope, this.jsScope, fnScope);
jsScope.name = "<qml-js>";
return jsScope;
},
};
// Helper for adding a variable to a scope.
function addVar(scope, node) {
return scope.defProp(node.name, node);
}
// Helper for finding a property in a scope.
function findProp(node, scope, pos) {
if (pos === null || pos === undefined || pos < 0) {
pos = Number.MAX_VALUE;
}
if (node.type === "QMLQualifiedID") {
return (function recurse(i, prop) {
if (i >= node.parts.length || pos < node.parts[i].start) {
return prop;
}
if (!prop) {
prop = scope.hasProp(node.parts[i].name);
if (prop) {
return recurse(i + 1, prop);
}
} else {
var obj = prop.getType();
if (obj) {
var p = obj.hasProp(node.parts[i].name);
if (p) {
return recurse(i + 1, p);
}
}
}
return null;
})(0, null);
} else if (node.type === "Identifier") {
return scope.hasProp(node.name);
}
return null;
}
// Helper for getting the current context's scope builder
function getScopeBuilder() {
return infer.cx().qmlScopeBuilder;
}
// 'infer' taken from infer.js
function inf(node, scope, out, name) {
var handler = infer.inferExprVisitor[node.type];
return handler ? handler(node, scope, out, name) : infer.ANull;
}
// Infers the property's type from its given primitive value
function infKind(kind, out) {
// TODO: infer list type
if (kind.primitive) {
switch (kind.id.name) {
case "int":
case "double":
case "real":
infer.cx().num.propagate(out);
break;
case "string":
case "color":
infer.cx().str.propagate(out);
break;
case "boolean":
infer.cx().bool.propagate(out);
break;
}
}
}
// 'ret' taken from infer.js
function ret(f) {
return function (node, scope, out, name) {
var r = f(node, scope, name);
if (out) r.propagate(out);
return r;
};
}
// 'fill' taken from infer.js
function fill(f) {
return function (node, scope, out, name) {
if (!out) out = new infer.AVal();
f(node, scope, out, name);
return out;
};
}
// Helper method to get the last index of an array
function getLastIndex(arr) {
return arr[arr.length - 1];
}
// Helper method to get the signal handler name of a signal function
function getSignalHandlerName(str) {
return "on" + str.charAt(0).toUpperCase() + str.slice(1);
}
// Object which holds two scopes. Used to store both the Member Scope and Object
// Scope for QML Property Bindings and Declarations.
function Scopes(obj, mem) {
this.object = obj;
this.member = mem;
}
// Helper to add functionality to a set of walk methods
function extendWalk(walker, funcs) {
for (var prop in funcs) {
walker[prop] = funcs[prop];
}
}
function extendTernScopeGatherer(scopeGatherer) {
// Extend the Tern scopeGatherer to build up our custom QML scoping
extendWalk(scopeGatherer, {
QMLObjectDefinition: function (node, scope, c) {
var inner = node.scope = getScopeBuilder().newObjScope(node);
c(node.body, inner);
},
QMLObjectBinding: function (node, scope, c) {
var inner = node.scope = getScopeBuilder().newObjScope(node);
c(node.body, inner);
},
QMLObjectInitializer: function (node, scope, c) {
var memScope = node.scope = getScopeBuilder().newMemberScope(scope, node);
for (var i = 0; i < node.members.length; i++) {
var member = node.members[i];
if (member.type === "FunctionDeclaration") {
c(member, scope);
// Insert the JavaScript scope after the Function has had a chance to build it's own scope
var jsScope = getScopeBuilder().newJSScope(scope, member, member.scope);
jsScope.fnType = member.scope.fnType;
member.scope.prev = member.scope.proto = null;
member.scope = jsScope;
// Indicate that the property is a function
var prop = scope.hasProp(member.id.name);
if (prop) {
prop.isFunction = true;
}
} else if (member.type === "QMLPropertyDeclaration" || member.type === "QMLPropertyBinding") {
c(member, new Scopes(scope, memScope));
} else {
c(member, scope);
}
}
},
QMLPropertyDeclaration: function (node, scopes, c) {
var prop = addVar(scopes.member, node.id);
if (node.binding) {
c(node.binding, scopes.object);
}
},
QMLPropertyBinding: function (node, scopes, c) {
// Check for the 'id' property being set
if (node.id.name == "id") {
if (node.binding.type === "QMLScriptBinding") {
var binding = node.binding;
if (!binding.block && binding.script.type === "Identifier") {
node.prop = addVar(getScopeBuilder().getIDScope(), binding.script);
}
}
}
// Delegate down to the expression
c(node.binding, scopes.object);
},
QMLScriptBinding: function (node, scope, c) {
var inner = node.scope = getScopeBuilder().newJSScope(scope, node);
c(node.script, inner);
},
QMLStatementBlock: function (node, scope, c) {
var inner = getScopeBuilder().newJSScope(scope, node);
node.scope = inner;
for (var i = 0; i < node.body.length; i++) {
c(node.body[i], inner, "Statement");
}
},
QMLSignalDefinition: function (node, scope, c) {
// Scope Builder
var sb = getScopeBuilder();
// Define the signal arguments in their own separate scope
var argNames = [];
var argVals = [];
var sigScope = new infer.Scope(null, node);
for (var i = 0; i < node.params.length; i++) {
var param = node.params[i];
argNames.push(param.id.name);
argVals.push(addVar(sigScope, param.id));
}
// Define the signal function type which can be referenced from JavaScript
var sig = addVar(scope, node.id);
sig.isFunction = true;
sig.sigType = new infer.Fn(node.id.name, new infer.AVal(), argVals, argNames, infer.ANull);
sig.sigType.sigScope = sigScope;
// Define the signal handler property
var handler = scope.defProp(getSignalHandlerName(node.id.name), node.id);
handler.sig = sig.sigType;
}
});
}
function extendTernInferExprVisitor(inferExprVisitor) {
// Extend the inferExprVisitor methods
extendWalk(inferExprVisitor, {
QMLStatementBlock: ret(function (node, scope, name) {
return infer.ANull; // TODO: check return statements
}),
QMLScriptBinding: fill(function (node, scope, out, name) {
return inf(node.script, node.scope, out, name);
}),
QMLObjectBinding: ret(function (node, scope, name) {
return node.scope.objType;
}),
QMLArrayBinding: ret(function (node, scope, name) {
return new infer.Arr(null); // TODO: populate with type of array contents
})
});
}
function extendTernInferWrapper(inferWrapper) {
// Extend the inferWrapper methods
extendWalk(inferWrapper, {
QMLObjectDefinition: function (node, scope, c) {
c(node.body, node.scope);
},
QMLObjectBinding: function (node, scope, c) {
c(node.body, node.scope);
},
QMLObjectInitializer: function (node, scope, c) {
for (var i = 0; i < node.members.length; i++) {
var member = node.members[i];
if (member.type === "QMLPropertyDeclaration" || member.type === "QMLPropertyBinding") {
c(member, new Scopes(scope, node.scope));
} else {
c(member, scope);
}
}
},
QMLPropertyDeclaration: function (node, scopes, c) {
var prop = findProp(node.id, scopes.member);
if (prop) {
infKind(node.kind, prop);
if (node.binding) {
c(node.binding, scopes.object);
inf(node.binding, scopes.object, prop, node.id.name);
}
}
},
QMLPropertyBinding: function (node, scopes, c) {
c(node.binding, scopes.object);
// Check for the 'id' property being set
if (node.id.name === "id") {
if (node.binding.type === "QMLScriptBinding") {
var binding = node.binding;
if (binding.script.type === "Identifier") {
scopes.object.objType.propagate(node.prop);
}
}
} else {
var prop = findProp(node.id, scopes.member);
if (prop) {
if (prop.sig) {
// This is a signal handler
node.binding.scope.fnScope = prop.sig.sigScope;
} else {
inf(node.binding, scopes.object, prop, getLastIndex(node.id.parts));
}
}
}
},
QMLScriptBinding: function (node, scope, c) {
c(node.script, node.scope);
},
QMLStatementBlock: function (node, scope, c) {
for (var i = 0; i < node.body.length; i++) {
c(node.body[i], node.scope, "Statement");
}
},
QMLSignalDefinition: function (node, scope, c) {
var sig = scope.getProp(node.id.name);
for (var i = 0; i < node.params.length; i++) {
var param = node.params[i];
infKind(param.kind, sig.sigType.args[i]);
}
sig.sigType.retval = infer.ANull;
sig.sigType.propagate(sig);
var handler = scope.getProp(getSignalHandlerName(node.id.name));
var obj = new infer.Obj(true, "Signal Handler");
obj.propagate(handler);
}
});
}
function extendTernTypeFinder(typeFinder) {
// Extend the type finder to return valid types for QML AST elements
extendWalk(typeFinder, {
QMLObjectDefinition: function (node, scope) {
return node.scope.objType;
},
QMLObjectBinding: function (node, scope) {
return node.scope.objType;
},
QMLObjectInitializer: function (node, scope) {
return infer.ANull;
},
FunctionDeclaration: function (node, scope) {
// Quick little hack to get 'findExprAt' to find a Function Declaration which
// is a QML Object Member. All other Function Declarations are ignored.
return scope.name === "<qml-obj>" ? infer.ANull : undefined;
},
QMLScriptBinding: function (node, scope) {
// Trick Tern into thinking this node is a type so that it will use
// this node's scope when handling improperly written script bindings
return infer.ANull;
},
QMLQualifiedID: function (node, scope) {
return findProp(node, scope) || infer.ANull;
},
QML_ID: function (node, scope) {
// Reverse the hack from search visitor before finding the property in
// the id scope
node.type = "Identifier";
return findProp(node, getScopeBuilder().getIDScope());
}
});
}
function extendTernSearchVisitor(searchVisitor) {
// Extend the search visitor to traverse the scope properly
extendWalk(searchVisitor, {
QMLObjectDefinition: function (node, scope, c) {
c(node.body, node.scope);
},
QMLObjectBinding: function (node, scope, c) {
c(node.body, node.scope);
},
QMLObjectInitializer: function (node, scope, c) {
for (var i = 0; i < node.members.length; i++) {
var member = node.members[i];
if (member.type === "QMLPropertyDeclaration" || member.type === "QMLPropertyBinding") {
c(member, new Scopes(scope, node.scope));
} else {
c(member, scope);
}
}
},
QMLSignalDefinition: function (node, scope, c) {
c(node.id, scope);
},
QMLPropertyDeclaration: function (node, scopes, c) {
c(node.id, scopes.member);
if (node.binding) {
c(node.binding, scopes.object);
}
},
QMLPropertyBinding: function (node, scopes, c) {
if (node.id.name === "id") {
if (node.binding.type === "QMLScriptBinding") {
var binding = node.binding;
if (binding.script.type === "Identifier") {
// Hack to bypass Tern's type finding algorithm which uses node.type instead
// of the overriden type.
binding.script.type = "QML_ID";
c(binding.script, binding.scope, "QML_ID");
binding.script.type = "Identifier";
}
}
var prop = findProp(node.id, scopes.member);
if (!prop) {
return;
}
}
c(node.id, scopes.member);
c(node.binding, scopes.object);
},
QMLScriptBinding: function (node, scope, c) {
c(node.script, node.scope);
},
QML_ID: function (node, st, c) {
// Ignore
},
QMLStatementBlock: function (node, scope, c) {
for (var i = 0; i < node.body.length; i++) {
c(node.body[i], node.scope, "Statement");
}
}
});
}
/*
* Prepares acorn to consume QML syntax rather than standard JavaScript
*/
function preParse(text, options) {
// Force ECMA Version to 5
options.ecmaVersion = 5;
// Register qml plugin with main parser
var plugins = options.plugins;
if (!plugins) plugins = options.plugins = {};
plugins.qml = true;
// Register qml plugin with loose parser
var pluginsLoose = options.pluginsLoose;
if (!pluginsLoose) pluginsLoose = options.pluginsLoose = {};
pluginsLoose.qml = true;
}
/*
* Initializes the file's top level scope and creates a ScopeBuilder to facilitate
* the creation of QML scopes.
*/
function beforeLoad(file) {
// We dont care for the Context's top scope
file.scope = null;
// Update the ImportHandler
qmlImportHandler.updateDirectoryImportList();
// Create the file's top scope
file.scope = new infer.Scope(infer.cx().topScope);
var name = file.name;
var end = file.name.lastIndexOf(".qml");
file.scope.name = end > 0 ? name.substring(0, end) : name;
// Get the ImportHandler to define imports for us
qmlImportHandler.defineImports(file, file.scope);
// Create the ScopeBuilder
var sb = new ScopeBuilder(file, infer.cx().parent.jsDefs);
infer.cx().qmlScopeBuilder = sb;
}
/*
* Helper to reset some of the internal state of the QML plugin when the server
* resets
*/
function reset() {
qmlImportHandler.reset();
}
/*
* Called when a completions query is made to the server
*/
function completions(file, query) {
// We can get relatively simple completions on QML Object Types for free if we
// update the Context.paths variable. Tern uses this variable to complete
// non Member Expressions that contain a '.' character. Will be much more
// accurate if we just roll our own completions for this in the future, but it
// works relatively well for now.
var cx = infer.cx();
cx.paths = {};
for (var prop in file.scope.props) {
cx.paths[prop] = file.scope[prop];
}
}
/*
* Called when a parse query is made to the server.
*/
function parse(srv, query, file) {
var ast = null;
if (query.file) {
// Get the file's AST. It should have been parsed already by the server.
ast = file.ast;
} else if (query.text) {
// Parse the file manually and get the AST.
var text = query.text;
var options = query.options || {
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
ecmaVersion: srv.options.ecmaVersion
};
srv.signalReturnFirst("preParse", text, options);
try {
ast = acorn.parse(text, options);
} catch (e) {
ast = acorn.parse_dammit(text, options);
}
srv.signal("postParse", ast, text);
}
return {
ast: ast
};
}
// Register the QML plugin in Tern
tern.registerPlugin("qml", function (server) {
// First we want to replace the top-level defs array with our own and save the
// JavaScript specific defs to a new array 'jsDefs'. In order to make sure no
// other plugins mess with the new defs after us, we override addDefs.
server.jsDefs = server.defs;
server.defs = [];
server.addDefs = function (defs, toFront) {
if (toFront) this.jsDefs.unshift(defs);
else this.jsDefs.push(defs);
if (this.cx) this.reset();
};
// Create the QML Import Handler
qmlImportHandler = exports.importHandler = new ImportHandler(server);
// Define the 'parseFile' and 'parseString' query types. The reason we need
// two separate queries for these is that Tern will not allow us to resolve
// a file without 'takesFile' being true. However, if we set 'takesFile' to
// true, Tern will not allow a null file in the query.
tern.defineQueryType("parseFile", {
takesFile: true,
run: parse
});
tern.defineQueryType("parseString", {
run: parse
});
// Hook into server signals
server.on("preParse", preParse);
server.on("beforeLoad", beforeLoad);
server.on("postReset", reset);
server.on("completion", completions);
// Extend Tern's inferencing system to include QML syntax
extendTernScopeGatherer(infer.scopeGatherer);
extendTernInferExprVisitor(infer.inferExprVisitor);
extendTernInferWrapper(infer.inferWrapper);
extendTernTypeFinder(infer.typeFinder);
extendTernSearchVisitor(infer.searchVisitor);
});
});