From e22e20fb569fd954533b86f8bb60b18a3e6437f9 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 27 Dec 2023 15:07:56 -0500 Subject: [PATCH 1/3] feat(utils): throw error on typed rule with invalid parser --- .../src/eslint-utils/getParserServices.ts | 13 ++- .../parserPathSeemsToBeTSESLint.ts | 3 + .../eslint-utils/getParserServices.test.ts | 102 ++++++++++++++++++ .../parserPathSeemsToBeTSESLint.test.ts | 24 +++++ 4 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 packages/utils/src/eslint-utils/parserPathSeemsToBeTSESLint.ts create mode 100644 packages/utils/tests/eslint-utils/getParserServices.test.ts create mode 100644 packages/utils/tests/eslint-utils/parserPathSeemsToBeTSESLint.test.ts diff --git a/packages/utils/src/eslint-utils/getParserServices.ts b/packages/utils/src/eslint-utils/getParserServices.ts index 485860036c63..555a331eeff0 100644 --- a/packages/utils/src/eslint-utils/getParserServices.ts +++ b/packages/utils/src/eslint-utils/getParserServices.ts @@ -3,8 +3,9 @@ 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.'; /* eslint-disable @typescript-eslint/unified-signatures */ @@ -56,6 +57,12 @@ function getParserServices( context: Readonly>, allowWithoutFullTypeInformation = false, ): ParserServices { + if (!parserPathSeemsToBeTSESLint(context.parserPath)) { + throw new Error( + `You have used a rule which requires @typescript-eslint/parser to generate type information. Unknown parser provided: '${context.parserPath}'.`, + ); + } + // This check is unnecessary if the user is using the latest version of our parser. // // However the world isn't perfect: @@ -72,7 +79,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); + throw new Error(ERROR_MESSAGE_REQUIRES_PARSER_SERVICES); } // if a rule requires full type information, then hard fail if it doesn't exist @@ -82,7 +89,7 @@ function getParserServices( context.parserServices.program == null && !allowWithoutFullTypeInformation ) { - throw new Error(ERROR_MESSAGE); + throw new Error(ERROR_MESSAGE_REQUIRES_PARSER_SERVICES); } // eslint-disable-next-line deprecation/deprecation -- TODO - support for ESLint v9 with backwards-compatible support for ESLint v8 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..7c717c07818e --- /dev/null +++ b/packages/utils/tests/eslint-utils/getParserServices.test.ts @@ -0,0 +1,102 @@ +/* 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 an error when given an unknown parser', () => { + const context = createMockRuleContext({ + parserPath: 'unknown', + }); + + expect(() => ESLintUtils.getParserServices(context)).toThrow( + new Error( + `You have used a rule which requires @typescript-eslint/parser to generate type information. Unknown parser provided: '${context.parserPath}'.`, + ), + ); + }); + + it('throws an error when parserOptions.esTreeNodeToTSNodeMap is missing', () => { + 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 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); + }); +}); From a75caa10e7045824ecd03f8d2234eb6fcb71fbca Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 28 Dec 2023 15:28:15 -0500 Subject: [PATCH 2/3] Switch to more informative error message --- .../src/eslint-utils/getParserServices.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/utils/src/eslint-utils/getParserServices.ts b/packages/utils/src/eslint-utils/getParserServices.ts index 555a331eeff0..30f16a8613d7 100644 --- a/packages/utils/src/eslint-utils/getParserServices.ts +++ b/packages/utils/src/eslint-utils/getParserServices.ts @@ -8,6 +8,9 @@ import { parserPathSeemsToBeTSESLint } from './parserPathSeemsToBeTSESLint'; 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. @@ -57,12 +60,6 @@ function getParserServices( context: Readonly>, allowWithoutFullTypeInformation = false, ): ParserServices { - if (!parserPathSeemsToBeTSESLint(context.parserPath)) { - throw new Error( - `You have used a rule which requires @typescript-eslint/parser to generate type information. Unknown parser provided: '${context.parserPath}'.`, - ); - } - // This check is unnecessary if the user is using the latest version of our parser. // // However the world isn't perfect: @@ -79,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_REQUIRES_PARSER_SERVICES); + throwError(context.parserPath); } // if a rule requires full type information, then hard fail if it doesn't exist @@ -89,7 +86,7 @@ function getParserServices( context.parserServices.program == null && !allowWithoutFullTypeInformation ) { - throw new Error(ERROR_MESSAGE_REQUIRES_PARSER_SERVICES); + throwError(context.parserPath); } // eslint-disable-next-line deprecation/deprecation -- TODO - support for ESLint v9 with backwards-compatible support for ESLint v8 @@ -97,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 }; From 7b30a3a5159a17f82d5ecf30aa336f1d5142d49e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 28 Dec 2023 15:32:35 -0500 Subject: [PATCH 3/3] fix tests --- .../tests/eslint-utils/getParserServices.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/utils/tests/eslint-utils/getParserServices.test.ts b/packages/utils/tests/eslint-utils/getParserServices.test.ts index 7c717c07818e..4fd4ed3cbff2 100644 --- a/packages/utils/tests/eslint-utils/getParserServices.test.ts +++ b/packages/utils/tests/eslint-utils/getParserServices.test.ts @@ -24,20 +24,24 @@ const createMockRuleContext = ( }) as unknown as UnknownRuleContext; describe('getParserServices', () => { - it('throws an error when given an unknown parser', () => { + it('throws a standard error when parserOptions.esTreeNodeToTSNodeMap is missing and the parser is known', () => { const context = createMockRuleContext({ - parserPath: 'unknown', + parserServices: { + ...defaults.parserServices, + esTreeNodeToTSNodeMap: undefined as any, + }, }); expect(() => ESLintUtils.getParserServices(context)).toThrow( new Error( - `You have used a rule which requires @typescript-eslint/parser to generate type information. Unknown parser provided: '${context.parserPath}'.`, + '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 parserOptions.esTreeNodeToTSNodeMap is missing', () => { + 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, @@ -46,7 +50,8 @@ describe('getParserServices', () => { 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.', + '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.', ), ); });