diff --git a/packages/utils/src/eslint-utils/getParserServices.ts b/packages/utils/src/eslint-utils/getParserServices.ts index 485860036c63..30f16a8613d7 100644 --- a/packages/utils/src/eslint-utils/getParserServices.ts +++ b/packages/utils/src/eslint-utils/getParserServices.ts @@ -3,10 +3,14 @@ import type { ParserServices, ParserServicesWithTypeInformation, } from '../ts-estree'; +import { parserPathSeemsToBeTSESLint } from './parserPathSeemsToBeTSESLint'; -const ERROR_MESSAGE = +const ERROR_MESSAGE_REQUIRES_PARSER_SERVICES = 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.'; +const ERROR_MESSAGE_UNKNOWN_PARSER = + 'Note: detected a parser other than @typescript-eslint/parser. Make sure the parser is configured to forward "parserOptions.project" to @typescript-eslint/parser.'; + /* eslint-disable @typescript-eslint/unified-signatures */ /** * Try to retrieve type-aware parser service from context. @@ -72,7 +76,7 @@ function getParserServices( // eslint-disable-next-line deprecation/deprecation, @typescript-eslint/no-unnecessary-condition -- TODO - support for ESLint v9 with backwards-compatible support for ESLint v8 context.parserServices.tsNodeToESTreeNodeMap == null ) { - throw new Error(ERROR_MESSAGE); + throwError(context.parserPath); } // if a rule requires full type information, then hard fail if it doesn't exist @@ -82,7 +86,7 @@ function getParserServices( context.parserServices.program == null && !allowWithoutFullTypeInformation ) { - throw new Error(ERROR_MESSAGE); + throwError(context.parserPath); } // eslint-disable-next-line deprecation/deprecation -- TODO - support for ESLint v9 with backwards-compatible support for ESLint v8 @@ -90,4 +94,15 @@ function getParserServices( } /* eslint-enable @typescript-eslint/unified-signatures */ +function throwError(parserPath: string): never { + throw new Error( + parserPathSeemsToBeTSESLint(parserPath) + ? ERROR_MESSAGE_REQUIRES_PARSER_SERVICES + : [ + ERROR_MESSAGE_REQUIRES_PARSER_SERVICES, + ERROR_MESSAGE_UNKNOWN_PARSER, + ].join('\n'), + ); +} + export { getParserServices }; diff --git a/packages/utils/src/eslint-utils/parserPathSeemsToBeTSESLint.ts b/packages/utils/src/eslint-utils/parserPathSeemsToBeTSESLint.ts new file mode 100644 index 000000000000..ab68b4367ab7 --- /dev/null +++ b/packages/utils/src/eslint-utils/parserPathSeemsToBeTSESLint.ts @@ -0,0 +1,3 @@ +export function parserPathSeemsToBeTSESLint(parserPath: string): boolean { + return /(?:typescript-eslint|\.\.)[\w/\\]*parser/.test(parserPath); +} diff --git a/packages/utils/tests/eslint-utils/getParserServices.test.ts b/packages/utils/tests/eslint-utils/getParserServices.test.ts new file mode 100644 index 000000000000..4fd4ed3cbff2 --- /dev/null +++ b/packages/utils/tests/eslint-utils/getParserServices.test.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, deprecation/deprecation -- wild and wacky testing */ +import type * as ts from 'typescript'; + +import type { ParserServices, TSESLint, TSESTree } from '../../src'; +import { ESLintUtils } from '../../src'; + +type UnknownRuleContext = Readonly>; + +const defaults = { + parserPath: '@typescript-eslint/parser/dist/index.js', + parserServices: { + esTreeNodeToTSNodeMap: new Map(), + program: {}, + tsNodeToESTreeNodeMap: new Map(), + } as unknown as ParserServices, +}; + +const createMockRuleContext = ( + overrides: Partial = {}, +): UnknownRuleContext => + ({ + ...defaults, + ...overrides, + }) as unknown as UnknownRuleContext; + +describe('getParserServices', () => { + it('throws a standard error when parserOptions.esTreeNodeToTSNodeMap is missing and the parser is known', () => { + const context = createMockRuleContext({ + parserServices: { + ...defaults.parserServices, + esTreeNodeToTSNodeMap: undefined as any, + }, + }); + + expect(() => ESLintUtils.getParserServices(context)).toThrow( + new Error( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', + ), + ); + }); + + it('throws an augment error when parserOptions.esTreeNodeToTSNodeMap is missing and the parser is unknown', () => { + const context = createMockRuleContext({ + parserPath: '@babel/parser.js', + parserServices: { + ...defaults.parserServices, + esTreeNodeToTSNodeMap: undefined as any, + }, + }); + + expect(() => ESLintUtils.getParserServices(context)).toThrow( + new Error( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.\n' + + 'Note: detected a parser other than @typescript-eslint/parser. Make sure the parser is configured to forward "parserOptions.project" to @typescript-eslint/parser.', + ), + ); + }); + + it('throws an error when parserOptions.tsNodeToESTreeNodeMap is missing', () => { + const context = createMockRuleContext({ + parserServices: { + ...defaults.parserServices, + tsNodeToESTreeNodeMap: undefined as any, + }, + }); + + expect(() => ESLintUtils.getParserServices(context)).toThrow( + new Error( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', + ), + ); + }); + + it('throws an error when parserServices.program is missing and allowWithoutFullTypeInformation is false', () => { + const context = createMockRuleContext({ + parserServices: { + ...defaults.parserServices, + program: undefined as any, + }, + }); + + expect(() => ESLintUtils.getParserServices(context)).toThrow( + new Error( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', + ), + ); + }); + + it('returns when parserServices.program is missing and allowWithoutFullTypeInformation is true', () => { + const context = createMockRuleContext({ + parserServices: { + ...defaults.parserServices, + program: undefined as any, + }, + }); + + expect(ESLintUtils.getParserServices(context, true)).toBe( + context.parserServices, + ); + }); + + it('returns when parserServices is filled out', () => { + const context = createMockRuleContext(); + + expect(ESLintUtils.getParserServices(context)).toBe(context.parserServices); + }); +}); diff --git a/packages/utils/tests/eslint-utils/parserPathSeemsToBeTSESLint.test.ts b/packages/utils/tests/eslint-utils/parserPathSeemsToBeTSESLint.test.ts new file mode 100644 index 000000000000..0c11c1664005 --- /dev/null +++ b/packages/utils/tests/eslint-utils/parserPathSeemsToBeTSESLint.test.ts @@ -0,0 +1,24 @@ +import { parserPathSeemsToBeTSESLint } from '../../src/eslint-utils/parserPathSeemsToBeTSESLint'; + +describe('parserPathSeemsToBeTSESLint', () => { + test.each([ + ['local.js', false], + ['../other.js', false], + ['@babel/eslint-parser/lib/index.cjs', false], + ['@babel\\eslint-parser\\lib\\index.cjs', false], + ['node_modules/@babel/eslint-parser/lib/index.cjs', false], + ['../parser/dist/index.js', true], + ['../@typescript-eslint/parser/dist/index.js', true], + ['@typescript-eslint/parser/dist/index.js', true], + ['@typescript-eslint\\parser\\dist\\index.js', true], + ['node_modules/@typescript-eslint/parser/dist/index.js', true], + ['/path/to/typescript-eslint/parser/dist/index.js', true], + ['/path/to/typescript-eslint/parser/index.js', true], + ['/path/to/typescript-eslint/packages/parser/dist/index.js', true], + ['/path/to/typescript-eslint/packages/parser/index.js', true], + ['/path/to/@typescript-eslint/packages/parser/dist/index.js', true], + ['/path/to/@typescript-eslint/packages/parser/index.js', true], + ])('%s', (parserPath, expected) => { + expect(parserPathSeemsToBeTSESLint(parserPath)).toBe(expected); + }); +});