Skip to content

Commit

Permalink
New: Support enhanced parsers (fixes #6974) (#6975)
Browse files Browse the repository at this point in the history
  • Loading branch information
nzakas committed Oct 18, 2016
1 parent 644d25b commit cbbe420
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 5 deletions.
37 changes: 33 additions & 4 deletions lib/eslint.js
Expand Up @@ -28,6 +28,18 @@ const assert = require("assert"),
rules = require("./rules"),
timing = require("./timing");

//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------

/**
* The result of a parsing operation from parseForESLint()
* @typedef {Object} CustomParseResult
* @property {ASTNode} ast The ESTree AST Program node.
* @property {Object} services An object containing additional services related
* to the parser.
*/

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -598,7 +610,8 @@ module.exports = (function() {
* @param {string} text The text to parse.
* @param {Object} config The ESLint configuration object.
* @param {string} filePath The path to the file being parsed.
* @returns {ASTNode} The AST if successful or null if not.
* @returns {ASTNode|CustomParseResult} The AST or parse result if successful,
* or null if not.
* @private
*/
function parse(text, config, filePath) {
Expand Down Expand Up @@ -642,7 +655,11 @@ module.exports = (function() {
* problem that ESLint identified just like any other.
*/
try {
return parser.parse(text, parserOptions);
if (typeof parser.parseForESLint === "function") {
return parser.parseForESLint(text, parserOptions);
} else {
return parser.parse(text, parserOptions);
}
} catch (ex) {

// If the message includes a leading line number, strip it:
Expand Down Expand Up @@ -738,6 +755,7 @@ module.exports = (function() {
api.verify = function(textOrSourceCode, config, filenameOrOptions, saveState) {
const text = (typeof textOrSourceCode === "string") ? textOrSourceCode : null;
let ast,
parseResult,
shebang,
allowInlineConfig;

Expand Down Expand Up @@ -778,7 +796,7 @@ module.exports = (function() {
return messages;
}

ast = parse(
parseResult = parse(
stripUnicodeBOM(text).replace(/^#!([^\r\n]+)/, function(match, captured) {
shebang = captured;
return `//${captured}`;
Expand All @@ -787,6 +805,14 @@ module.exports = (function() {
currentFilename
);

// if this result is from a parseForESLint() method, normalize
if (parseResult && parseResult.ast) {
ast = parseResult.ast;
} else {
ast = parseResult;
parseResult = null;
}

if (ast) {
sourceCode = new SourceCode(text, ast);
}
Expand Down Expand Up @@ -832,7 +858,10 @@ module.exports = (function() {
try {
const ruleContext = new RuleContext(
key, api, severity, options,
config.settings, config.parserOptions, config.parser, ruleCreator.meta);
config.settings, config.parserOptions, config.parser,
ruleCreator.meta,
(parseResult && parseResult.services ? parseResult.services : {})
);

const rule = ruleCreator.create ? ruleCreator.create(ruleContext) :
ruleCreator(ruleContext);
Expand Down
6 changes: 5 additions & 1 deletion lib/rule-context.js
Expand Up @@ -71,8 +71,9 @@ const PASSTHROUGHS = [
* @param {Object} parserOptions The parserOptions settings passed from the config file.
* @param {Object} parserPath The parser setting passed from the config file.
* @param {Object} meta The metadata of the rule
* @param {Object} parserServices The parser services for the rule.
*/
function RuleContext(ruleId, eslint, severity, options, settings, parserOptions, parserPath, meta) {
function RuleContext(ruleId, eslint, severity, options, settings, parserOptions, parserPath, meta, parserServices) {

// public.
this.id = ruleId;
Expand All @@ -82,6 +83,9 @@ function RuleContext(ruleId, eslint, severity, options, settings, parserOptions,
this.parserPath = parserPath;
this.meta = meta;

// create a separate copy and freeze it (it's not nice to freeze other people's objects)
this.parserServices = Object.freeze(Object.assign({}, parserServices));

// private.
this.eslint = eslint;
this.severity = severity;
Expand Down
18 changes: 18 additions & 0 deletions tests/fixtures/parsers/enhanced-parser.js
@@ -0,0 +1,18 @@
var espree = require("espree");

exports.parseForESLint = function(code, options) {
return {
ast: espree.parse(code, options),
services: {
test: {
getMessage() {
return "Hi!";
}
}
}
};
};

exports.parse = function() {
throw new Error("Use parseForESLint() instead.");
};
35 changes: 35 additions & 0 deletions tests/lib/eslint.js
Expand Up @@ -1268,6 +1268,41 @@ describe("eslint", function() {

eslint.verify("0", config, filename);
});

it("should use parseForESLint() in custom parser when custom parser is specified", function() {

const alternateParser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js");

eslint.reset();

const config = { rules: {}, parser: alternateParser };
const messages = eslint.verify("0", config, filename);

assert.equal(messages.length, 0);
});

it("should expose parser services when using parseForESLint() and services are specified", function() {

const alternateParser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js");

eslint.reset();
eslint.defineRule("test-service-rule", function(context) {
return {
Literal(node) {
context.report({
node,
message: context.parserServices.test.getMessage()
});
}
};
});

const config = { rules: { "test-service-rule": 2 }, parser: alternateParser };
const messages = eslint.verify("0", config, filename);

assert.equal(messages.length, 1);
assert.equal(messages[0].message, "Hi!");
});
}

it("should pass parser as parserPath to all rules when default parser is used", function() {
Expand Down
32 changes: 32 additions & 0 deletions tests/lib/rule-context.js
Expand Up @@ -10,6 +10,7 @@
//------------------------------------------------------------------------------

const sinon = require("sinon"),
assert = require("chai").assert,
leche = require("leche"),
realESLint = require("../../lib/eslint"),
RuleContext = require("../../lib/rule-context");
Expand Down Expand Up @@ -95,4 +96,35 @@ describe("RuleContext", function() {
});
});

describe("parserServices", function() {

it("should pass through parserServices properties to context", function() {
const services = {
test: {}
};
const ruleContext = new RuleContext("fake-rule", {}, 2, {}, {}, {}, "espree", {}, services);

assert.equal(ruleContext.parserServices.test, services.test);
});

it("should copy parserServices properties to a new object", function() {
const services = {
test: {}
};
const ruleContext = new RuleContext("fake-rule", {}, 2, {}, {}, {}, "espree", {}, services);

assert.notEqual(ruleContext.parserServices, services);
});

it("should make context.parserServices a frozen object", function() {
const services = {
test: {}
};
const ruleContext = new RuleContext("fake-rule", {}, 2, {}, {}, {}, "espree", {}, services);

assert.ok(Object.isFrozen(ruleContext.parserServices));
});

});

});

0 comments on commit cbbe420

Please sign in to comment.