summaryrefslogtreecommitdiff
path: root/tools/eslint/lib/eslint.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/eslint/lib/eslint.js')
-rw-r--r--tools/eslint/lib/eslint.js1059
1 files changed, 1059 insertions, 0 deletions
diff --git a/tools/eslint/lib/eslint.js b/tools/eslint/lib/eslint.js
new file mode 100644
index 0000000000..0a3e39192b
--- /dev/null
+++ b/tools/eslint/lib/eslint.js
@@ -0,0 +1,1059 @@
+/**
+ * @fileoverview Main ESLint object.
+ * @author Nicholas C. Zakas
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+var estraverse = require("estraverse-fb"),
+ escope = require("escope"),
+ environments = require("../conf/environments"),
+ assign = require("object-assign"),
+ rules = require("./rules"),
+ util = require("./util"),
+ RuleContext = require("./rule-context"),
+ timing = require("./timing"),
+ createTokenStore = require("./token-store.js"),
+ EventEmitter = require("events").EventEmitter,
+ escapeRegExp = require("escape-string-regexp");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+// TODO: Remove when estraverse is updated
+estraverse.Syntax.Super = "Super";
+estraverse.VisitorKeys.Super = [];
+
+/**
+ * Parses a list of "name:boolean_value" or/and "name" options divided by comma or
+ * whitespace.
+ * @param {string} string The string to parse.
+ * @returns {Object} Result map object of names and boolean values
+ */
+function parseBooleanConfig(string) {
+ var items = {};
+ // Collapse whitespace around : to make parsing easier
+ string = string.replace(/\s*:\s*/g, ":");
+ // Collapse whitespace around ,
+ string = string.replace(/\s*,\s*/g, ",");
+ string.split(/\s|,+/).forEach(function(name) {
+ if (!name) {
+ return;
+ }
+ var pos = name.indexOf(":"),
+ value;
+ if (pos !== -1) {
+ value = name.substring(pos + 1, name.length);
+ name = name.substring(0, pos);
+ }
+
+ items[name] = (value === "true");
+
+ });
+ return items;
+}
+
+/**
+ * Parses a JSON-like config.
+ * @param {string} string The string to parse.
+ * @param {Object} location Start line and column of comments for potential error message.
+ * @param {Object[]} messages The messages queue for potential error message.
+ * @returns {Object} Result map object
+ */
+function parseJsonConfig(string, location, messages) {
+ var items = {};
+ string = string.replace(/([a-zA-Z0-9\-\/]+):/g, "\"$1\":").replace(/(\]|[0-9])\s+(?=")/, "$1,");
+ try {
+ items = JSON.parse("{" + string + "}");
+ } catch(ex) {
+
+ messages.push({
+ fatal: true,
+ severity: 2,
+ message: "Failed to parse JSON from '" + string + "': " + ex.message,
+ line: location.start.line,
+ column: location.start.column
+ });
+
+ }
+
+ return items;
+}
+
+/**
+ * Parses a config of values separated by comma.
+ * @param {string} string The string to parse.
+ * @returns {Object} Result map of values and true values
+ */
+function parseListConfig(string) {
+ var items = {};
+ // Collapse whitespace around ,
+ string = string.replace(/\s*,\s*/g, ",");
+ string.split(/,+/).forEach(function(name) {
+ name = name.trim();
+ if (!name) {
+ return;
+ }
+ items[name] = true;
+ });
+ return items;
+}
+
+/**
+ * @param {Scope} scope The scope object to check.
+ * @param {string} name The name of the variable to look up.
+ * @returns {Variable} The variable object if found or null if not.
+ */
+function getVariable(scope, name) {
+ var variable = null;
+ scope.variables.some(function(v) {
+ if (v.name === name) {
+ variable = v;
+ return true;
+ } else {
+ return false;
+ }
+
+ });
+ return variable;
+}
+
+/**
+ * Ensures that variables representing built-in properties of the Global Object,
+ * and any globals declared by special block comments, are present in the global
+ * scope.
+ * @param {ASTNode} program The top node of the AST.
+ * @param {Scope} globalScope The global scope.
+ * @param {Object} config The existing configuration data.
+ * @returns {void}
+ */
+function addDeclaredGlobals(program, globalScope, config) {
+ var declaredGlobals = {},
+ explicitGlobals = {},
+ builtin = environments.builtin;
+
+ assign(declaredGlobals, builtin);
+
+ Object.keys(config.env).forEach(function (name) {
+ if (config.env[name]) {
+ var environmentGlobals = environments[name] && environments[name].globals;
+ if (environmentGlobals) {
+ assign(declaredGlobals, environmentGlobals);
+ }
+ }
+ });
+
+ assign(declaredGlobals, config.globals);
+ assign(explicitGlobals, config.astGlobals);
+
+ Object.keys(declaredGlobals).forEach(function(name) {
+ var variable = getVariable(globalScope, name);
+ if (!variable) {
+ variable = new escope.Variable(name, globalScope);
+ variable.eslintExplicitGlobal = false;
+ globalScope.variables.push(variable);
+ }
+ variable.writeable = declaredGlobals[name];
+ });
+
+ Object.keys(explicitGlobals).forEach(function(name) {
+ var variable = getVariable(globalScope, name);
+ if (!variable) {
+ variable = new escope.Variable(name, globalScope);
+ variable.eslintExplicitGlobal = true;
+ globalScope.variables.push(variable);
+ }
+ variable.writeable = explicitGlobals[name];
+ });
+}
+
+/**
+ * Add data to reporting configuration to disable reporting for list of rules
+ * starting from start location
+ * @param {Object[]} reportingConfig Current reporting configuration
+ * @param {Object} start Position to start
+ * @param {string[]} rulesToDisable List of rules
+ * @returns {void}
+ */
+function disableReporting(reportingConfig, start, rulesToDisable) {
+
+ if (rulesToDisable.length) {
+ rulesToDisable.forEach(function(rule) {
+ reportingConfig.push({
+ start: start,
+ end: null,
+ rule: rule
+ });
+ });
+ } else {
+ reportingConfig.push({
+ start: start,
+ end: null,
+ rule: null
+ });
+ }
+}
+
+/**
+ * Add data to reporting configuration to enable reporting for list of rules
+ * starting from start location
+ * @param {Object[]} reportingConfig Current reporting configuration
+ * @param {Object} start Position to start
+ * @param {string[]} rulesToEnable List of rules
+ * @returns {void}
+ */
+function enableReporting(reportingConfig, start, rulesToEnable) {
+ var i;
+
+ if (rulesToEnable.length) {
+ rulesToEnable.forEach(function(rule) {
+ for (i = reportingConfig.length - 1; i >= 0; i--) {
+ if (!reportingConfig[i].end && reportingConfig[i].rule === rule ) {
+ reportingConfig[i].end = start;
+ break;
+ }
+ }
+ });
+ } else {
+ // find all previous disabled locations if they was started as list of rules
+ var prevStart;
+ for (i = reportingConfig.length - 1; i >= 0; i--) {
+ if (prevStart && prevStart !== reportingConfig[i].start) {
+ break;
+ }
+
+ if (!reportingConfig[i].end) {
+ reportingConfig[i].end = start;
+ prevStart = reportingConfig[i].start;
+ }
+ }
+ }
+}
+
+/**
+ * Parses comments in file to extract file-specific config of rules, globals
+ * and environments and merges them with global config; also code blocks
+ * where reporting is disabled or enabled and merges them with reporting config.
+ * @param {ASTNode} ast The top node of the AST.
+ * @param {Object} config The existing configuration data.
+ * @param {Object[]} reportingConfig The existing reporting configuration data.
+ * @param {Object[]} messages The messages queue.
+ * @returns {void}
+ */
+function modifyConfigsFromComments(ast, config, reportingConfig, messages) {
+
+ var commentConfig = {
+ astGlobals: {},
+ rules: {},
+ env: {}
+ };
+ var commentRules = {};
+
+ ast.comments.forEach(function(comment) {
+
+ var value = comment.value.trim();
+ var match = /^(eslint-\w+|eslint-\w+-\w+|eslint|globals?)(\s|$)/.exec(value);
+
+ if (match) {
+ value = value.substring(match.index + match[1].length);
+
+ if (comment.type === "Block") {
+ switch (match[1]) {
+ case "globals":
+ case "global":
+ assign(commentConfig.astGlobals, parseBooleanConfig(value));
+ break;
+
+ case "eslint-env":
+ assign(commentConfig.env, parseListConfig(value));
+ break;
+
+ case "eslint-disable":
+ disableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
+ break;
+
+ case "eslint-enable":
+ enableReporting(reportingConfig, comment.loc.start, Object.keys(parseListConfig(value)));
+ break;
+
+ case "eslint":
+ var items = parseJsonConfig(value, comment.loc, messages);
+ Object.keys(items).forEach(function(name) {
+ var ruleValue = items[name];
+ if (typeof ruleValue === "number" || (Array.isArray(ruleValue) && typeof ruleValue[0] === "number")) {
+ commentRules[name] = ruleValue;
+ }
+ });
+ break;
+
+ // no default
+ }
+ } else {
+ // comment.type === "Line"
+ if (match[1] === "eslint-disable-line") {
+ disableReporting(reportingConfig, { "line": comment.loc.start.line, "column": 0 }, Object.keys(parseListConfig(value)));
+ enableReporting(reportingConfig, comment.loc.end, Object.keys(parseListConfig(value)));
+ }
+ }
+ }
+ });
+
+ // apply environment rules before user rules
+ Object.keys(commentConfig.env).forEach(function (name) {
+ var environmentRules = environments[name] && environments[name].rules;
+ if (commentConfig.env[name] && environmentRules) {
+ assign(commentConfig.rules, environmentRules);
+ }
+ });
+ assign(commentConfig.rules, commentRules);
+
+ util.mergeConfigs(config, commentConfig);
+}
+
+/**
+ * Check if message of rule with ruleId should be ignored in location
+ * @param {Object[]} reportingConfig Collection of ignore records
+ * @param {string} ruleId Id of rule
+ * @param {Object} location Location of message
+ * @returns {boolean} True if message should be ignored, false otherwise
+ */
+function isDisabledByReportingConfig(reportingConfig, ruleId, location) {
+
+ for (var i = 0, c = reportingConfig.length; i < c; i++) {
+
+ var ignore = reportingConfig[i];
+ if ((!ignore.rule || ignore.rule === ruleId) &&
+ (location.line > ignore.start.line || (location.line === ignore.start.line && location.column >= ignore.start.column)) &&
+ (!ignore.end || (location.line < ignore.end.line || (location.line === ignore.end.line && location.column <= ignore.end.column)))) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Process initial config to make it safe to extend by file comment config
+ * @param {Object} config Initial config
+ * @returns {Object} Processed config
+ */
+function prepareConfig(config) {
+
+ config.globals = config.globals || config.global || {};
+ delete config.global;
+
+ var copiedRules = {},
+ ecmaFeatures = {},
+ preparedConfig;
+
+ if (typeof config.rules === "object") {
+ Object.keys(config.rules).forEach(function(k) {
+ var rule = config.rules[k];
+ if (rule === null) {
+ throw new Error("Invalid config for rule '" + k + "'\.");
+ }
+ if (Array.isArray(rule)) {
+ copiedRules[k] = rule.slice();
+ } else {
+ copiedRules[k] = rule;
+ }
+ });
+ }
+
+ // merge in environment ecmaFeatures
+ if (typeof config.env === "object") {
+ Object.keys(config.env).forEach(function(env) {
+ if (config.env[env] && environments[env].ecmaFeatures) {
+ assign(ecmaFeatures, environments[env].ecmaFeatures);
+ }
+ });
+ }
+
+ preparedConfig = {
+ rules: copiedRules,
+ parser: config.parser || "espree",
+ globals: util.mergeConfigs({}, config.globals),
+ env: util.mergeConfigs({}, config.env || {}),
+ settings: util.mergeConfigs({}, config.settings || {}),
+ ecmaFeatures: util.mergeConfigs(ecmaFeatures, config.ecmaFeatures || {})
+ };
+
+ // can't have global return inside of modules
+ if (preparedConfig.ecmaFeatures.modules) {
+ preparedConfig.ecmaFeatures.globalReturn = false;
+ }
+
+ return preparedConfig;
+}
+
+//------------------------------------------------------------------------------
+// Public Interface
+//------------------------------------------------------------------------------
+
+/**
+ * Object that is responsible for verifying JavaScript text
+ * @name eslint
+ */
+module.exports = (function() {
+
+ var api = Object.create(new EventEmitter()),
+ messages = [],
+ currentText = null,
+ currentTextLines = [],
+ currentConfig = null,
+ currentTokens = null,
+ currentScopes = null,
+ scopeMap = null,
+ scopeManager = null,
+ currentFilename = null,
+ controller = null,
+ reportingConfig = [],
+ commentLocsEnter = [],
+ commentLocsExit = [],
+ currentAST = null;
+
+ /**
+ * Parses text into an AST. Moved out here because the try-catch prevents
+ * optimization of functions, so it's best to keep the try-catch as isolated
+ * as possible
+ * @param {string} text The text to parse.
+ * @param {Object} config The ESLint configuration object.
+ * @returns {ASTNode} The AST if successful or null if not.
+ * @private
+ */
+ function parse(text, config) {
+
+ var parser;
+
+ try {
+ parser = require(config.parser);
+ } catch (ex) {
+ messages.push({
+ fatal: true,
+ severity: 2,
+ message: ex.message,
+ line: 0,
+ column: 0
+ });
+
+ return null;
+ }
+
+ /*
+ * Check for parsing errors first. If there's a parsing error, nothing
+ * else can happen. However, a parsing error does not throw an error
+ * from this method - it's just considered a fatal error message, a
+ * problem that ESLint identified just like any other.
+ */
+ try {
+ return parser.parse(text, {
+ loc: true,
+ range: true,
+ raw: true,
+ tokens: true,
+ comment: true,
+ attachComment: true,
+ ecmaFeatures: config.ecmaFeatures
+ });
+ } catch (ex) {
+
+ messages.push({
+ fatal: true,
+ severity: 2,
+
+ // messages come as "Line X: Unexpected token foo", so strip off leading part
+ message: ex.message.substring(ex.message.indexOf(":") + 1).trim(),
+
+ line: ex.lineNumber,
+ column: ex.column
+ });
+
+ return null;
+ }
+ }
+
+ /**
+ * Check collection of comments to prevent double event for comment as
+ * leading and trailing, then emit event if passing
+ * @param {ASTNode[]} comments Collection of comment nodes
+ * @param {Object[]} locs List of locations of previous comment nodes
+ * @param {string} eventName Event name postfix
+ * @returns {void}
+ */
+ function emitComments(comments, locs, eventName) {
+
+ if (comments.length) {
+ comments.forEach(function(node) {
+ if (locs.indexOf(node.loc) >= 0) {
+ locs.splice(locs.indexOf(node.loc), 1);
+ } else {
+ locs.push(node.loc);
+ api.emit(node.type + eventName, node);
+ }
+ });
+ }
+ }
+
+ /**
+ * Shortcut to check and emit enter of comment nodes
+ * @param {ASTNode[]} comments Collection of comment nodes
+ * @returns {void}
+ */
+ function emitCommentsEnter(comments) {
+ emitComments(comments, commentLocsEnter, "Comment");
+ }
+
+ /**
+ * Shortcut to check and emit exit of comment nodes
+ * @param {ASTNode[]} comments Collection of comment nodes
+ * @returns {void}
+ */
+ function emitCommentsExit(comments) {
+ emitComments(comments, commentLocsExit, "Comment:exit");
+ }
+
+ /**
+ * Get the severity level of a rule (0 - none, 1 - warning, 2 - error)
+ * Returns 0 if the rule config is not valid (an Array or a number)
+ * @param {Array|number} ruleConfig rule configuration
+ * @returns {number} 0, 1, or 2, indicating rule severity
+ */
+ function getRuleSeverity(ruleConfig) {
+ if (typeof ruleConfig === "number") {
+ return ruleConfig;
+ } else if (Array.isArray(ruleConfig)) {
+ return ruleConfig[0];
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Get the options for a rule (not including severity), if any
+ * @param {Array|number} ruleConfig rule configuration
+ * @returns {Array} of rule options, empty Array if none
+ */
+ function getRuleOptions(ruleConfig) {
+ if (Array.isArray(ruleConfig)) {
+ return ruleConfig.slice(1);
+ } else {
+ return [];
+ }
+ }
+
+ // set unlimited listeners (see https://github.com/eslint/eslint/issues/524)
+ api.setMaxListeners(0);
+
+ /**
+ * Resets the internal state of the object.
+ * @returns {void}
+ */
+ api.reset = function() {
+ this.removeAllListeners();
+ messages = [];
+ currentAST = null;
+ currentConfig = null;
+ currentText = null;
+ currentTextLines = [];
+ currentTokens = null;
+ currentScopes = null;
+ scopeMap = null;
+ scopeManager = null;
+ controller = null;
+ reportingConfig = [];
+ commentLocsEnter = [];
+ commentLocsExit = [];
+ };
+
+ /**
+ * Verifies the text against the rules specified by the second argument.
+ * @param {string} text The JavaScript text to verify.
+ * @param {Object} config An object whose keys specify the rules to use.
+ * @param {string=} filename The optional filename of the file being checked.
+ * If this is not set, the filename will default to '<input>' in the rule context.
+ * @param {boolean=} saveState Indicates if the state from the last run should be saved.
+ * Mostly useful for testing purposes.
+ * @returns {Object[]} The results as an array of messages or null if no messages.
+ */
+ api.verify = function(text, config, filename, saveState) {
+
+ var ast,
+ shebang,
+ ecmaFeatures,
+ ecmaVersion;
+
+ // set the current parsed filename
+ currentFilename = filename;
+
+ if (!saveState) {
+ this.reset();
+ }
+
+ // there's no input, just exit here
+ if (text.trim().length === 0) {
+ currentText = text;
+ return messages;
+ }
+
+ // process initial config to make it safe to extend
+ config = prepareConfig(config || {});
+
+ ast = parse(text.replace(/^#!([^\r\n]+)/, function(match, captured) {
+ shebang = captured;
+ return "//" + captured;
+ }), config);
+
+ // if espree failed to parse the file, there's no sense in setting up rules
+ if (ast) {
+
+ currentAST = ast;
+
+ // parse global comments and modify config
+ modifyConfigsFromComments(ast, config, reportingConfig, messages);
+
+ // enable appropriate rules
+ Object.keys(config.rules).filter(function(key) {
+ return getRuleSeverity(config.rules[key]) > 0;
+ }).forEach(function(key) {
+
+ var ruleCreator = rules.get(key),
+ severity = getRuleSeverity(config.rules[key]),
+ options = getRuleOptions(config.rules[key]),
+ rule;
+
+ if (ruleCreator) {
+ try {
+ rule = ruleCreator(new RuleContext(
+ key, api, severity, options,
+ config.settings, config.ecmaFeatures
+ ));
+
+ // add all the node types as listeners
+ Object.keys(rule).forEach(function(nodeType) {
+ api.on(nodeType, timing.enabled
+ ? timing.time(key, rule[nodeType])
+ : rule[nodeType]
+ );
+ });
+ } catch(ex) {
+ ex.message = "Error while loading rule '" + key + "': " + ex.message;
+ throw ex;
+ }
+
+ } else {
+ throw new Error("Definition for rule '" + key + "' was not found.");
+ }
+ });
+
+ // save config so rules can access as necessary
+ currentConfig = config;
+ currentText = text;
+ controller = new estraverse.Controller();
+
+ ecmaFeatures = currentConfig.ecmaFeatures;
+ ecmaVersion = (ecmaFeatures.blockBindings || ecmaFeatures.classes ||
+ ecmaFeatures.modules || ecmaFeatures.defaultParams ||
+ ecmaFeatures.destructuring) ? 6 : 5;
+
+
+ // gather data that may be needed by the rules
+ scopeManager = escope.analyze(ast, {
+ ignoreEval: true,
+ nodejsScope: ecmaFeatures.globalReturn,
+ ecmaVersion: ecmaVersion,
+ sourceType: ecmaFeatures.modules ? "module" : "script"
+ });
+ currentScopes = scopeManager.scopes;
+
+ /*
+ * Index the scopes by the start range of their block for efficient
+ * lookup in getScope.
+ */
+ scopeMap = [];
+ currentScopes.forEach(function (scope, index) {
+ var range = scope.block.range[0];
+
+ // Sometimes two scopes are returned for a given node. This is
+ // handled later in a known way, so just don't overwrite here.
+ if (!scopeMap[range]) {
+ scopeMap[range] = index;
+ }
+ });
+
+ /*
+ * Split text here into array of lines so
+ * it's not being done repeatedly
+ * by individual rules.
+ */
+ currentTextLines = currentText.split(/\r\n|\r|\n|\u2028|\u2029/g);
+
+ // Freezing so array isn't accidentally changed by a rule.
+ Object.freeze(currentTextLines);
+
+ currentTokens = createTokenStore(ast.tokens);
+ Object.keys(currentTokens).forEach(function(method) {
+ api[method] = currentTokens[method];
+ });
+
+ // augment global scope with declared global variables
+ addDeclaredGlobals(ast, currentScopes[0], currentConfig);
+
+ // remove shebang comments
+ if (shebang && ast.comments.length && ast.comments[0].value === shebang) {
+ ast.comments.splice(0, 1);
+
+ if (ast.body.length && ast.body[0].leadingComments && ast.body[0].leadingComments[0].value === shebang) {
+ ast.body[0].leadingComments.splice(0, 1);
+ }
+ }
+
+ /*
+ * Each node has a type property. Whenever a particular type of node is found,
+ * an event is fired. This allows any listeners to automatically be informed
+ * that this type of node has been found and react accordingly.
+ */
+ controller.traverse(ast, {
+ enter: function(node, parent) {
+
+ var comments = api.getComments(node);
+
+ emitCommentsEnter(comments.leading);
+ node.parent = parent;
+ api.emit(node.type, node);
+ emitCommentsEnter(comments.trailing);
+ },
+ leave: function(node) {
+
+ var comments = api.getComments(node);
+
+ emitCommentsExit(comments.trailing);
+ api.emit(node.type + ":exit", node);
+ emitCommentsExit(comments.leading);
+ }
+ });
+
+ }
+
+ // sort by line and column
+ messages.sort(function(a, b) {
+ var lineDiff = a.line - b.line;
+
+ if (lineDiff === 0) {
+ return a.column - b.column;
+ } else {
+ return lineDiff;
+ }
+ });
+
+ return messages;
+ };
+
+ /**
+ * Reports a message from one of the rules.
+ * @param {string} ruleId The ID of the rule causing the message.
+ * @param {number} severity The severity level of the rule as configured.
+ * @param {ASTNode} node The AST node that the message relates to.
+ * @param {Object=} location An object containing the error line and column
+ * numbers. If location is not provided the node's start location will
+ * be used.
+ * @param {string} message The actual message.
+ * @param {Object} opts Optional template data which produces a formatted message
+ * with symbols being replaced by this object's values.
+ * @returns {void}
+ */
+ api.report = function(ruleId, severity, node, location, message, opts) {
+
+ if (typeof location === "string") {
+ opts = message;
+ message = location;
+ location = node.loc.start;
+ }
+
+ Object.keys(opts || {}).forEach(function (key) {
+ var rx = new RegExp("{{" + escapeRegExp(key) + "}}", "g");
+ message = message.replace(rx, opts[key]);
+ });
+
+ if (isDisabledByReportingConfig(reportingConfig, ruleId, location)) {
+ return;
+ }
+
+ messages.push({
+ ruleId: ruleId,
+ severity: severity,
+ message: message,
+ line: location.line,
+ column: location.column,
+ nodeType: node.type,
+ source: currentTextLines[location.line - 1] || ""
+ });
+ };
+
+ /**
+ * Gets the source code for the given node.
+ * @param {ASTNode=} node The AST node to get the text for.
+ * @param {int=} beforeCount The number of characters before the node to retrieve.
+ * @param {int=} afterCount The number of characters after the node to retrieve.
+ * @returns {string} The text representing the AST node.
+ */
+ api.getSource = function(node, beforeCount, afterCount) {
+ if (node) {
+ return (currentText !== null) ? currentText.slice(Math.max(node.range[0] - (beforeCount || 0), 0),
+ node.range[1] + (afterCount || 0)) : null;
+ } else {
+ return currentText;
+ }
+
+ };
+
+ /**
+ * Gets the entire source text split into an array of lines.
+ * @returns {Array} The source text as an array of lines.
+ */
+ api.getSourceLines = function() {
+ return currentTextLines;
+ };
+
+ /**
+ * Retrieves an array containing all comments in the source code.
+ * @returns {ASTNode[]} An array of comment nodes.
+ */
+ api.getAllComments = function() {
+ return currentAST.comments;
+ };
+
+ /**
+ * Gets all comments for the given node.
+ * @param {ASTNode} node The AST node to get the comments for.
+ * @returns {Object} The list of comments indexed by their position.
+ */
+ api.getComments = function(node) {
+
+ var leadingComments = node.leadingComments || [],
+ trailingComments = node.trailingComments || [];
+
+ /*
+ * espree adds a "comments" array on Program nodes rather than
+ * leadingComments/trailingComments. Comments are only left in the
+ * Program node comments array if there is no executable code.
+ */
+ if (node.type === "Program") {
+ if (node.body.length === 0) {
+ leadingComments = node.comments;
+ }
+ }
+
+ return {
+ leading: leadingComments,
+ trailing: trailingComments
+ };
+ };
+
+ /**
+ * Retrieves the JSDoc comment for a given node.
+ * @param {ASTNode} node The AST node to get the comment for.
+ * @returns {ASTNode} The BlockComment node containing the JSDoc for the
+ * given node or null if not found.
+ */
+ api.getJSDocComment = function(node) {
+
+ var parent = node.parent,
+ line = node.loc.start.line;
+
+ /**
+ * Finds a JSDoc comment node in an array of comment nodes.
+ * @param {ASTNode[]} comments The array of comment nodes to search.
+ * @returns {ASTNode} The node if found, null if not.
+ * @private
+ */
+ function findJSDocComment(comments) {
+
+ if (comments) {
+ for (var i = comments.length - 1; i >= 0; i--) {
+ if (comments[i].type === "Block" && comments[i].value.charAt(0) === "*") {
+
+ if (line - comments[i].loc.end.line <= 1) {
+ return comments[i];
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ switch (node.type) {
+ case "FunctionDeclaration":
+ return findJSDocComment(node.leadingComments);
+
+ case "ArrowFunctionExpression":
+ case "FunctionExpression":
+
+ if (parent.type !== "CallExpression" || parent.callee !== node) {
+ while (parent && !parent.leadingComments && !/Function/.test(parent.type)) {
+ parent = parent.parent;
+ }
+
+ return parent && (parent.type !== "FunctionDeclaration") ? findJSDocComment(parent.leadingComments) : null;
+ }
+
+ // falls through
+
+ default:
+ return null;
+ }
+ };
+
+ /**
+ * Gets nodes that are ancestors of current node.
+ * @returns {ASTNode[]} Array of objects representing ancestors.
+ */
+ api.getAncestors = function() {
+ return controller.parents();
+ };
+
+ /**
+ * Gets the deepest node containing a range index.
+ * @param {int} index Range index of the desired node.
+ * @returns {ASTNode} [description]
+ */
+ api.getNodeByRangeIndex = function(index) {
+ var result = null;
+
+ estraverse.traverse(controller.root, {
+ enter: function (node) {
+ if (node.range[0] <= index && index < node.range[1]) {
+ result = node;
+ } else {
+ this.skip();
+ }
+ },
+ leave: function (node) {
+ if (node === result) {
+ this.break();
+ }
+ }
+ });
+
+ return result;
+ };
+
+ /**
+ * Gets the scope for the current node.
+ * @returns {Object} An object representing the current node's scope.
+ */
+ api.getScope = function() {
+ var parents = controller.parents(),
+ scope = currentScopes[0];
+
+ // Don't do this for Program nodes - they have no parents
+ if (parents.length) {
+
+ // if current node is function declaration, add it to the list
+ var current = controller.current();
+ if (["FunctionDeclaration", "FunctionExpression",
+ "ArrowFunctionExpression"].indexOf(current.type) >= 0) {
+ parents.push(current);
+ }
+
+ // Ascend the current node's parents
+ for (var i = parents.length - 1; i >= 0; --i) {
+
+ scope = scopeManager.acquire(parents[i]);
+ if (scope) {
+ if (scope.type === "function-expression-name") {
+ return scope.childScopes[0];
+ } else {
+ return scope;
+ }
+ }
+
+ }
+
+ }
+
+ return currentScopes[0];
+ };
+
+ /**
+ * Record that a particular variable has been used in code
+ * @param {string} name The name of the variable to mark as used
+ * @returns {boolean} True if the variable was found and marked as used,
+ * false if not.
+ */
+ api.markVariableAsUsed = function(name) {
+ var scope = this.getScope(),
+ specialScope = currentConfig.ecmaFeatures.globalReturn || currentConfig.ecmaFeatures.modules,
+ variables,
+ i,
+ len;
+
+ // Special Node.js scope means we need to start one level deeper
+ if (scope.type === "global" && specialScope) {
+ scope = scope.childScopes[0];
+ }
+
+ do {
+ variables = scope.variables;
+ for (i = 0, len = variables.length; i < len; i++) {
+ if (variables[i].name === name) {
+ variables[i].eslintUsed = true;
+ return true;
+ }
+ }
+ } while ( (scope = scope.upper) );
+
+ return false;
+ };
+
+ /**
+ * Gets the filename for the currently parsed source.
+ * @returns {string} The filename associated with the source being parsed.
+ * Defaults to "<input>" if no filename info is present.
+ */
+ api.getFilename = function() {
+ if (typeof currentFilename === "string") {
+ return currentFilename;
+ } else {
+ return "<input>";
+ }
+ };
+
+ /**
+ * Defines a new linting rule.
+ * @param {string} ruleId A unique rule identifier
+ * @param {Function} ruleModule Function from context to object mapping AST node types to event handlers
+ * @returns {void}
+ */
+ var defineRule = api.defineRule = function(ruleId, ruleModule) {
+ rules.define(ruleId, ruleModule);
+ };
+
+ /**
+ * Defines many new linting rules.
+ * @param {object} rulesToDefine map from unique rule identifier to rule
+ * @returns {void}
+ */
+ api.defineRules = function(rulesToDefine) {
+ Object.getOwnPropertyNames(rulesToDefine).forEach(function(ruleId) {
+ defineRule(ruleId, rulesToDefine[ruleId]);
+ });
+ };
+
+ /**
+ * Gets the default eslint configuration.
+ * @returns {Object} Object mapping rule IDs to their default configurations
+ */
+ api.defaults = function() {
+ return require("../conf/eslint.json");
+ };
+
+ return api;
+
+}());