Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [ban-ts-comment] add descriptionFormat option (#…
…5026)

* feat(eslint-plugin): [ban-ts-comment] add descriptionFormat option for ts-expect-error

* test: test

* Update packages/eslint-plugin/src/rules/ban-ts-comment.ts

Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>

* refactor: deduplicate

Co-authored-by: Josh Goldberg <me@joshuakgoldberg.com>
  • Loading branch information
Josh-Cena and JoshuaKGoldberg committed May 25, 2022
1 parent 37f258d commit 1fb31a4
Show file tree
Hide file tree
Showing 2 changed files with 337 additions and 60 deletions.
127 changes: 70 additions & 57 deletions packages/eslint-plugin/src/rules/ban-ts-comment.ts
@@ -1,19 +1,43 @@
import { AST_TOKEN_TYPES } from '@typescript-eslint/utils';
import * as util from '../util';

type DirectiveConfig =
| boolean
| 'allow-with-description'
| { descriptionFormat: string };

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'?: DirectiveConfig;
'ts-ignore'?: DirectiveConfig;
'ts-nocheck'?: DirectiveConfig;
'ts-check'?: DirectiveConfig;
minimumDescriptionLength?: number;
}

const directiveConfigSchema = {
oneOf: [
{
type: 'boolean',
default: true,
},
{
enum: ['allow-with-description'],
},
{
type: 'object',
properties: {
descriptionFormat: { type: 'string' },
},
},
],
};

export const defaultMinimumDescriptionLength = 3;

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

export default util.createRule<[Options], MessageIds>({
name: 'ban-ts-comment',
Expand All @@ -29,55 +53,17 @@ 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 the "@ts-{{directive}}" directive must match the {{format}} format.',
},
schema: [
{
type: 'object',
properties: {
'ts-expect-error': {
oneOf: [
{
type: 'boolean',
default: true,
},
{
enum: ['allow-with-description'],
},
],
},
'ts-ignore': {
oneOf: [
{
type: 'boolean',
default: true,
},
{
enum: ['allow-with-description'],
},
],
},
'ts-nocheck': {
oneOf: [
{
type: 'boolean',
default: true,
},
{
enum: ['allow-with-description'],
},
],
},
'ts-check': {
oneOf: [
{
type: 'boolean',
default: true,
},
{
enum: ['allow-with-description'],
},
],
},
'ts-expect-error': directiveConfigSchema,
'ts-ignore': directiveConfigSchema,
'ts-nocheck': directiveConfigSchema,
'ts-check': directiveConfigSchema,
minimumDescriptionLength: {
type: 'number',
default: defaultMinimumDescriptionLength,
Expand All @@ -99,25 +85,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 +133,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)) {
context.report({
data: { directive, format: format.source },
node: comment,
messageId: 'tsDirectiveCommentDescriptionNotMatchPattern',
});
}
}
});
Expand Down

0 comments on commit 1fb31a4

Please sign in to comment.