blob: 9aaf81bcfd1376b0b2724ca387cd3b6797c2d49b [file] [log] [blame]
/*******************************************************************************
* 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;
},
resolveModule: function (module) {
var impl = this.server.options.resolveModule;
if (impl) {
return impl(module);
}
},
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];
}
}
// 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 a method on the server object that parses a string. We can't make this
// a query due to the fact that tern always does its infer processing on every
// query regardless of its contents.
server.parseString = function (text, options, callback) {
try {
var opts = {
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
ecmaVersion: this.options.ecmaVersion
};
for (var opt in options) {
opts[opt] = options[opt];
}
text = this.signalReturnFirst("preParse", text, opts) || text;
var ast = infer.parse(text, opts);
callback(null, {
ast: ast
});
this.signal("postParse", ast, text);
} catch (err) {
callback(err, null);
}
};
// Create the QML Import Handler
qmlImportHandler = exports.importHandler = new ImportHandler(server);
// Define the 'parseFile' query type.
tern.defineQueryType("parseFile", {
takesFile: true,
run: function (srv, query, file) {
return {
ast: file.ast
};
}
});
// 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);
});
});