Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
refactor: Easier RuleContext creation
refs #18787
  • Loading branch information
nzakas committed May 7, 2025
commit ea2eb9c44fc60c15531087ed9df94fd1739ff5a5
11 changes: 11 additions & 0 deletions lib/linter/file-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ class FileContext {
getSourceCode() {
return this.sourceCode;
}

/**
* Creates a new object with the current object as the prototype and
* the specified properties as its own properties.
* @param {Object} extension The properties to add to the new object.
* @returns {FileContext} A new object with the current object as the prototype
* and the specified properties as its own properties.
*/
extend(extension) {
return Object.freeze(Object.assign(Object.create(this), extension));
}
}

exports.FileContext = FileContext;
102 changes: 50 additions & 52 deletions lib/linter/linter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,7 @@ function runRules(
* All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
* properties once for each rule.
*/
const sharedTraversalContext = new FileContext({
const fileContext = new FileContext({
cwd,
filename,
physicalFilename: physicalFilename || filename,
Expand Down Expand Up @@ -1221,63 +1221,61 @@ function runRules(

const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(Object.create(sharedTraversalContext), {
id: ruleId,
options: getRuleOptions(
configuredRules[ruleId],
applyDefaultOptions ? rule.meta?.defaultOptions : void 0,
),
report(...args) {
/*
* Create a report translator lazily.
* In a vast majority of cases, any given rule reports zero errors on a given
* piece of code. Creating a translator lazily avoids the performance cost of
* creating a new translator function for each rule that usually doesn't get
* called.
*
* Using lazy report translators improves end-to-end performance by about 3%
* with Node 8.4.0.
*/
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes,
language,
});
}
const problem = reportTranslator(...args);
const ruleContext = fileContext.extend({
id: ruleId,
options: getRuleOptions(
configuredRules[ruleId],
applyDefaultOptions ? rule.meta?.defaultOptions : void 0,
),
report(...args) {
/*
* Create a report translator lazily.
* In a vast majority of cases, any given rule reports zero errors on a given
* piece of code. Creating a translator lazily avoids the performance cost of
* creating a new translator function for each rule that usually doesn't get
* called.
*
* Using lazy report translators improves end-to-end performance by about 3%
* with Node 8.4.0.
*/
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes,
language,
});
}
const problem = reportTranslator(...args);

if (problem.fix && !(rule.meta && rule.meta.fixable)) {
throw new Error(
'Fixable rules must set the `meta.fixable` property to "code" or "whitespace".',
);
}
if (problem.fix && !(rule.meta && rule.meta.fixable)) {
throw new Error(
'Fixable rules must set the `meta.fixable` property to "code" or "whitespace".',
);
}
if (
problem.suggestions &&
!(rule.meta && rule.meta.hasSuggestions === true)
) {
if (
problem.suggestions &&
!(rule.meta && rule.meta.hasSuggestions === true)
rule.meta &&
rule.meta.docs &&
typeof rule.meta.docs.suggestion !== "undefined"
) {
if (
rule.meta &&
rule.meta.docs &&
typeof rule.meta.docs.suggestion !== "undefined"
) {
// Encourage migration from the former property name.
throw new Error(
"Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.",
);
}
// Encourage migration from the former property name.
throw new Error(
"Rules with suggestions must set the `meta.hasSuggestions` property to `true`.",
"Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.",
);
}
lintingProblems.push(problem);
},
}),
);
throw new Error(
"Rules with suggestions must set the `meta.hasSuggestions` property to `true`.",
);
}
lintingProblems.push(problem);
},
});

const ruleListenersReturn =
timing.enabled || stats
Expand Down
177 changes: 177 additions & 0 deletions tests/lib/linter/file-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* @fileoverview Tests for FileContext class.
* @author Nicholas C. Zakas
*/

"use strict";

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

const assert = require("chai").assert;
const { FileContext } = require("../../../lib/linter/file-context");

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

describe("FileContext", () => {
const mockSourceCode = {};
const defaultConfig = {
cwd: "/path/to/project",
filename: "test.js",
physicalFilename: "/path/to/project/test.js",
sourceCode: mockSourceCode,
parserOptions: { ecmaVersion: 2021 },
parserPath: "/path/to/parser",
languageOptions: { ecmaVersion: 2022 },
settings: { env: { es6: true } },
};

describe("constructor", () => {
it("should create a frozen instance with all properties set", () => {
const context = new FileContext(defaultConfig);

assert.strictEqual(context.cwd, defaultConfig.cwd);
assert.strictEqual(context.filename, defaultConfig.filename);
assert.strictEqual(
context.physicalFilename,
defaultConfig.physicalFilename,
);
assert.strictEqual(context.sourceCode, defaultConfig.sourceCode);
assert.deepStrictEqual(
context.parserOptions,
defaultConfig.parserOptions,
);
assert.strictEqual(context.parserPath, defaultConfig.parserPath);
assert.deepStrictEqual(
context.languageOptions,
defaultConfig.languageOptions,
);
assert.deepStrictEqual(context.settings, defaultConfig.settings);

// Verify the instance is frozen
assert.throws(() => {
context.cwd = "changed";
}, TypeError);
});

it("should allow partial configuration", () => {
const partialConfig = {
cwd: "/path/to/project",
filename: "test.js",
physicalFilename: "/path/to/project/test.js",
sourceCode: mockSourceCode,
};

const context = new FileContext(partialConfig);

assert.strictEqual(context.cwd, partialConfig.cwd);
assert.strictEqual(context.filename, partialConfig.filename);
assert.strictEqual(
context.physicalFilename,
partialConfig.physicalFilename,
);
assert.strictEqual(context.sourceCode, partialConfig.sourceCode);
assert.isUndefined(context.parserOptions);
assert.isUndefined(context.parserPath);
assert.isUndefined(context.languageOptions);
assert.isUndefined(context.settings);
});
});

describe("deprecated methods", () => {
let context;

beforeEach(() => {
context = new FileContext(defaultConfig);
});

it("getCwd() should return the cwd property", () => {
assert.strictEqual(context.getCwd(), context.cwd);
assert.strictEqual(context.getCwd(), defaultConfig.cwd);
});

it("getFilename() should return the filename property", () => {
assert.strictEqual(context.getFilename(), context.filename);
assert.strictEqual(context.getFilename(), defaultConfig.filename);
});

it("getPhysicalFilename() should return the physicalFilename property", () => {
assert.strictEqual(
context.getPhysicalFilename(),
context.physicalFilename,
);
assert.strictEqual(
context.getPhysicalFilename(),
defaultConfig.physicalFilename,
);
});

it("getSourceCode() should return the sourceCode property", () => {
assert.strictEqual(context.getSourceCode(), context.sourceCode);
assert.strictEqual(
context.getSourceCode(),
defaultConfig.sourceCode,
);
});
});

describe("extend()", () => {
let context;

beforeEach(() => {
context = new FileContext(defaultConfig);
});

it("should create a new object with the original as prototype", () => {
const extension = { extraProperty: "extra" };
const extended = context.extend(extension);

// Verify new properties
assert.strictEqual(extended.extraProperty, "extra");

// Verify inherited properties
assert.strictEqual(extended.cwd, context.cwd);
assert.strictEqual(extended.filename, context.filename);
assert.strictEqual(
extended.physicalFilename,
context.physicalFilename,
);
assert.strictEqual(extended.sourceCode, context.sourceCode);
assert.deepStrictEqual(
extended.parserOptions,
context.parserOptions,
);
assert.strictEqual(extended.parserPath, context.parserPath);
assert.deepStrictEqual(
extended.languageOptions,
context.languageOptions,
);
assert.deepStrictEqual(extended.settings, context.settings);
});

it("should freeze the extended object", () => {
const extension = { extraProperty: "extra" };
const extended = context.extend(extension);

// Verify the extended object is frozen
assert.throws(() => {
extended.cwd = "changed";
}, TypeError);

assert.throws(() => {
extended.extraProperty = "changed";
}, TypeError);
});

it("should throw an error when attempting to override existing properties", () => {
const extension = { cwd: "newCwd" };

assert.throws(() => {
context.extend(extension);
}, TypeError);
});
});
});