Skip to content

Commit

Permalink
feat(eslint-plugin): [ban-ts-comment] add descriptionFormat option fo…
Browse files Browse the repository at this point in the history
…r ts-expect-error
  • Loading branch information
Josh-Cena committed May 20, 2022
1 parent 7e7b24c commit 3ec575a
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 13 deletions.
74 changes: 61 additions & 13 deletions packages/eslint-plugin/src/rules/ban-ts-comment.ts
Expand Up @@ -2,18 +2,31 @@ import { AST_TOKEN_TYPES } from '@typescript-eslint/utils';
import * as util from '../util';

interface Options {
'ts-expect-error'?: boolean | 'allow-with-description';
'ts-ignore'?: boolean | 'allow-with-description';
'ts-nocheck'?: boolean | 'allow-with-description';
'ts-check'?: boolean | 'allow-with-description';
'ts-expect-error'?:
| boolean
| 'allow-with-description'
| { descriptionFormat: string };
'ts-ignore'?:
| boolean
| 'allow-with-description'
| { descriptionFormat: string };
'ts-nocheck'?:
| boolean
| 'allow-with-description'
| { descriptionFormat: string };
'ts-check'?:
| boolean
| 'allow-with-description'
| { descriptionFormat: string };
minimumDescriptionLength?: number;
}

export const defaultMinimumDescriptionLength = 3;

type MessageIds =
| 'tsDirectiveComment'
| 'tsDirectiveCommentRequiresDescription';
| 'tsDirectiveCommentRequiresDescription'
| 'tsDirectiveCommentDescriptionNotMatchPattern';

export default util.createRule<[Options], MessageIds>({
name: 'ban-ts-comment',
Expand All @@ -29,6 +42,8 @@ export default util.createRule<[Options], MessageIds>({
'Do not use "@ts-{{directive}}" because it alters compilation errors.',
tsDirectiveCommentRequiresDescription:
'Include a description after the "@ts-{{directive}}" directive to explain why the @ts-{{directive}} is necessary. The description must be {{minimumDescriptionLength}} characters or longer.',
tsDirectiveCommentDescriptionNotMatchPattern:
'The description for "@ts-{{directive}}" directive must match the {{format}} format.',
},
schema: [
{
Expand All @@ -43,6 +58,12 @@ export default util.createRule<[Options], MessageIds>({
{
enum: ['allow-with-description'],
},
{
type: 'object',
properties: {
descriptionFormat: { type: 'string' },
},
},
],
},
'ts-ignore': {
Expand Down Expand Up @@ -99,25 +120,42 @@ export default util.createRule<[Options], MessageIds>({
create(context, [options]) {
/*
The regex used are taken from the ones used in the official TypeScript repo -
https://github.com/microsoft/TypeScript/blob/main/src/compiler/scanner.ts#L281-L289
https://github.com/microsoft/TypeScript/blob/408c760fae66080104bc85c449282c2d207dfe8e/src/compiler/scanner.ts#L288-L296
*/
const commentDirectiveRegExSingleLine =
/^\/*\s*@ts-(expect-error|ignore|check|nocheck)(.*)/;
/^\/*\s*@ts-(?<directive>expect-error|ignore|check|nocheck)(?<description>.*)/;
const commentDirectiveRegExMultiLine =
/^\s*(?:\/|\*)*\s*@ts-(expect-error|ignore|check|nocheck)(.*)/;
/^\s*(?:\/|\*)*\s*@ts-(?<directive>expect-error|ignore|check|nocheck)(?<description>.*)/;
const sourceCode = context.getSourceCode();

const descriptionFormats = new Map<string, RegExp>();
for (const directive of [
'ts-expect-error',
'ts-ignore',
'ts-nocheck',
'ts-check',
] as const) {
const option = options[directive];
if (typeof option === 'object' && option.descriptionFormat) {
descriptionFormats.set(directive, new RegExp(option.descriptionFormat));
}
}

return {
Program(): void {
const comments = sourceCode.getAllComments();

comments.forEach(comment => {
let regExp = commentDirectiveRegExSingleLine;
const regExp =
comment.type === AST_TOKEN_TYPES.Line
? commentDirectiveRegExSingleLine
: commentDirectiveRegExMultiLine;

if (comment.type !== AST_TOKEN_TYPES.Line) {
regExp = commentDirectiveRegExMultiLine;
const match = regExp.exec(comment.value);
if (!match) {
return;
}
const [, directive, description] = regExp.exec(comment.value) ?? [];
const { directive, description } = match.groups!;

const fullDirective = `ts-${directive}` as keyof Options;

Expand All @@ -130,16 +168,26 @@ export default util.createRule<[Options], MessageIds>({
});
}

if (option === 'allow-with-description') {
if (
option === 'allow-with-description' ||
(typeof option === 'object' && option.descriptionFormat)
) {
const {
minimumDescriptionLength = defaultMinimumDescriptionLength,
} = options;
const format = descriptionFormats.get(fullDirective);
if (description.trim().length < minimumDescriptionLength) {
context.report({
data: { directive, minimumDescriptionLength },
node: comment,
messageId: 'tsDirectiveCommentRequiresDescription',
});
} else if (format && !format.test(description.trim())) {
context.report({
data: { directive, format: format.source },
node: comment,
messageId: 'tsDirectiveCommentDescriptionNotMatchPattern',
});
}
}
});
Expand Down
48 changes: 48 additions & 0 deletions packages/eslint-plugin/tests/rules/ban-ts-comment.test.ts
Expand Up @@ -34,6 +34,17 @@ ruleTester.run('ts-expect-error', rule, {
},
],
},
{
code: '// @ts-expect-error: TS1234 because xyz',
options: [
{
'ts-expect-error': {
descriptionFormat: ': TS\\d+ because .+',
},
minimumDescriptionLength: 10,
},
],
},
],
invalid: [
{
Expand Down Expand Up @@ -162,6 +173,43 @@ if (false) {
},
],
},
{
code: '// @ts-expect-error: TS1234 because xyz',
options: [
{
'ts-expect-error': {
descriptionFormat: ': TS\\d+ because .+',
},
minimumDescriptionLength: 25,
},
],
errors: [
{
data: { directive: 'expect-error', minimumDescriptionLength: 25 },
messageId: 'tsDirectiveCommentRequiresDescription',
line: 1,
column: 1,
},
],
},
{
code: '// @ts-expect-error: TS1234',
options: [
{
'ts-expect-error': {
descriptionFormat: ': TS\\d+ because .+',
},
},
],
errors: [
{
data: { directive: 'expect-error', format: ': TS\\d+ because .+' },
messageId: 'tsDirectiveCommentDescriptionNotMatchPattern',
line: 1,
column: 1,
},
],
},
],
});

Expand Down

0 comments on commit 3ec575a

Please sign in to comment.