Skip to content

Commit

Permalink
Chore: Extract config comment parsing (#11091)
Browse files Browse the repository at this point in the history
* Chore: Extract config comment parsing

* Add more debug messages

* Fix linting errors

* Remove unused tests and constructor

* Add comment to updated test

* Fix lint issue
  • Loading branch information
nzakas committed Nov 19, 2018
1 parent 709190f commit f394a1d
Show file tree
Hide file tree
Showing 4 changed files with 388 additions and 112 deletions.
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;
}

};

0 comments on commit f394a1d

Please sign in to comment.