diff --git a/common/changes/eslint-plugin-tsdoc/eslint-7-filename-fallback_2026-10-07-18-30.json b/common/changes/eslint-plugin-tsdoc/eslint-7-filename-fallback_2026-10-07-18-30.json new file mode 100644 index 00000000..64f6fcbc --- /dev/null +++ b/common/changes/eslint-plugin-tsdoc/eslint-7-filename-fallback_2026-10-07-18-30.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "eslint-plugin-tsdoc", + "comment": "Fix an \"Unexpected exception ... Received undefined\" warning on ESLint versions that do not define `context.filename` by falling back to `context.getFilename()`.", + "type": "patch" + } + ], + "packageName": "eslint-plugin-tsdoc" +} diff --git a/eslint-plugin/src/index.ts b/eslint-plugin/src/index.ts index ab0c35ae..d936b343 100644 --- a/eslint-plugin/src/index.ts +++ b/eslint-plugin/src/index.ts @@ -62,7 +62,7 @@ const plugin: IPlugin = { } }, create: (context: eslint.Rule.RuleContext) => { - const sourceFilePath: string = context.filename; + const sourceFilePath: string = context.filename ?? context.getFilename(); // If eslint is configured with @typescript-eslint/parser, there is a parser option // to explicitly specify where the tsconfig file is. Use that if available. const tsConfigDir: string | undefined = getRootDirectoryFromContext( diff --git a/eslint-plugin/src/tests/plugin.test.ts b/eslint-plugin/src/tests/plugin.test.ts index 976ac9ce..39382ac2 100644 --- a/eslint-plugin/src/tests/plugin.test.ts +++ b/eslint-plugin/src/tests/plugin.test.ts @@ -2,6 +2,9 @@ // See LICENSE in the project root for license information. import { RuleTester } from 'eslint'; +import type * as eslint from 'eslint'; +import * as os from 'node:os'; +import * as path from 'node:path'; import * as plugin from '../index'; const ruleTester: RuleTester = new RuleTester(); @@ -30,3 +33,92 @@ ruleTester.run('"tsdoc/syntax" rule', plugin.rules.syntax, { } ] }); + +interface ITestReportDescriptor { + messageId?: string; +} + +// A SourceCode stub exposing only the members that the "tsdoc/syntax" rule reads. +function createSourceCode(code: string): eslint.SourceCode { + const commentEndIndex: number = code.indexOf('*/') + 2; + return { + text: code, + getAllComments: () => [{ type: 'Block', range: [0, commentEndIndex] }], + getLocFromIndex: (index: number) => { + const lines: string[] = code.slice(0, index).split('\n'); + return { line: lines.length, column: lines[lines.length - 1].length }; + } + } as unknown as eslint.SourceCode; +} + +function runRule(context: eslint.Rule.RuleContext): void { + const listeners: eslint.Rule.RuleListener = plugin.rules.syntax.create(context); + (listeners.Program as () => void)(); +} + +test('"tsdoc/syntax" rule supports a method-only ESLint 7 style rule context', () => { + const code: string = '/**\n * A great function!\n */\nfunction foobar() {}\n'; + const sourceFileFolder: string = os.tmpdir(); + const sourceFilePath: string = path.join(sourceFileFolder, 'method-only-context.ts'); + const reportedMessageIds: (string | undefined)[] = []; + + // ESLint 7 provides getFilename(), getSourceCode(), and getCwd(), but does not + // define the filename, sourceCode, and cwd properties. + const context: eslint.Rule.RuleContext = { + getFilename: () => sourceFilePath, + getSourceCode: () => createSourceCode(code), + getCwd: () => sourceFileFolder, + parserOptions: {}, + report: (descriptor: ITestReportDescriptor) => { + reportedMessageIds.push(descriptor.messageId); + } + } as unknown as eslint.Rule.RuleContext; + + runRule(context); + + expect(reportedMessageIds).toEqual([]); +}); + +test('"tsdoc/syntax" rule reports TSDoc errors with a method-only ESLint 7 style rule context', () => { + const code: string = '/**\n * This `is wrong\n */\nfunction foobar() {}\n'; + const sourceFileFolder: string = os.tmpdir(); + const sourceFilePath: string = path.join(sourceFileFolder, 'method-only-context.ts'); + const reportedMessageIds: (string | undefined)[] = []; + + const context: eslint.Rule.RuleContext = { + getFilename: () => sourceFilePath, + getSourceCode: () => createSourceCode(code), + getCwd: () => sourceFileFolder, + parserOptions: {}, + report: (descriptor: ITestReportDescriptor) => { + reportedMessageIds.push(descriptor.messageId); + } + } as unknown as eslint.Rule.RuleContext; + + runRule(context); + + expect(reportedMessageIds).toEqual(['tsdoc-code-span-missing-delimiter']); +}); + +test('"tsdoc/syntax" rule supports a property-only rule context', () => { + const code: string = '/**\n * A great function!\n */\nfunction foobar() {}\n'; + const sourceFileFolder: string = os.tmpdir(); + const sourceFilePath: string = path.join(sourceFileFolder, 'property-only-context.ts'); + const reportedMessageIds: (string | undefined)[] = []; + + // Newer ESLint versions expose the filename, sourceCode, and cwd properties and + // may drop the deprecated getFilename(), getSourceCode(), and getCwd() methods. + const context: eslint.Rule.RuleContext = { + filename: sourceFilePath, + sourceCode: createSourceCode(code), + cwd: sourceFileFolder, + parserOptions: {}, + report: (descriptor: ITestReportDescriptor) => { + reportedMessageIds.push(descriptor.messageId); + } + } as unknown as eslint.Rule.RuleContext; + + runRule(context); + + expect(reportedMessageIds).toEqual([]); +});