Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: Extract config comment parsing #11091

Merged
merged 6 commits into from Nov 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 7 additions & 112 deletions lib/linter.js
Expand Up @@ -11,7 +11,6 @@

const eslintScope = require("eslint-scope"),
evk = require("eslint-visitor-keys"),
levn = require("levn"),
lodash = require("lodash"),
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
ConfigOps = require("./config/config-ops"),
Expand All @@ -25,13 +24,15 @@ const eslintScope = require("eslint-scope"),
createReportTranslator = require("./report-translator"),
Rules = require("./rules"),
timing = require("./util/timing"),
ConfigCommentParser = require("./util/config-comment-parser"),
astUtils = require("./util/ast-utils"),
pkg = require("../package.json"),
SourceCodeFixer = require("./util/source-code-fixer");

const debug = require("debug")("eslint:linter");
const MAX_AUTOFIX_PASSES = 10;
const DEFAULT_PARSER_NAME = "espree";
const commentParser = new ConfigCommentParser();

//------------------------------------------------------------------------------
// Typedefs
Expand Down Expand Up @@ -59,112 +60,6 @@ const DEFAULT_PARSER_NAME = "espree";
// Helpers
//------------------------------------------------------------------------------

/**
* Parses a list of "name:boolean_value" or/and "name" options divided by comma or
* whitespace.
* @param {string} string The string to parse.
* @param {Comment} comment The comment node which has the string.
* @returns {Object} Result map object of names and boolean values
*/
function parseBooleanConfig(string, comment) {
const items = {};

// Collapse whitespace around `:` and `,` to make parsing easier
const trimmedString = string.replace(/\s*([:,])\s*/g, "$1");

trimmedString.split(/\s|,+/).forEach(name => {
if (!name) {
return;
}

// value defaults to "false" (if not provided), e.g: "foo" => ["foo", "false"]
const [key, value = "false"] = name.split(":");

items[key] = {
value: value === "true",
comment
};
});
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.
* @returns {({success: true, config: Object}|{success: false, error: Problem})} Result map object
*/
function parseJsonConfig(string, location) {
let items = {};

// Parses a JSON-like comment by the same way as parsing CLI option.
try {
items = levn.parse("Object", string) || {};

// Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`.
// Also, commaless notations have invalid severity:
// "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
// Should ignore that case as well.
if (ConfigOps.isEverySeverityValid(items)) {
return {
success: true,
config: items
};
}
} catch (ex) {

// ignore to parse the string by a fallback.
}

/*
* Optionator cannot parse commaless notations.
* But we are supporting that. So this is a fallback for that.
*/
items = {};
const normalizedString = string.replace(/([a-zA-Z0-9\-/]+):/g, "\"$1\":").replace(/(]|[0-9])\s+(?=")/, "$1,");

try {
items = JSON.parse(`{${normalizedString}}`);
} catch (ex) {
return {
success: false,
error: {
ruleId: null,
fatal: true,
severity: 2,
message: `Failed to parse JSON from '${normalizedString}': ${ex.message}`,
line: location.start.line,
column: location.start.column + 1
}
};

}

return {
success: true,
config: 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) {
const items = {};

// Collapse whitespace around ,
string.replace(/\s*,\s*/g, ",").split(/,+/).forEach(name => {
const trimmedName = name.trim();

if (trimmedName) {
items[trimmedName] = true;
}
});
return items;
}

/**
* Ensures that variables representing built-in properties of the Global Object,
* and any globals declared by special block comments, are present in the global
Expand Down Expand Up @@ -243,7 +138,7 @@ function addDeclaredGlobals(globalScope, configGlobals, commentDirectives) {
* @returns {DisableDirective[]} Directives from the comment
*/
function createDisableDirectives(type, loc, value) {
const ruleIds = Object.keys(parseListConfig(value));
const ruleIds = Object.keys(commentParser.parseListConfig(value));
const directiveRules = ruleIds.length ? ruleIds : [null];

return directiveRules.map(ruleId => ({ type, line: loc.line, column: loc.column + 1, ruleId }));
Expand Down Expand Up @@ -296,12 +191,12 @@ function getDirectiveComments(filename, ast, ruleMapper) {
} else if (comment.type === "Block") {
switch (match[1]) {
case "exported":
Object.assign(exportedVariables, parseBooleanConfig(directiveValue, comment));
Object.assign(exportedVariables, commentParser.parseBooleanConfig(directiveValue, comment));
break;

case "globals":
case "global":
Object.assign(enabledGlobals, parseBooleanConfig(directiveValue, comment));
Object.assign(enabledGlobals, commentParser.parseBooleanConfig(directiveValue, comment));
break;

case "eslint-disable":
Expand All @@ -313,7 +208,7 @@ function getDirectiveComments(filename, ast, ruleMapper) {
break;

case "eslint": {
const parseResult = parseJsonConfig(directiveValue, comment.loc);
const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc);

if (parseResult.success) {
Object.keys(parseResult.config).forEach(name => {
Expand Down Expand Up @@ -393,7 +288,7 @@ function findEslintEnv(text) {
eslintEnvPattern.lastIndex = 0;

while ((match = eslintEnvPattern.exec(text))) {
retv = Object.assign(retv || {}, parseListConfig(match[1]));
retv = Object.assign(retv || {}, commentParser.parseListConfig(match[1]));
}

return retv;
Expand Down
144 changes: 144 additions & 0 deletions lib/util/config-comment-parser.js
@@ -0,0 +1,144 @@
/**
* @fileoverview Config Comment Parser
* @author Nicholas C. Zakas
*/

/* eslint-disable class-methods-use-this*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const levn = require("levn"),
ConfigOps = require("../config/config-ops");

const debug = require("debug")("eslint:config-comment-parser");

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

/**
* Object to parse ESLint configuration comments inside JavaScript files.
* @name ConfigCommentParser
*/
module.exports = class ConfigCommentParser {

/**
* Parses a list of "name:boolean_value" or/and "name" options divided by comma or
* whitespace. Used for "global" and "exported" comments.
* @param {string} string The string to parse.
* @param {Comment} comment The comment node which has the string.
* @returns {Object} Result map object of names and boolean values
*/
parseBooleanConfig(string, comment) {
debug("Parsing Boolean config");

const items = {};

// Collapse whitespace around `:` and `,` to make parsing easier
const trimmedString = string.replace(/\s*([:,])\s*/g, "$1");

trimmedString.split(/\s|,+/).forEach(name => {
if (!name) {
return;
}

// value defaults to "false" (if not provided), e.g: "foo" => ["foo", "false"]
const [key, value = "false"] = name.split(":");

items[key] = {
value: value === "true",
comment
};
});
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.
* @returns {({success: true, config: Object}|{success: false, error: Problem})} Result map object
*/
parseJsonConfig(string, location) {
debug("Parsing JSON config");

let items = {};

// Parses a JSON-like comment by the same way as parsing CLI option.
try {
items = levn.parse("Object", string) || {};

// Some tests say that it should ignore invalid comments such as `/*eslint no-alert:abc*/`.
// Also, commaless notations have invalid severity:
// "no-alert: 2 no-console: 2" --> {"no-alert": "2 no-console: 2"}
// Should ignore that case as well.
if (ConfigOps.isEverySeverityValid(items)) {
return {
success: true,
config: items
};
}
} catch (ex) {

debug("Levn parsing failed; falling back to manual parsing.");

// ignore to parse the string by a fallback.
}

/*
* Optionator cannot parse commaless notations.
* But we are supporting that. So this is a fallback for that.
*/
items = {};
const normalizedString = string.replace(/([a-zA-Z0-9\-/]+):/g, "\"$1\":").replace(/(]|[0-9])\s+(?=")/, "$1,");

try {
items = JSON.parse(`{${normalizedString}}`);
} catch (ex) {
debug("Manual parsing failed.");

return {
success: false,
error: {
ruleId: null,
fatal: true,
severity: 2,
message: `Failed to parse JSON from '${normalizedString}': ${ex.message}`,
line: location.start.line,
column: location.start.column + 1
}
};

}

return {
success: true,
config: 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
*/
parseListConfig(string) {
debug("Parsing list config");

const items = {};

// Collapse whitespace around commas
string.replace(/\s*,\s*/g, ",").split(/,+/).forEach(name => {
const trimmedName = name.trim();

if (trimmedName) {
items[trimmedName] = true;
}
});
return items;
}

};