blob: 8860585008227138372c5948255cc8fddd6e40e2 [file] [log] [blame]
"use strict";
var _ = require("underscore");
var util = require("util");
var Peakle = require("peakle").Peakle;
var warnings = require("./constants.js").warnings;
var errors = require("./constants.js").errors;
function safe(name) {
if (name === "__proto__")
return "(__proto__)";
var special = Object.getOwnPropertyNames(Object.prototype);
for (var i = 0; i < special.length; i++) {
if (name === special[i])
return "(" + name + ")";
}
return name;
}
function interpolate(string, data) {
return string.replace(/\{([^{}]*)\}/g, function (match, key) {
var repl = data[key];
if (typeof repl === 'string' || typeof repl === 'number')
return repl;
return match;
});
}
// ScopeStack stores all the environments we encounter while
// traversing syntax trees. It also keeps track of all
// variables defined and/or used in these environments.
//
// We use linked-list implementation of a stack. The first
// element, representing global environment, doesn't have
// a reference to its parent.
//
// runtimeOnly means that we can't tell if identifier
// is a variable or a property by analysing the source. It
// is true only within the `with` statement.
function ScopeStack() {
this.stack = [];
this.curid = null;
this.runtimeOnly = false;
this.push("(global)");
}
ScopeStack.prototype = {
get current() {
if (this.curid === null)
return null;
return this.stack[this.curid];
},
get parent() {
if (this.curid === null)
return null;
var parid = this.current.parid;
if (parid === null)
return null;
return this.stack[parid];
},
// Push a new environment into the stack.
push: function (name) {
var curid = this.curid;
this.curid = this.stack.length;
this.stack.push({
parid: curid,
name: name,
strict: false,
switches: {},
ignores: {},
vars: {},
uses: {}
});
},
// Exit from the current environment. Even though
// this method is called `pop` it doesn't actually
// delete the environment--it simply jumps into the
// parent one.
pop: function () {
this.curid = this.current.parid;
},
any: function (cond, env) {
env = env || this.current;
while (env) {
if (cond.call(env))
return true;
env = this.stack[env.parid];
}
return false;
},
isDefined: function (name, env) {
return this.any(function () { return _.has(this.vars, safe(name)); }, env);
},
isStrictMode: function (env) {
return this.any(function () { return this.strict; }, env);
},
isSwitchEnabled: function (name, env) {
return this.any(function () { return this.switches[name]; }, env);
},
isMessageIgnored: function (code, env) {
return this.any(function () { return this.ignores[code]; }, env);
},
addUse: function (name, range) {
name = safe(name);
if (this.runtimeOnly)
return;
if (this.current.uses[name] === undefined)
this.current.uses[name] = [range];
else
this.current.uses[name].push(range);
},
addVariable: function (opts) {
this.current.vars[safe(opts.name)] = {
writeable: opts.writeable || false
};
},
addGlobalVariable: function (opts) {
this.stack[0].vars[safe(opts.name)] = {
writeable: opts.writeable || false
};
},
addSwitch: function (name) {
this.current.switches[name] = true;
},
addIgnore: function (name) {
this.current.ignores[name] = true;
}
};
function Report(source) {
this.ERROR = 1;
this.WARNING = 2;
this.length = 0;
this.messages = {};
this.ranges = [];
this.source = source;
}
Report.prototype = {
lineFromRange: function (range) {
var lines = this.source.slice(0, range[0]).split("\n");
return lines.length || -1;
},
getMessages: function (cond) {
var ret = [];
cond = cond || function () { return true; };
_.each(this.messages, function (pool) {
_.each(pool, function (msg) {
if (cond(msg))
ret.push(msg);
});
});
return ret;
},
get errors() {
var type = this.ERROR;
return this.getMessages(function (msg) {
return msg.type === type;
});
},
get warnings() {
var type = this.WARNING;
return this.getMessages(function (msg) {
return msg.type === type;
});
},
addMessage: function (obj) {
var line = obj.line;
this.messages[line] = _.union(this.messages[line] || [], [obj]);
this.length += 1;
},
addWarning: function (label, loc, data) {
var line = _.isArray(loc) ? this.lineFromRange(loc) : loc;
var warn = warnings[label];
if (!warn)
throw new Error("Warning " + label + "is not defined.");
warn.desc = interpolate(warn.desc, data);
this.addMessage({
type: this.WARNING,
line: line,
data: warn
});
},
addError: function (label, loc, data) {
var line = _.isArray(loc) ? this.lineFromRange(loc) : loc;
var err = errors[label];
if (!err)
throw new Error("Error " + label + " is not defined.");
err.desc = interpolate(err.desc, data);
this.addMessage({
type: this.ERROR,
line: line,
data: err
});
}
};
function Token(obj) {
_.extend(this, obj);
}
_.each(["Punctuator", "Keyword", "Identifier"], function (name) {
Token.prototype["is" + name] = function (values) {
if (!Array.isArray(values))
values = [ values ];
return values.some(function (value) {
return this.type === name && this.value === value;
}, this);
};
});
function Tokens(list) {
var self = this;
Peakle.call(self, list);
// A hash-table to make tokens lookup by their starting
// position cheaper (see Tokens.find for more info).
self.byStart = {};
self.list = _.map(list || [], function (obj, i) {
var token = new Token(obj);
self.byStart[token.range[0]] = i;
return token;
});
}
util.inherits(Tokens, Peakle);
Tokens.prototype.find = function (rangeIndex) {
// First try to lookup our token in byStart in
// case this index is the starting point for the token.
var index = this.byStart[rangeIndex];
if (index)
return index;
// If we could find it, step back, token by token
// until we find one that starts before the one we're
// looking for,
var cur = rangeIndex - 1;
do {
index = this.byStart[cur];
cur = cur - 1;
} while (index === undefined && cur > 0);
// If we're in the beginning and still nothing--return.
if (index === undefined)
return -1;
// Otherwise go in a slow O(N) loop looking for our token.
for (var i = index; i < this.length; i++) {
if (this.list[i].range[0] >= rangeIndex)
return i;
}
return -1;
};
Tokens.prototype.getRange = function (range) {
var slice = [];
var length = this.list.length;
var token;
for (var i = this.byStart[range[0]] || 0; i < length; i++) {
token = this.list[i];
if (token.range[0] < range[0])
continue;
if (token.range[1] <= range[1])
slice.push(token);
else
break;
}
return new Tokens(slice);
};
var commentsTypes = { "set": true, "ignore": true };
function parseComment(text) {
var parts = text.trim().split(" ");
var defval = { type: "text", value: text };
var head, body;
if (parts.length === 0)
return defval;
head = parts[0].split(":");
if (head[0] !== "jshint" || commentsTypes[head[1]] !== true)
return defval;
body = parts.slice(1).join(" ").split(",").map(function (s) {
return s.trim();
});
return {
type: head[1],
value: body
};
}
exports.Report = Report;
exports.Token = Token;
exports.Tokens = Tokens;
exports.ScopeStack = ScopeStack;
exports.parseComment = parseComment;