From 52018f21c19b3e461cae32843cddd17ed42f19cd Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Sat, 29 Apr 2023 07:17:58 +0530 Subject: [PATCH] feat: add `filename` property to the rule context (#17108) Co-authored-by: Milos Djermanovic --- docs/src/extend/custom-rules.md | 3 +- lib/linter/linter.js | 3 +- .../testers/rule-tester/no-test-filename | 2 +- tests/lib/linter/linter.js | 89 ++++++++++++++++--- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 8484a59b140..b416318a319 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -124,6 +124,7 @@ As the name implies, the `context` object contains information that is relevant The `context` object has the following properties: * `id`: (`string`) The rule ID. +* `filename`: (`string`) The filename associated with the source. * `options`: (`array`) An array of the [configured options](../use/configure/rules) for this rule. This array does not include the rule severity (see the [dedicated section](#accessing-options-passed-to-a-rule)). * `settings`: (`object`) The [shared settings](../use/configure/configuration-files#adding-shared-settings) from the configuration. * `parserPath`: (`string`) The name of the `parser` from the configuration. @@ -144,7 +145,7 @@ Additionally, the `context` object has the following methods: * If the node is an `ImportDeclaration`, variables for all of its specifiers are returned. * If the node is an `ImportSpecifier`, `ImportDefaultSpecifier`, or `ImportNamespaceSpecifier`, the declared variable is returned. * Otherwise, if the node does not declare any variables, an empty array is returned. -* `getFilename()`: Returns the filename associated with the source. +* `getFilename()`: (**Deprecated:** Use `context.filename` instead.) Returns the filename associated with the source. * `getPhysicalFilename()`: When linting a file, it returns the full path of the file on disk without any code block information. When linting text, it returns the value passed to `—stdin-filename` or `` if not specified. * `getScope()`: (**Deprecated:** Use `SourceCode#getScope(node)` instead.) Returns the [scope](./scope-manager-interface#scope-interface) of the currently-traversed node. This information can be used to track references to variables. * `getSourceCode()`: Returns a `SourceCode` object that you can use to work with the source that was passed to ESLint (see [Accessing the Source Code](#accessing-the-source-code)). diff --git a/lib/linter/linter.js b/lib/linter/linter.js index b49eaa134c4..9573e7aefc2 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -592,7 +592,7 @@ function findEslintEnv(text) { * Convert "/path/to/" to "". * `CLIEngine#executeOnText()` method gives "/path/to/" if the filename * was omitted because `configArray.extractConfig()` requires an absolute path. - * But the linter should pass `` to `RuleContext#getFilename()` in that + * But the linter should pass `` to `RuleContext#filename` in that * case. * Also, code blocks can have their virtual filename. If the parent filename was * ``, the virtual filename is `/0_foo.js` or something like (i.e., @@ -952,6 +952,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO getDeclaredVariables: node => sourceCode.getDeclaredVariables(node), getCwd: () => cwd, getFilename: () => filename, + filename, getPhysicalFilename: () => physicalFilename || filename, getScope: () => sourceCode.getScope(currentNode), getSourceCode: () => sourceCode, diff --git a/tests/fixtures/testers/rule-tester/no-test-filename b/tests/fixtures/testers/rule-tester/no-test-filename index b3cde257352..78d8f34d80b 100644 --- a/tests/fixtures/testers/rule-tester/no-test-filename +++ b/tests/fixtures/testers/rule-tester/no-test-filename @@ -17,7 +17,7 @@ module.exports = { create(context) { return { "Program": function(node) { - if (context.getFilename() === '') { + if (context.filename === '') { context.report(node, "Filename test was not defined."); } } diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 0ece277dc9a..efb265d69f3 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1826,7 +1826,8 @@ describe("Linter", () => { linter.defineRule(code, { create: context => ({ Literal(node) { - context.report(node, context.getFilename()); + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); } }) }); @@ -1866,7 +1867,8 @@ describe("Linter", () => { linter.defineRule(code, { create: context => ({ Literal(node) { - context.report(node, context.getFilename()); + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); } }) }); @@ -4817,7 +4819,7 @@ var a = "test2"; describe("filenames", () => { it("should allow filename to be passed on options object", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), "foo.js"); + assert.strictEqual(context.filename, "foo.js"); return {}; }); @@ -4828,7 +4830,7 @@ var a = "test2"; it("should allow filename to be passed as third argument", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), "bar.js"); + assert.strictEqual(context.filename, "bar.js"); return {}; }); @@ -4839,7 +4841,7 @@ var a = "test2"; it("should default filename to when options object doesn't have filename", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), ""); + assert.strictEqual(context.filename, ""); return {}; }); @@ -4850,7 +4852,7 @@ var a = "test2"; it("should default filename to when only two arguments are passed", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), ""); + assert.strictEqual(context.filename, ""); return {}; }); @@ -6751,7 +6753,7 @@ var a = "test2"; linter.defineRule("report-original-text", { create: context => ({ Program(ast) { - receivedFilenames.push(context.getFilename()); + receivedFilenames.push(context.filename); receivedPhysicalFilenames.push(context.getPhysicalFilename()); context.report({ node: ast, message: context.getSourceCode().text }); } @@ -9109,6 +9111,69 @@ describe("Linter with FlatConfigArray", () => { }); }); + describe("context.filename", () => { + const ruleId = "filename-rule"; + + it("has access to the filename", () => { + + const config = { + plugins: { + test: { + rules: { + [ruleId]: { + create: context => ({ + Literal(node) { + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); + } + }) + } + } + } + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; + + const messages = linter.verify("0", config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, filename); + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("defaults filename to ''", () => { + + const config = { + plugins: { + test: { + rules: { + [ruleId]: { + create: context => ({ + Literal(node) { + assert.strictEqual(context.getFilename(), context.filename); + context.report(node, context.filename); + } + }) + } + } + } + }, + rules: { + [`test/${ruleId}`]: 1 + } + }; + + + const messages = linter.verify("0", config); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages[0].message, ""); + assert.strictEqual(suppressedMessages.length, 0); + }); + }); + describe("context.getPhysicalFilename()", () => { const ruleId = "filename-rule"; @@ -11063,7 +11128,7 @@ describe("Linter with FlatConfigArray", () => { describe("filename", () => { it("should allow filename to be passed on options object", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), "foo.js"); + assert.strictEqual(context.filename, "foo.js"); return {}; }); @@ -11086,7 +11151,7 @@ describe("Linter with FlatConfigArray", () => { it("should allow filename to be passed as third argument", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), "bar.js"); + assert.strictEqual(context.filename, "bar.js"); return {}; }); @@ -11109,7 +11174,7 @@ describe("Linter with FlatConfigArray", () => { it("should default filename to when options object doesn't have filename", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), ""); + assert.strictEqual(context.filename, ""); return {}; }); @@ -11132,7 +11197,7 @@ describe("Linter with FlatConfigArray", () => { it("should default filename to when only two arguments are passed", () => { const filenameChecker = sinon.spy(context => { - assert.strictEqual(context.getFilename(), ""); + assert.strictEqual(context.filename, ""); return {}; }); @@ -15369,7 +15434,7 @@ var a = "test2"; create(context) { return { Program(ast) { - receivedFilenames.push(context.getFilename()); + receivedFilenames.push(context.filename); receivedPhysicalFilenames.push(context.getPhysicalFilename()); context.report({ node: ast, message: context.getSourceCode().text }); }