/******************************************************************************* * Copyright (c) 2015 QNX Software Systems and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * 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 = ""; // JavaScript Scope this.jsScope = new infer.Scope(); this.jsScope.name = ""; 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 = ""; 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 = ""; return memScope; }, newJSScope: function (scope, node, fnScope) { var jsScope = new QMLJSScope(scope, node, this.idScope, this.jsScope, fnScope); jsScope.name = ""; 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 === "" ? 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); }); });