mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-04-23 14:42:11 +02:00
1036 lines
No EOL
32 KiB
JavaScript
1036 lines
No EOL
32 KiB
JavaScript
/*******************************************************************************
|
|
* 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 = "<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);
|
|
});
|
|
}); |