/******************************************************************************* | |
* 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); | |
}); | |
}); |