Skip to content

Commit f11183c

Browse files
authored
chore(eslint-plugin-internal): [plugin-test-formatting] support random object literal tests (#5895)
* chore(eslint-plugin-internal): [plugin-test-formatting] support random object literal tests * update test column
1 parent c874e50 commit f11183c

File tree

7 files changed

+3225
-2945
lines changed

7 files changed

+3225
-2945
lines changed

Diff for: packages/eslint-plugin-internal/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@types/prettier": "*",
1717
"@typescript-eslint/scope-manager": "5.42.0",
18+
"@typescript-eslint/type-utils": "5.42.0",
1819
"@typescript-eslint/utils": "5.42.0",
1920
"prettier": "*"
2021
}

Diff for: packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts

+94-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { getContextualType } from '@typescript-eslint/type-utils';
12
import type { TSESTree } from '@typescript-eslint/utils';
2-
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3+
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
34
import { format, resolveConfig } from 'prettier';
45

56
import { createRule } from '../util';
@@ -108,6 +109,7 @@ export default createRule<Options, MessageIds>({
108109
docs: {
109110
description: `Enforces that eslint-plugin test snippets are correctly formatted`,
110111
recommended: 'error',
112+
requiresTypeChecking: true,
111113
},
112114
fixable: 'code',
113115
schema: [
@@ -146,6 +148,11 @@ export default createRule<Options, MessageIds>({
146148
],
147149
create(context, [{ formatWithPrettier }]) {
148150
const sourceCode = context.getSourceCode();
151+
const { program, esTreeNodeToTSNodeMap } =
152+
ESLintUtils.getParserServices(context);
153+
const checker = program.getTypeChecker();
154+
155+
const checkedObjects = new Set<TSESTree.ObjectExpression>();
149156

150157
function prettierFormat(
151158
code: string,
@@ -448,6 +455,12 @@ export default createRule<Options, MessageIds>({
448455
test: TSESTree.ObjectExpression,
449456
isErrorTest = true,
450457
): void {
458+
if (checkedObjects.has(test)) {
459+
return;
460+
}
461+
462+
checkedObjects.add(test);
463+
451464
for (const prop of test.properties) {
452465
if (
453466
prop.type !== AST_NODE_TYPES.Property ||
@@ -478,33 +491,99 @@ export default createRule<Options, MessageIds>({
478491
}
479492
}
480493

481-
const invalidTestsSelectorPath = [
482-
AST_NODE_TYPES.CallExpression,
483-
AST_NODE_TYPES.ObjectExpression,
484-
'Property[key.name = "invalid"]',
485-
AST_NODE_TYPES.ArrayExpression,
486-
AST_NODE_TYPES.ObjectExpression,
487-
];
488-
489494
return {
490495
// valid
491496
'CallExpression > ObjectExpression > Property[key.name = "valid"] > ArrayExpression':
492497
checkValidTest,
493498
// invalid - errors
494-
[invalidTestsSelectorPath.join(' > ')]: checkInvalidTest,
495-
// invalid - suggestions
496499
[[
497-
...invalidTestsSelectorPath,
498-
'Property[key.name = "errors"]',
499-
AST_NODE_TYPES.ArrayExpression,
500+
AST_NODE_TYPES.CallExpression,
500501
AST_NODE_TYPES.ObjectExpression,
501-
'Property[key.name = "suggestions"]',
502+
'Property[key.name = "invalid"]',
502503
AST_NODE_TYPES.ArrayExpression,
503504
AST_NODE_TYPES.ObjectExpression,
504505
].join(' > ')]: checkInvalidTest,
505506
// special case for our batchedSingleLineTests utility
506507
'CallExpression[callee.name = "batchedSingleLineTests"] > ObjectExpression':
507508
checkInvalidTest,
509+
510+
/**
511+
* generic, type-aware handling for any old object
512+
* this is a fallback to handle random variables people declare or object
513+
* literals that are passed via array maps, etc
514+
*/
515+
ObjectExpression(node): void {
516+
if (checkedObjects.has(node)) {
517+
return;
518+
}
519+
520+
const type = getContextualType(
521+
checker,
522+
esTreeNodeToTSNodeMap.get(node),
523+
);
524+
if (!type) {
525+
return;
526+
}
527+
528+
const typeString = checker.typeToString(type);
529+
if (/^RunTests\b/.test(typeString)) {
530+
checkedObjects.add(node);
531+
532+
for (const prop of node.properties) {
533+
if (
534+
prop.type === AST_NODE_TYPES.SpreadElement ||
535+
prop.computed ||
536+
prop.key.type !== AST_NODE_TYPES.Identifier ||
537+
prop.value.type !== AST_NODE_TYPES.ArrayExpression
538+
) {
539+
continue;
540+
}
541+
542+
switch (prop.key.name) {
543+
case 'valid':
544+
checkValidTest(prop.value);
545+
break;
546+
547+
case 'invalid':
548+
for (const element of prop.value.elements) {
549+
if (element.type === AST_NODE_TYPES.ObjectExpression) {
550+
checkInvalidTest(element);
551+
}
552+
}
553+
break;
554+
}
555+
}
556+
return;
557+
}
558+
559+
if (/^ValidTestCase\b/.test(typeString)) {
560+
checkInvalidTest(node);
561+
return;
562+
}
563+
564+
if (/^InvalidTestCase\b/.test(typeString)) {
565+
checkInvalidTest(node);
566+
for (const testProp of node.properties) {
567+
if (
568+
testProp.type === AST_NODE_TYPES.SpreadElement ||
569+
testProp.computed ||
570+
testProp.key.type !== AST_NODE_TYPES.Identifier ||
571+
testProp.key.name !== 'errors' ||
572+
testProp.value.type !== AST_NODE_TYPES.ArrayExpression
573+
) {
574+
continue;
575+
}
576+
577+
for (const errorElement of testProp.value.elements) {
578+
if (errorElement.type !== AST_NODE_TYPES.ObjectExpression) {
579+
continue;
580+
}
581+
582+
checkInvalidTest(errorElement);
583+
}
584+
}
585+
}
586+
},
508587
};
509588
},
510589
});

Diff for: packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts

+210-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import rule from '../../src/rules/plugin-test-formatting';
2-
import { RuleTester } from '../RuleTester';
2+
import { getFixturesRootDir, RuleTester } from '../RuleTester';
33

44
const ruleTester = new RuleTester({
55
parser: '@typescript-eslint/parser',
66
parserOptions: {
7+
project: './tsconfig.json',
8+
tsconfigRootDir: getFixturesRootDir(),
79
sourceType: 'module',
810
},
911
});
@@ -132,6 +134,44 @@ ${CODE_INDENT}const a = 1;
132134
133135
${CODE_INDENT}const b = 1;
134136
${PARENT_INDENT}\``,
137+
138+
// random, unannotated variables aren't checked
139+
`
140+
const test1 = {
141+
code: 'const badlyFormatted = "code"',
142+
};
143+
const test2 = {
144+
valid: [
145+
'const badlyFormatted = "code"',
146+
{
147+
code: 'const badlyFormatted = "code"',
148+
},
149+
],
150+
invalid: [
151+
{
152+
code: 'const badlyFormatted = "code"',
153+
errors: [],
154+
},
155+
],
156+
};
157+
`,
158+
159+
// TODO - figure out how to handle this pattern
160+
`
161+
import { TSESLint } from '@typescript-eslint/utils';
162+
163+
const test = [
164+
{
165+
code: 'const badlyFormatted = "code1"',
166+
},
167+
{
168+
code: 'const badlyFormatted = "code2"',
169+
},
170+
].map<TSESLint.InvalidTestCase<[]>>(test => ({
171+
code: test.code,
172+
errors: [],
173+
}));
174+
`,
135175
],
136176
invalid: [
137177
// Literal
@@ -506,5 +546,174 @@ foo
506546
},
507547
],
508548
},
549+
550+
// annotated variables are checked
551+
{
552+
code: `
553+
const test: RunTests = {
554+
valid: [
555+
'const badlyFormatted = "code"',
556+
{
557+
code: 'const badlyFormatted = "code"',
558+
},
559+
],
560+
invalid: [
561+
{
562+
code: 'const badlyFormatted = "code"',
563+
errors: [],
564+
},
565+
],
566+
};
567+
`,
568+
output: `
569+
const test: RunTests = {
570+
valid: [
571+
"const badlyFormatted = 'code';",
572+
{
573+
code: "const badlyFormatted = 'code';",
574+
},
575+
],
576+
invalid: [
577+
{
578+
code: "const badlyFormatted = 'code';",
579+
errors: [],
580+
},
581+
],
582+
};
583+
`,
584+
errors: [
585+
{
586+
messageId: 'invalidFormatting',
587+
},
588+
{
589+
messageId: 'invalidFormatting',
590+
},
591+
{
592+
messageId: 'invalidFormattingErrorTest',
593+
},
594+
],
595+
},
596+
{
597+
code: `
598+
import { TSESLint } from '@typescript-eslint/utils';
599+
600+
const test: TSESLint.RunTests<'', []> = {
601+
valid: [
602+
'const badlyFormatted = "code"',
603+
{
604+
code: 'const badlyFormatted = "code"',
605+
},
606+
],
607+
invalid: [
608+
{
609+
code: 'const badlyFormatted = "code"',
610+
errors: [],
611+
},
612+
],
613+
};
614+
`,
615+
output: `
616+
import { TSESLint } from '@typescript-eslint/utils';
617+
618+
const test: TSESLint.RunTests<'', []> = {
619+
valid: [
620+
"const badlyFormatted = 'code';",
621+
{
622+
code: "const badlyFormatted = 'code';",
623+
},
624+
],
625+
invalid: [
626+
{
627+
code: "const badlyFormatted = 'code';",
628+
errors: [],
629+
},
630+
],
631+
};
632+
`,
633+
errors: [
634+
{
635+
messageId: 'invalidFormatting',
636+
},
637+
{
638+
messageId: 'invalidFormatting',
639+
},
640+
{
641+
messageId: 'invalidFormattingErrorTest',
642+
},
643+
],
644+
},
645+
{
646+
code: `
647+
import { TSESLint } from '@typescript-eslint/utils';
648+
649+
const test: TSESLint.ValidTestCase<[]> = {
650+
code: 'const badlyFormatted = "code"',
651+
};
652+
`,
653+
output: `
654+
import { TSESLint } from '@typescript-eslint/utils';
655+
656+
const test: TSESLint.ValidTestCase<[]> = {
657+
code: "const badlyFormatted = 'code';",
658+
};
659+
`,
660+
errors: [
661+
{
662+
messageId: 'invalidFormattingErrorTest',
663+
},
664+
],
665+
},
666+
{
667+
code: `
668+
import { TSESLint } from '@typescript-eslint/utils';
669+
670+
const test: TSESLint.InvalidTestCase<'', []> = {
671+
code: 'const badlyFormatted = "code1"',
672+
errors: [
673+
{
674+
code: 'const badlyFormatted = "code2"',
675+
// shouldn't get fixed as per rule ignoring output
676+
output: 'const badlyFormatted = "code3"',
677+
suggestions: [
678+
{
679+
messageId: '',
680+
// shouldn't get fixed as per rule ignoring output
681+
output: 'const badlyFormatted = "code4"',
682+
},
683+
],
684+
},
685+
],
686+
};
687+
`,
688+
output: `
689+
import { TSESLint } from '@typescript-eslint/utils';
690+
691+
const test: TSESLint.InvalidTestCase<'', []> = {
692+
code: "const badlyFormatted = 'code1';",
693+
errors: [
694+
{
695+
code: "const badlyFormatted = 'code2';",
696+
// shouldn't get fixed as per rule ignoring output
697+
output: 'const badlyFormatted = "code3"',
698+
suggestions: [
699+
{
700+
messageId: '',
701+
// shouldn't get fixed as per rule ignoring output
702+
output: 'const badlyFormatted = "code4"',
703+
},
704+
],
705+
},
706+
],
707+
};
708+
`,
709+
errors: [
710+
{
711+
messageId: 'invalidFormattingErrorTest',
712+
},
713+
{
714+
messageId: 'invalidFormattingErrorTest',
715+
},
716+
],
717+
},
509718
],
510719
});

0 commit comments

Comments
 (0)