Skip to content

Commit

Permalink
fix(utils): improve error message on typed rule with invalid parser (#…
Browse files Browse the repository at this point in the history
…8146)

* feat(utils): throw error on typed rule with invalid parser

* Switch to more informative error message

* fix tests
  • Loading branch information
JoshuaKGoldberg committed Jan 7, 2024
1 parent 5a33c2b commit 687f73c
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 3 deletions.
21 changes: 18 additions & 3 deletions packages/utils/src/eslint-utils/getParserServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -82,12 +86,23 @@ 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
return context.parserServices;
}
/* 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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function parserPathSeemsToBeTSESLint(parserPath: string): boolean {
return /(?:typescript-eslint|\.\.)[\w/\\]*parser/.test(parserPath);
}
107 changes: 107 additions & 0 deletions packages/utils/tests/eslint-utils/getParserServices.test.ts
Original file line number Diff line number Diff line change
@@ -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<TSESLint.RuleContext<string, unknown[]>>;

const defaults = {
parserPath: '@typescript-eslint/parser/dist/index.js',
parserServices: {
esTreeNodeToTSNodeMap: new Map<TSESTree.Node, ts.Node>(),
program: {},
tsNodeToESTreeNodeMap: new Map<ts.Node, TSESTree.Node>(),
} as unknown as ParserServices,
};

const createMockRuleContext = (
overrides: Partial<UnknownRuleContext> = {},
): 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);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 687f73c

Please sign in to comment.