Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(utils): improve error message on typed rule with invalid parser #8146

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
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);
});
});