kopia lustrzana https://github.com/c9/core
888 wiersze
28 KiB
JavaScript
888 wiersze
28 KiB
JavaScript
/**
|
|
* JavaScript scope analysis module and warning reporter.
|
|
*
|
|
* This handler does a couple of things:
|
|
* 1. It does scope analysis and attaches a scope object to every variable, variable declaration and function declaration
|
|
* 2. It creates markers for undeclared variables
|
|
* 3. It creates markers for unused variables
|
|
* 4. It implements the local variable refactoring
|
|
*
|
|
* @depend ext/jslanguage/parse
|
|
*
|
|
* @copyright 2013, Ajax.org B.V.
|
|
*/
|
|
define(function(require, exports, module) {
|
|
|
|
var baseLanguageHandler = require('plugins/c9.ide.language/base_handler');
|
|
var completeUtil = require("plugins/c9.ide.language/complete_util");
|
|
var handler = module.exports = Object.create(baseLanguageHandler);
|
|
var JSResolver = require('plugins/c9.ide.language.javascript/JSResolver').JSResolver;
|
|
require("treehugger/traverse"); // add traversal functions to trees
|
|
|
|
var CALLBACK_METHODS = ["forEach", "map", "reduce", "filter", "every", "some",
|
|
"__defineGetter__", , "__defineSetter__"];
|
|
var CALLBACK_FUNCTIONS = ["require", "setTimeout", "setInterval"];
|
|
var PROPER = module.exports.PROPER = 80;
|
|
var MAYBE_PROPER = module.exports.MAYBE_PROPER = 1;
|
|
var NOT_PROPER = module.exports.NOT_PROPER = 0;
|
|
var KIND_EVENT = module.exports.KIND_EVENT = "event";
|
|
var KIND_PACKAGE = module.exports.KIND_PACKAGE = "package";
|
|
var KIND_HIDDEN = module.exports.KIND_HIDDEN = "hidden";
|
|
var KIND_DEFAULT = module.exports.KIND_DEFAULT = undefined;
|
|
var IN_CALLBACK_DEF = 1;
|
|
var IN_CALLBACK_BODY = 2;
|
|
var IN_CALLBACK_BODY_MAYBE = 3;
|
|
|
|
var lastValue;
|
|
var lastAST;
|
|
|
|
// Based on https://github.com/jshint/jshint/blob/master/jshint.js#L331
|
|
var GLOBALS = {
|
|
// Literals
|
|
"true": true,
|
|
"false": true,
|
|
"undefined": true,
|
|
"null": true,
|
|
"arguments": true,
|
|
"Infinity": true,
|
|
onmessage: true,
|
|
postMessage: true,
|
|
importScripts: true,
|
|
"continue": true,
|
|
"return": true,
|
|
"else": true,
|
|
// Browser
|
|
ArrayBuffer: true,
|
|
Attr: true,
|
|
Audio: true,
|
|
addEventListener: true,
|
|
applicationCache: true,
|
|
blur: true,
|
|
clearInterval: true,
|
|
clearTimeout: true,
|
|
close: true,
|
|
closed: true,
|
|
DataView: true,
|
|
defaultStatus: true,
|
|
document: true,
|
|
event: true,
|
|
FileReader: true,
|
|
Float32Array: true,
|
|
Float64Array: true,
|
|
FormData: true,
|
|
getComputedStyle: true,
|
|
Int16Array: true,
|
|
Int32Array: true,
|
|
Int8Array: true,
|
|
parent: true,
|
|
print: true,
|
|
removeEventListener: true,
|
|
resizeBy: true,
|
|
resizeTo: true,
|
|
self: true,
|
|
screen: true,
|
|
scroll: true,
|
|
scrollBy: true,
|
|
scrollTo: true,
|
|
sessionStorage: true,
|
|
setInterval: true,
|
|
setTimeout: true,
|
|
SharedWorker: true,
|
|
Uint16Array: true,
|
|
Uint32Array: true,
|
|
Uint8Array: true,
|
|
WebSocket: true,
|
|
window: true,
|
|
Worker: true,
|
|
XMLHttpRequest: true,
|
|
// Devel
|
|
alert: true,
|
|
confirm: true,
|
|
console: true,
|
|
prompt: true,
|
|
// require.js
|
|
define: true,
|
|
// node.js
|
|
__filename: true,
|
|
__dirname: true,
|
|
Buffer: true,
|
|
exports: true,
|
|
GLOBAL: true,
|
|
global: true,
|
|
module: true,
|
|
process: true,
|
|
require: true,
|
|
// Standard
|
|
Array: true,
|
|
Boolean: true,
|
|
Date: true,
|
|
decodeURI: true,
|
|
decodeURIComponent: true,
|
|
encodeURI: true,
|
|
encodeURIComponent: true,
|
|
Error: true,
|
|
'eval': true,
|
|
EvalError: true,
|
|
Function: true,
|
|
hasOwnProperty: true,
|
|
isFinite: true,
|
|
isNaN: true,
|
|
JSON: true,
|
|
Math: true,
|
|
Number: true,
|
|
Object: true,
|
|
parseInt: true,
|
|
parseFloat: true,
|
|
RangeError: true,
|
|
ReferenceError: true,
|
|
RegExp: true,
|
|
String: true,
|
|
requestAnimationFrame: true,
|
|
SyntaxError: true,
|
|
TypeError: true,
|
|
URIError: true,
|
|
// non-standard
|
|
escape: true,
|
|
unescape: true,
|
|
// meteor
|
|
Match: true,
|
|
MeteorSubscribeHandle: true,
|
|
Accounts: true,
|
|
Blaze: true,
|
|
DDP: true,
|
|
EJSON: true,
|
|
Meteor: true,
|
|
Mongo: true,
|
|
Tracker: true,
|
|
Assets: true,
|
|
App: true,
|
|
Plugin: true,
|
|
Package: true,
|
|
Npm: true,
|
|
Cordova: true,
|
|
currentUser: true,
|
|
loggingIn: true,
|
|
Template: true,
|
|
MethodInvocation: true,
|
|
Subscription: true,
|
|
CompileStep: true,
|
|
check: true,
|
|
Email: true,
|
|
HTTP: true,
|
|
ReactiveVar: true,
|
|
Session: true,
|
|
PackageAPI: true,
|
|
};
|
|
|
|
var KEYWORDS = [
|
|
"break",
|
|
"const",
|
|
"continue",
|
|
"delete",
|
|
"do",
|
|
"while",
|
|
"export",
|
|
"for",
|
|
"in",
|
|
"function",
|
|
"if",
|
|
"else",
|
|
"import",
|
|
"instanceof",
|
|
"new",
|
|
"return",
|
|
"switch",
|
|
"this",
|
|
"throw",
|
|
"try",
|
|
"catch",
|
|
"typeof",
|
|
"void",
|
|
"with",
|
|
"debugger"
|
|
];
|
|
|
|
/** @internal */
|
|
handler.GLOBALS = GLOBALS;
|
|
|
|
handler.addGlobals = function(globals) {
|
|
globals.forEach(function(g) {
|
|
GLOBALS[g] = true;
|
|
});
|
|
};
|
|
|
|
handler.handlesLanguage = function(language) {
|
|
// Note that we don't really support jsx here,
|
|
// but rather tolerate it...
|
|
return language === "javascript" || language === "jsx";
|
|
};
|
|
|
|
handler.getResolutions = function(value, ast, markers, callback) {
|
|
var resolver = new JSResolver(value, ast);
|
|
resolver.addResolutions(markers);
|
|
callback(markers);
|
|
};
|
|
|
|
handler.getMaxFileSizeSupported = function() {
|
|
// .25 of current base_handler default
|
|
return .25 * 10 * 1000 * 80;
|
|
};
|
|
|
|
/*
|
|
handler.hasResolution = function(value, ast, marker) {
|
|
if (marker.resolutions && marker.resolutions.length) {
|
|
return true;
|
|
}
|
|
var resolver = new JSResolver(value, ast);
|
|
return resolver.getType(marker);
|
|
};
|
|
*/
|
|
|
|
var scopeId = 0;
|
|
|
|
var Variable = module.exports.Variable = function Variable(declaration) {
|
|
this.declarations = [];
|
|
if (declaration)
|
|
this.declarations.push(declaration);
|
|
this.uses = [];
|
|
this.values = [];
|
|
};
|
|
|
|
Variable.prototype.addUse = function(node) {
|
|
this.uses.push(node);
|
|
};
|
|
|
|
Variable.prototype.addDeclaration = function(node) {
|
|
this.declarations.push(node);
|
|
};
|
|
|
|
Variable.prototype.markProperDeclaration = function(confidence) {
|
|
if (!confidence)
|
|
return;
|
|
else if (!this.properDeclarationConfidence)
|
|
this.properDeclarationConfidence = confidence;
|
|
else if (this.properDeclarationConfidence < PROPER)
|
|
this.properDeclarationConfidence += confidence;
|
|
};
|
|
|
|
Variable.prototype.isProperDeclaration = function() {
|
|
return this.properDeclarationConfidence > MAYBE_PROPER;
|
|
};
|
|
|
|
/**
|
|
* Implements Javascript's scoping mechanism using a hashmap with parent
|
|
* pointers.
|
|
*/
|
|
var Scope = module.exports.Scope = function Scope(parent) {
|
|
this.id = scopeId++;
|
|
this.parent = parent;
|
|
this.vars = {};
|
|
};
|
|
|
|
/**
|
|
* Declare a variable in the current scope
|
|
*/
|
|
Scope.prototype.declare = function(name, resolveNode, properDeclarationConfidence, kind) {
|
|
var result;
|
|
var vars = this.getVars(kind);
|
|
if (!vars['_' + name]) {
|
|
result = vars['_' + name] = new Variable(resolveNode);
|
|
}
|
|
else if (resolveNode) {
|
|
result = vars['_' + name];
|
|
result.addDeclaration(resolveNode);
|
|
}
|
|
if (result) {
|
|
result.markProperDeclaration(properDeclarationConfidence);
|
|
result.kind = kind;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
Scope.prototype.declareAlias = function(kind, originalName, newName) {
|
|
var vars = this.getVars(kind);
|
|
vars["_" + newName] = vars["_" + originalName];
|
|
};
|
|
|
|
Scope.prototype.getVars = function(kind) {
|
|
if (kind)
|
|
return this.vars[kind] = this.vars[kind] || {};
|
|
else
|
|
return this.vars;
|
|
};
|
|
|
|
Scope.prototype.isDeclared = function(name) {
|
|
return !!this.get(name);
|
|
};
|
|
|
|
/**
|
|
* Get possible values of a variable
|
|
* @param name name of variable
|
|
* @return Variable instance
|
|
*/
|
|
Scope.prototype.get = function(name, kind) {
|
|
var vars = this.getVars(kind);
|
|
if (vars['_' + name])
|
|
return vars['_' + name];
|
|
else if (this.parent)
|
|
return this.parent.get(name, kind);
|
|
};
|
|
|
|
Scope.prototype.getVariableNames = function() {
|
|
return this.getNamesByKind(KIND_DEFAULT);
|
|
};
|
|
|
|
Scope.prototype.getNamesByKind = function(kind) {
|
|
var results = [];
|
|
var vars = this.getVars(kind);
|
|
for (var v in vars) {
|
|
if (vars.hasOwnProperty(v) && v !== KIND_HIDDEN && v !== KIND_PACKAGE)
|
|
results.push(v.slice(1));
|
|
}
|
|
if (this.parent) {
|
|
var namesFromParent = this.parent.getNamesByKind(kind);
|
|
for (var i = 0; i < namesFromParent.length; i++) {
|
|
results.push(namesFromParent[i]);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
var SCOPE_ARRAY = Object.keys(GLOBALS).concat(KEYWORDS);
|
|
|
|
handler.getIdentifierRegex = function() {
|
|
// Allow slashes for package names
|
|
return (/[a-zA-Z_0-9\$\/]/);
|
|
};
|
|
|
|
handler.complete = function(doc, ast, pos, options, callback) {
|
|
if (!options.node || options.node.cons === "Var" || options.line[pos.column] === ".")
|
|
return callback();
|
|
|
|
var identifier = options.identifierPrefix;
|
|
|
|
var matches = completeUtil.findCompletions(identifier, SCOPE_ARRAY);
|
|
callback(matches.map(function(m) {
|
|
return {
|
|
name: m,
|
|
replaceText: m,
|
|
icon: null,
|
|
meta: "EcmaScript",
|
|
priority: 0,
|
|
isGeneric: true
|
|
};
|
|
}));
|
|
};
|
|
|
|
/**
|
|
* @param minimalAnalysis Only analyse bare basics, don't investigate errors.
|
|
* Most useful for inference analysis.
|
|
*/
|
|
handler.analyze = function(value, ast, callback, minimalAnalysis) {
|
|
var handler = this;
|
|
var markers = [];
|
|
|
|
if (minimalAnalysis && value === lastValue && lastAST == ast)
|
|
return callback();
|
|
lastValue = value;
|
|
lastAST = ast;
|
|
|
|
// Preclare variables (pre-declares, yo!)
|
|
function preDeclareHoisted(scope, node) {
|
|
node.traverseTopDown(
|
|
// var bla;
|
|
'VarDecl(x)', 'ConstDecl(x)', 'LetDecl(x)', function(b, node) {
|
|
node.setAnnotation("scope", scope);
|
|
scope.declare(b.x.value, b.x, PROPER);
|
|
return node;
|
|
},
|
|
// var bla = 10;
|
|
'VarDeclInit(x, e)', 'ConstDeclInit(x, e)', 'LetDeclInit(x, e)', function(b, node) {
|
|
node.setAnnotation("scope", scope);
|
|
scope.declare(b.x.value, b.x, PROPER);
|
|
},
|
|
// function bla(farg) { }
|
|
'Function(x, _, _)', function(b, node) {
|
|
node.setAnnotation("scope", scope);
|
|
if (b.x.value) {
|
|
scope.declare(b.x.value, b.x, PROPER);
|
|
}
|
|
return node;
|
|
},
|
|
'ImportDecl(_, x)', 'ImportBatchDecl(x)', function(b, node) {
|
|
if (b.x.cons !== "Var")
|
|
return node;
|
|
scope.declare(b.x[0].value, b.x[0], PROPER);
|
|
return node;
|
|
}
|
|
);
|
|
}
|
|
|
|
function scopeAnalyzer(scope, node, parentLocalVars, inCallback) {
|
|
preDeclareHoisted(scope, node);
|
|
node.setAnnotation("scope", scope);
|
|
function analyze(scope, node, inCallback) {
|
|
node.traverseTopDown(
|
|
'Assign(Var(x), e)', function(b, node) {
|
|
if (scope.isDeclared(b.x.value)) {
|
|
node[0].setAnnotation("scope", scope);
|
|
scope.get(b.x.value).addUse(node[0]);
|
|
}
|
|
analyze(scope, b.e, inCallback);
|
|
return node;
|
|
},
|
|
/*
|
|
'Var("this")', function(b, node) {
|
|
if (inCallback === IN_CALLBACK_BODY) {
|
|
markers.push({
|
|
pos: this.getPos(),
|
|
level: 'warning',
|
|
type: 'warning',
|
|
message: "Use of 'this' in callback function"
|
|
});
|
|
}
|
|
else if (inCallback === IN_CALLBACK_BODY_MAYBE) {
|
|
markers.push({
|
|
pos: this.getPos(),
|
|
level: 'info',
|
|
type: 'info',
|
|
message: "Use of 'this' in closure"
|
|
});
|
|
}
|
|
},
|
|
*/
|
|
'ImportDecl(_, x)', 'ImportBatchDecl(x)', function(b, node) {
|
|
return node;
|
|
},
|
|
'Var(x)', function(b, node) {
|
|
node.setAnnotation("scope", scope);
|
|
if (scope.isDeclared(b.x.value)) {
|
|
scope.get(b.x.value).addUse(node);
|
|
}
|
|
else if (b.x.value === "self"
|
|
&& !scope.isDeclared(b.x.value)
|
|
&& handler.isFeatureEnabled("undeclaredVars")) {
|
|
markers.push({
|
|
pos: this.getPos(),
|
|
level: 'warning',
|
|
type: 'warning',
|
|
message: "Use 'window.self' to refer to the 'self' global."
|
|
});
|
|
return;
|
|
}
|
|
return node;
|
|
},
|
|
'Function(x, fargs, body)', function(b, node) {
|
|
var newScope = new Scope(scope);
|
|
node.setAnnotation("localScope", newScope);
|
|
newScope.declare("this");
|
|
b.fargs.forEach(function(farg) {
|
|
farg.setAnnotation("scope", newScope);
|
|
newScope.declare(farg[0].value, farg);
|
|
});
|
|
var inBody = inCallback === IN_CALLBACK_DEF ? IN_CALLBACK_BODY : isCallback(node);
|
|
scopeAnalyzer(newScope, b.body, null, inBody);
|
|
return node;
|
|
},
|
|
'Arrow(fargs, body)', function(b, node) {
|
|
var newScope = new Scope(scope);
|
|
node.setAnnotation("localScope", newScope);
|
|
newScope.declare("this");
|
|
b.fargs.forEach(function(farg) {
|
|
farg.setAnnotation("scope", newScope);
|
|
newScope.declare(farg[0].value, farg);
|
|
});
|
|
scopeAnalyzer(newScope, b.body, null, inCallback);
|
|
return node;
|
|
},
|
|
'Catch(x, body)', function(b, node) {
|
|
var oldVar = scope.get(b.x.value);
|
|
// Temporarily override
|
|
scope.vars["_" + b.x.value] = new Variable(b.x);
|
|
scopeAnalyzer(scope, b.body, parentLocalVars, inCallback);
|
|
// Put back
|
|
scope.vars["_" + b.x.value] = oldVar;
|
|
return node;
|
|
},
|
|
/*
|
|
* Catches errors like these:
|
|
* if (err) callback(err);
|
|
* which in 99% of cases is wrong: a return should be added:
|
|
* if (err) return callback(err);
|
|
*/
|
|
'If(Var("err"), Call(fn, args), None())', function(b, node) {
|
|
// Check if the `err` variable is used somewhere in the function arguments.
|
|
if (b.args.collectTopDown('Var("err")').length > 0 &&
|
|
!b.fn.isMatch('PropAccess(Var("console"), _)') &&
|
|
!b.fn.isMatch('PropAccess(_, "log")'))
|
|
markers.push({
|
|
pos: b.fn.getPos(),
|
|
type: 'warning',
|
|
level: 'warning',
|
|
message: "Did you forget a 'return' here?"
|
|
});
|
|
},
|
|
'PropAccess(_, "lenght")', function(b, node) {
|
|
markers.push({
|
|
pos: node.getPos(),
|
|
type: 'warning',
|
|
level: 'warning',
|
|
message: "Did you mean 'length'?"
|
|
});
|
|
},
|
|
'Call(PropAccess(e1, "bind"), e2)', function(b) {
|
|
analyze(scope, b.e1, 0);
|
|
analyze(scope, b.e2, inCallback);
|
|
return this;
|
|
},
|
|
'Call(e, args)', function(b, node) {
|
|
analyze(scope, b.e, inCallback);
|
|
var newInCallback = inCallback || (isCallbackCall(node) ? IN_CALLBACK_DEF : 0);
|
|
analyze(scope, b.args, newInCallback);
|
|
return node;
|
|
},
|
|
'Block(_)', function(b, node) {
|
|
node.setAnnotation("scope", scope);
|
|
},
|
|
'For(e1, e2, e3, body)', function(b) {
|
|
analyze(scope, b.e1, inCallback);
|
|
analyze(scope, b.e2, inCallback);
|
|
analyze(scope, b.body, inCallback);
|
|
analyze(scope, b.e3, inCallback);
|
|
return node;
|
|
},
|
|
'ForIn(e1, e2, body)', 'ForOf(e1, e2, body)', function(b) {
|
|
analyze(scope, b.e2, inCallback);
|
|
analyze(scope, b.e1, inCallback);
|
|
analyze(scope, b.body, inCallback);
|
|
return node;
|
|
}
|
|
);
|
|
}
|
|
analyze(scope, node, inCallback);
|
|
}
|
|
|
|
if (ast) {
|
|
var rootScope = new Scope();
|
|
scopeAnalyzer(rootScope, ast);
|
|
addDefineWarnings(ast, markers);
|
|
}
|
|
return callback(markers);
|
|
};
|
|
|
|
function addDefineWarnings(ast, markers) {
|
|
var isArchitect;
|
|
var outerStrictNode;
|
|
ast.forEach(function(node) {
|
|
node.rewrite(
|
|
'String("use strict")', function(b, node) {
|
|
outerStrictNode = node;
|
|
},
|
|
'Call(Var("define"), [Function(_, _, body)])', function(b, node) {
|
|
b.body.forEach(function(node) {
|
|
if (outerStrictNode) {
|
|
markers.push({
|
|
pos: outerStrictNode.getPos(),
|
|
type: 'warning',
|
|
level: 'warning',
|
|
message: '"use strict" outside define()'
|
|
});
|
|
}
|
|
|
|
node.rewrite(
|
|
'Assign(PropAccess(Var("main"), "provides"),_)', function(b, node) {
|
|
isArchitect = true;
|
|
},
|
|
'Function("main", _, body)', function(b, node) {
|
|
if (!isArchitect)
|
|
return;
|
|
addCloud9PluginWarnings(b.body, markers);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
function addCloud9PluginWarnings(body, markers) {
|
|
var isCoreSource = /plugins\/c9\./.test(handler.path);
|
|
var pluginVars = {};
|
|
var unloadFunction;
|
|
var unloadReference;
|
|
var maybeUnloadFunction;
|
|
|
|
body.forEach(function(node) {
|
|
node.rewrite(
|
|
'VarDecls(vars)', function(b, node) {
|
|
b.vars.forEach(function(v) {
|
|
v.rewrite(
|
|
'VarDecl(x)', 'LetDecl(x)',
|
|
'VarDeclInit(x, _)', 'LetDeclInit(x, _)',
|
|
function(b, node) {
|
|
pluginVars[b.x.value] = node;
|
|
}
|
|
);
|
|
});
|
|
},
|
|
'Call(PropAccess(Var("plugin"), "on"), [String("unload"), Function(_, _, fn)])', function(b, node) {
|
|
unloadFunction = b.fn;
|
|
},
|
|
'Call(PropAccess(Var("plugin"), "on"), [String("unload"), ref])', function(b, node) {
|
|
unloadReference = b.ref;
|
|
},
|
|
'Function("unload", _, fn)', function(b, node) {
|
|
maybeUnloadFunction = b.fn;
|
|
}
|
|
);
|
|
});
|
|
|
|
if (!unloadFunction && unloadReference && maybeUnloadFunction
|
|
&& unloadReference[0] && unloadReference[0].value === "unload")
|
|
unloadFunction = maybeUnloadFunction;
|
|
|
|
if (!unloadFunction) {
|
|
if (pluginVars.plugin && !unloadReference) {
|
|
markers.push({
|
|
pos: pluginVars.plugin.getPos(),
|
|
type: isCoreSource ? "info" : "warning",
|
|
message:
|
|
isCoreSource
|
|
? "No plugin.on(\"load\", function() {}) and/or plugin.on(\"unload\", function() {}) found"
|
|
: "Missing plugin.on(\"load\", function() {}) or plugin.on(\"unload\", function() {})"
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
var mustUninitVars = {};
|
|
body.traverseTopDown(
|
|
'Assign(Var(x), _)', 'Call(Var(x), "push", _)', function(b, node) {
|
|
if (pluginVars[b.x.value])
|
|
mustUninitVars[b.x.value] = pluginVars[b.x.value];
|
|
}
|
|
);
|
|
|
|
unloadFunction.traverseTopDown(
|
|
'Var(x)', function(b, node) {
|
|
delete mustUninitVars[b.x.value];
|
|
}
|
|
);
|
|
|
|
for (var v in mustUninitVars) {
|
|
if (v === v.toUpperCase())
|
|
continue;
|
|
markers.push({
|
|
pos: mustUninitVars[v].getPos(),
|
|
type: isCoreSource ? "info" : "warning",
|
|
message: "Plugin state; please uninit/reset '" + v + "' in plugin unload function"
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if any callbacks in the current call
|
|
* should definitely get a warning for any uses of 'this'.
|
|
*/
|
|
var isCallbackCall = function(node) {
|
|
var result;
|
|
node.rewrite(
|
|
'Call(PropAccess(_, p), args)', function(b) {
|
|
if (b.args.length === 1 && CALLBACK_METHODS.indexOf(b.p.value) !== -1)
|
|
result = true;
|
|
},
|
|
'Call(Var(f), _)', function(b) {
|
|
if (CALLBACK_FUNCTIONS.indexOf(b.f.value) !== -1)
|
|
result = true;
|
|
}
|
|
);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Determine if the current callback should get a warning marker
|
|
* (IN_CALLBACK_BODY) for any uses of this, or just an info marker
|
|
* (IN_CALLBACK_BODY_MAYBE). Or, none at all (0).
|
|
*/
|
|
var isCallback = function(node) {
|
|
var parent = node.parent;
|
|
var parentParent = parent && parent.parent;
|
|
if (!parentParent)
|
|
return false;
|
|
try {
|
|
if (!parentParent.isMatch)
|
|
console.log("isCallback debug:", JSON.stringify(parentParent));
|
|
} catch (e) {
|
|
// Cannot print circular JSON in server-side tests
|
|
}
|
|
if (parent.isMatch('PropAccess(_, "call")')
|
|
|| parent.isMatch('PropAccess(_, "apply")')
|
|
|| parent.isMatch('PropAccess(_, "bind")')
|
|
|| !parentParent.isMatch('Call(_, _)')
|
|
|| (parentParent.cons === "Call" &&
|
|
parentParent[0].cons === "PropAccess" &&
|
|
parentParent[1].length > 1 &&
|
|
CALLBACK_METHODS.indexOf(parentParent[0][1].value) > -1)
|
|
)
|
|
return false;
|
|
var result = 0;
|
|
node.rewrite(
|
|
'Function(_, fargs, _)', function(b) {
|
|
if (b.fargs.length === 0 || b.fargs[0].cons !== 'FArg')
|
|
return result = IN_CALLBACK_BODY_MAYBE;
|
|
var name = b.fargs[0][0].value;
|
|
result = name === 'err' || name === 'error' || name === 'exc'
|
|
? IN_CALLBACK_BODY
|
|
: IN_CALLBACK_BODY_MAYBE;
|
|
}
|
|
);
|
|
return result;
|
|
};
|
|
|
|
handler.getRefactorings =
|
|
handler.highlightOccurrences = function(doc, fullAst, cursorPos, options, callback) {
|
|
if (!options.node)
|
|
return callback();
|
|
|
|
if (!fullAst.annos.scope) {
|
|
return handler.analyze(doc.getValue(), fullAst, function() {
|
|
handler.highlightOccurrences(doc, fullAst, cursorPos, options, callback);
|
|
}, true);
|
|
}
|
|
|
|
var markers = [];
|
|
var enableRefactorings = [];
|
|
|
|
function highlightVariable(v) {
|
|
if (!v)
|
|
return;
|
|
v.declarations.forEach(function(decl) {
|
|
if (decl.getPos())
|
|
markers.push({
|
|
pos: decl.getPos(),
|
|
type: 'occurrence_main'
|
|
});
|
|
});
|
|
v.uses.forEach(function(node) {
|
|
markers.push({
|
|
pos: node.getPos(),
|
|
type: 'occurrence_other'
|
|
});
|
|
});
|
|
}
|
|
options.node.rewrite(
|
|
'Var(x)', function(b, node) {
|
|
var scope = node.getAnnotation("scope");
|
|
if (!scope)
|
|
return;
|
|
var v = scope.get(b.x.value);
|
|
highlightVariable(v);
|
|
// Let's not enable renaming 'this' and only rename declared variables
|
|
if (b.x.value !== "this" && v)
|
|
enableRefactorings.push("rename");
|
|
},
|
|
'VarDeclInit(x, _)', 'ConstDeclInit(x, _)', 'LetDeclInit(x, _)', function(b) {
|
|
highlightVariable(this.getAnnotation("scope").get(b.x.value));
|
|
enableRefactorings.push("rename");
|
|
},
|
|
'VarDecl(x)', 'ConstDecl(x)', 'LetDecl(x)', function(b) {
|
|
highlightVariable(this.getAnnotation("scope").get(b.x.value));
|
|
enableRefactorings.push("rename");
|
|
},
|
|
'FArg(x)', function(b) {
|
|
highlightVariable(this.getAnnotation("scope").get(b.x.value));
|
|
enableRefactorings.push("rename");
|
|
},
|
|
'Function(x, _, _)', function(b, node) {
|
|
// Only for named functions
|
|
if (!b.x.value || !node.getAnnotation("scope"))
|
|
return;
|
|
highlightVariable(node.getAnnotation("scope").get(b.x.value));
|
|
enableRefactorings.push("rename");
|
|
}
|
|
);
|
|
|
|
callback({
|
|
markers: markers,
|
|
refactorings: enableRefactorings
|
|
});
|
|
};
|
|
|
|
handler.getRenamePositions = function(doc, fullAst, cursorPos, options, callback) {
|
|
var currentNode = options.node;
|
|
if (!fullAst || !currentNode)
|
|
return callback();
|
|
|
|
if (!fullAst.annos.scope) {
|
|
return handler.analyze(doc.getValue(), fullAst, function() {
|
|
handler.getRenamePositions(doc, fullAst, cursorPos, options, callback);
|
|
}, true);
|
|
}
|
|
|
|
var v;
|
|
var mainNode;
|
|
currentNode.rewrite(
|
|
'VarDeclInit(x, _)', 'ConstDeclInit(x, _)', 'LetDeclInit(x, _)', function(b, node) {
|
|
v = node.getAnnotation("scope").get(b.x.value);
|
|
mainNode = b.x;
|
|
},
|
|
'VarDecl(x)', 'ConstDecl(x)', 'LetDecl(x)', function(b, node) {
|
|
v = node.getAnnotation("scope").get(b.x.value);
|
|
mainNode = b.x;
|
|
},
|
|
'FArg(x)', function(b, node) {
|
|
v = node.getAnnotation("scope").get(b.x.value);
|
|
mainNode = node;
|
|
},
|
|
'Function(x, _, _)', function(b, node) {
|
|
if (!b.x.value)
|
|
return;
|
|
v = node.getAnnotation("scope").get(b.x.value);
|
|
mainNode = b.x;
|
|
},
|
|
'Var(x)', function(b, node) {
|
|
v = node.getAnnotation("scope").get(b.x.value);
|
|
mainNode = node;
|
|
}
|
|
);
|
|
|
|
// no mainnode can be found then invoke callback wo value because then we've got no clue
|
|
// what were doing
|
|
if (!mainNode) {
|
|
return callback();
|
|
}
|
|
|
|
var pos = mainNode.getPos();
|
|
var declarations = [];
|
|
var uses = [];
|
|
|
|
var length = pos.ec - pos.sc;
|
|
|
|
// if the annotation cant be found we will skip this to avoid null ref errors
|
|
v && v.declarations.forEach(function(node) {
|
|
if (node !== currentNode[0]) {
|
|
var pos = node.getPos();
|
|
declarations.push({ column: pos.sc, row: pos.sl });
|
|
}
|
|
});
|
|
|
|
v && v.uses.forEach(function(node) {
|
|
if (node !== currentNode) {
|
|
var pos = node.getPos();
|
|
uses.push({ column: pos.sc, row: pos.sl });
|
|
}
|
|
});
|
|
callback({
|
|
length: length,
|
|
pos: {
|
|
row: pos.sl,
|
|
column: pos.sc
|
|
},
|
|
others: declarations.concat(uses),
|
|
declarations: declarations,
|
|
uses: uses
|
|
});
|
|
};
|
|
|
|
});
|