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

feat: Implement SourceCode#applyInlineConfig() #17351

Merged
merged 29 commits into from Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3a43264
Implement SourceCode#applyInlineConfig()
nzakas May 4, 2023
398a115
Fix lint errors
nzakas Jul 7, 2023
056cced
Add tests for forbidden methods
nzakas Jul 10, 2023
5ff2a9f
Clean up naming
nzakas Jul 10, 2023
dc94b59
Add more tests
nzakas Jul 13, 2023
3cf661a
Fix last test
nzakas Jul 13, 2023
7e02709
Fixing some bugs -- still tests failing
nzakas Jul 18, 2023
1895168
Further refactoring and bug fixes
nzakas Jul 20, 2023
d579475
Fix test for Node.js 19
nzakas Jul 20, 2023
e3e0282
Forgot to save the file
nzakas Jul 20, 2023
10b5ecb
Remove proxy; update RuleTester/FlatRuleTester
nzakas Aug 30, 2023
81b75eb
Clean up
nzakas Sep 6, 2023
24d417a
Use WeakSet to track method calls
nzakas Sep 11, 2023
c681c11
Update tests/lib/rule-tester/rule-tester.js
nzakas Sep 13, 2023
1af148b
Update tests/lib/rule-tester/rule-tester.js
nzakas Sep 13, 2023
b4ba86d
Re-add tests for FlatRuleTester
nzakas Sep 13, 2023
2180336
Apply feedback
nzakas Sep 15, 2023
271f8ba
Cleanup tests
nzakas Sep 18, 2023
0fc3fe6
Update lib/source-code/source-code.js
nzakas Sep 19, 2023
645265d
Update lib/linter/linter.js
nzakas Sep 19, 2023
41f91df
Fix inline config problems in flat config mode
nzakas Sep 19, 2023
6b8e656
Update JSON parse error message
nzakas Sep 19, 2023
5c9f32f
Fix JSON parse message again
nzakas Sep 19, 2023
36baf92
Update lib/source-code/source-code.js
nzakas Sep 22, 2023
3969b41
Update lib/linter/linter.js
nzakas Sep 22, 2023
22a16a6
Update lib/linter/linter.js
nzakas Sep 22, 2023
ff145b0
Update tests/lib/linter/linter.js
nzakas Sep 22, 2023
14edf42
Apply feedback
nzakas Sep 22, 2023
5a8363f
Merge branch 'get-inline-config' of github.com:eslint/eslint into get…
nzakas Sep 22, 2023
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
12 changes: 11 additions & 1 deletion lib/config/flat-config-schema.js
Expand Up @@ -507,7 +507,7 @@ const eslintrcKeys = [
// Full schema
//-----------------------------------------------------------------------------

exports.flatConfigSchema = {
const flatConfigSchema = {

// eslintrc-style keys that should always error
...Object.fromEntries(eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)])),
Expand All @@ -533,3 +533,13 @@ exports.flatConfigSchema = {
plugins: pluginsSchema,
rules: rulesSchema
};

//-----------------------------------------------------------------------------
// Exports
//-----------------------------------------------------------------------------

module.exports = {
flatConfigSchema,
assertIsRuleSeverity,
assertIsRuleOptions
};
227 changes: 170 additions & 57 deletions lib/linter/linter.js
Expand Up @@ -42,15 +42,15 @@ const
ruleReplacements = require("../../conf/replacements.json");
const { getRuleFromConfig } = require("../config/flat-config-helpers");
const { FlatConfigArray } = require("../config/flat-config-array");

const { RuleValidator } = require("../config/rule-validator");
const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema");
const debug = require("debug")("eslint:linter");
const MAX_AUTOFIX_PASSES = 10;
const DEFAULT_PARSER_NAME = "espree";
const DEFAULT_ECMA_VERSION = 5;
const commentParser = new ConfigCommentParser();
const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
const parserSymbol = Symbol.for("eslint.RuleTester.parser");
const globals = require("../../conf/globals");

//------------------------------------------------------------------------------
// Typedefs
Expand Down Expand Up @@ -145,29 +145,6 @@ function isEspree(parser) {
return !!(parser === espree || parser[parserSymbol] === espree);
}

/**
* Retrieves globals for the given ecmaVersion.
* @param {number} ecmaVersion The version to retrieve globals for.
* @returns {Object} The globals for the given ecmaVersion.
*/
function getGlobalsForEcmaVersion(ecmaVersion) {

switch (ecmaVersion) {
case 3:
return globals.es3;

case 5:
return globals.es5;

default:
if (ecmaVersion < 2015) {
return globals[`es${ecmaVersion + 2009}`];
}

return globals[`es${ecmaVersion}`];
}
}

/**
* 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 @@ -361,13 +338,13 @@ function extractDirectiveComment(value) {
* 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 {SourceCode} sourceCode The SourceCode object to get comments from.
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
* @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from.
* @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: LintMessage[], disableDirectives: DisableDirective[]}}
* A collection of the directive comments that were found, along with any problems that occurred when parsing
*/
function getDirectiveComments(ast, ruleMapper, warnInlineConfig) {
function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) {
const configuredRules = {};
const enabledGlobals = Object.create(null);
const exportedVariables = {};
Expand All @@ -377,7 +354,7 @@ function getDirectiveComments(ast, ruleMapper, warnInlineConfig) {
builtInRules: Rules
});

ast.comments.filter(token => token.type !== "Shebang").forEach(comment => {
sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => {
const { directivePart, justificationPart } = extractDirectiveComment(comment.value);

const match = directivesPattern.exec(directivePart);
Expand Down Expand Up @@ -511,6 +488,71 @@ function getDirectiveComments(ast, ruleMapper, warnInlineConfig) {
};
}

/**
* 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.
nzakas marked this conversation as resolved.
Show resolved Hide resolved
* @param {SourceCode} sourceCode The SourceCode object to get comments from.
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
* @returns {{problems: LintMessage[], disableDirectives: DisableDirective[]}}
* A collection of the directive comments that were found, along with any problems that occurred when parsing
*/
function getDirectiveCommentsForFlatConfig(sourceCode, ruleMapper) {
const problems = [];
const disableDirectives = [];

sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => {
const { directivePart, justificationPart } = extractDirectiveComment(comment.value);

const match = directivesPattern.exec(directivePart);

if (!match) {
return;
}
const directiveText = match[1];
const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText);

if (comment.type === "Line" && !lineCommentSupported) {
return;
}

if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
const message = `${directiveText} comment should not span multiple lines.`;

problems.push(createLintingProblem({
ruleId: null,
message,
loc: comment.loc
}));
return;
}

const directiveValue = directivePart.slice(match.index + directiveText.length);

switch (directiveText) {
case "eslint-disable":
case "eslint-enable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = directiveText.slice("eslint-".length);
const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
const { directives, directiveProblems } = createDisableDirectives(options);

disableDirectives.push(...directives);
problems.push(...directiveProblems);
break;
}

// no default
}
});

return {
problems,
disableDirectives
};
}

/**
* Normalize ECMAScript version from the initial config
* @param {Parser} parser The parser which uses this options.
Expand Down Expand Up @@ -1313,7 +1355,7 @@ class Linter {

const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
? getDirectiveComments(sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };

// augment global scope with declared global variables
Expand All @@ -1324,7 +1366,6 @@ class Linter {
);

const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);

let lintingProblems;

try {
Expand Down Expand Up @@ -1540,19 +1581,6 @@ class Linter {
languageOptions.ecmaVersion
);

/*
* add configured globals and language globals
*
* using Object.assign instead of object spread for performance reasons
* https://github.com/eslint/eslint/issues/16302
*/
const configuredGlobals = Object.assign(
{},
getGlobalsForEcmaVersion(languageOptions.ecmaVersion),
languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0,
languageOptions.globals
);

// double check that there is a parser to avoid mysterious error messages
if (!languageOptions.parser) {
throw new TypeError(`No parser specified for ${options.filename}`);
Expand Down Expand Up @@ -1608,25 +1636,109 @@ class Linter {
}

const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
? getDirectiveComments(
sourceCode.ast,
ruleId => getRuleFromConfig(ruleId, config),
options.warnInlineConfig
)
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };

// augment global scope with declared global variables
addDeclaredGlobals(
sourceCode.scopeManager.scopes[0],
configuredGlobals,
{ exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
);
/*
* Make adjustments based on the language options. For JavaScript,
* this is primarily about adding variables into the global scope
* to account for ecmaVersion and configured globals.
*/
sourceCode.applyLanguageOptions(languageOptions);

const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
const mergedInlineConfig = {
rules: {}
};
const inlineConfigProblems = [];
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved

/*
* Inline config can be either enabled or disabled. If disabled, it's possible
* to detect the inline config and emit a warning (though this is not required).
* So we first check to see if inline config is allowed at all, and if so, we
* need to check if it's a warning or not.
*/
if (options.allowInlineConfig) {

// if inline config should warn then add the warnings
if (options.warnInlineConfig) {
sourceCode.getInlineConfigNodes().forEach(node => {
inlineConfigProblems.push(createLintingProblem({
ruleId: null,
message: `'${sourceCode.text.slice(node.range[0], node.range[1])}' has no effect because you have 'noInlineConfig' setting in ${options.warnInlineConfig}.`,
loc: node.loc,
severity: 1
}));

});
} else {
const inlineConfigResult = sourceCode.applyInlineConfig();

inlineConfigProblems.push(...inlineConfigResult.problems.map(createLintingProblem));

// need to verify rule existence options
if (inlineConfigResult.ok) {
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved

const ruleValidator = new RuleValidator();

for (const { config: inlineConfig, node } of inlineConfigResult.configs) {

Object.keys(inlineConfig.rules).forEach(ruleId => {
const rule = getRuleFromConfig(ruleId, config);
const ruleValue = inlineConfig.rules[ruleId];

if (!rule) {
inlineConfigProblems.push(createLintingProblem({ ruleId, loc: node.loc }));
return;
}

try {

const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];

assertIsRuleOptions(ruleId, ruleValue);
assertIsRuleSeverity(ruleId, ruleOptions[0]);

ruleValidator.validate({
plugins: config.plugins,
rules: {
[ruleId]: ruleOptions
}
});
mergedInlineConfig.rules[ruleId] = ruleValue;
} catch (err) {

let baseMessage = err.message.slice(
err.message.startsWith("Key \"rules\":")
? err.message.indexOf(":", 12) + 1
: err.message.indexOf(":")
).trim();

if (err.messageTemplate) {
baseMessage += ` You passed "${ruleValue}".`;
}

inlineConfigProblems.push(createLintingProblem({
ruleId,
message: `Inline configuration for rule "${ruleId}" is invalid:\n\t${baseMessage}\n`,
loc: node.loc
}));
}
});
}
}
}
}

const commentDirectives = options.allowInlineConfig && !options.warnInlineConfig
? getDirectiveCommentsForFlatConfig(
sourceCode,
ruleId => getRuleFromConfig(ruleId, config)
)
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };

const configuredRules = Object.assign({}, config.rules, mergedInlineConfig.rules, commentDirectives.configuredRules);
nzakas marked this conversation as resolved.
Show resolved Hide resolved
let lintingProblems;

sourceCode.finalize();

try {
lintingProblems = runRules(
sourceCode,
Expand Down Expand Up @@ -1667,6 +1779,7 @@ class Linter {
disableFixes: options.disableFixes,
problems: lintingProblems
.concat(commentDirectives.problems)
.concat(inlineConfigProblems)
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
});
Expand Down