mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-04-23 06:32:10 +02:00

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