Skip to content

Commit

Permalink
chore(eslint-plugin-internal): [plugin-test-formatting] support rando…
Browse files Browse the repository at this point in the history
…m object literal tests (#5895)

* chore(eslint-plugin-internal): [plugin-test-formatting] support random object literal tests

* update test column
  • Loading branch information
bradzacher committed Nov 2, 2022
1 parent c874e50 commit f11183c
Show file tree
Hide file tree
Showing 7 changed files with 3,225 additions and 2,945 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin-internal/package.json
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@types/prettier": "*",
"@typescript-eslint/scope-manager": "5.42.0",
"@typescript-eslint/type-utils": "5.42.0",
"@typescript-eslint/utils": "5.42.0",
"prettier": "*"
}
Expand Down
109 changes: 94 additions & 15 deletions packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts
@@ -1,5 +1,6 @@
import { getContextualType } from '@typescript-eslint/type-utils';
import type { TSESTree } from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
import { format, resolveConfig } from 'prettier';

import { createRule } from '../util';
Expand Down Expand Up @@ -108,6 +109,7 @@ export default createRule<Options, MessageIds>({
docs: {
description: `Enforces that eslint-plugin test snippets are correctly formatted`,
recommended: 'error',
requiresTypeChecking: true,
},
fixable: 'code',
schema: [
Expand Down Expand Up @@ -146,6 +148,11 @@ export default createRule<Options, MessageIds>({
],
create(context, [{ formatWithPrettier }]) {
const sourceCode = context.getSourceCode();
const { program, esTreeNodeToTSNodeMap } =
ESLintUtils.getParserServices(context);
const checker = program.getTypeChecker();

const checkedObjects = new Set<TSESTree.ObjectExpression>();

function prettierFormat(
code: string,
Expand Down Expand Up @@ -448,6 +455,12 @@ export default createRule<Options, MessageIds>({
test: TSESTree.ObjectExpression,
isErrorTest = true,
): void {
if (checkedObjects.has(test)) {
return;
}

checkedObjects.add(test);

for (const prop of test.properties) {
if (
prop.type !== AST_NODE_TYPES.Property ||
Expand Down Expand Up @@ -478,33 +491,99 @@ export default createRule<Options, MessageIds>({
}
}

const invalidTestsSelectorPath = [
AST_NODE_TYPES.CallExpression,
AST_NODE_TYPES.ObjectExpression,
'Property[key.name = "invalid"]',
AST_NODE_TYPES.ArrayExpression,
AST_NODE_TYPES.ObjectExpression,
];

return {
// valid
'CallExpression > ObjectExpression > Property[key.name = "valid"] > ArrayExpression':
checkValidTest,
// invalid - errors
[invalidTestsSelectorPath.join(' > ')]: checkInvalidTest,
// invalid - suggestions
[[
...invalidTestsSelectorPath,
'Property[key.name = "errors"]',
AST_NODE_TYPES.ArrayExpression,
AST_NODE_TYPES.CallExpression,
AST_NODE_TYPES.ObjectExpression,
'Property[key.name = "suggestions"]',
'Property[key.name = "invalid"]',
AST_NODE_TYPES.ArrayExpression,
AST_NODE_TYPES.ObjectExpression,
].join(' > ')]: checkInvalidTest,
// special case for our batchedSingleLineTests utility
'CallExpression[callee.name = "batchedSingleLineTests"] > ObjectExpression':
checkInvalidTest,

/**
* generic, type-aware handling for any old object
* this is a fallback to handle random variables people declare or object
* literals that are passed via array maps, etc
*/
ObjectExpression(node): void {
if (checkedObjects.has(node)) {
return;
}

const type = getContextualType(
checker,
esTreeNodeToTSNodeMap.get(node),
);
if (!type) {
return;
}

const typeString = checker.typeToString(type);
if (/^RunTests\b/.test(typeString)) {
checkedObjects.add(node);

for (const prop of node.properties) {
if (
prop.type === AST_NODE_TYPES.SpreadElement ||
prop.computed ||
prop.key.type !== AST_NODE_TYPES.Identifier ||
prop.value.type !== AST_NODE_TYPES.ArrayExpression
) {
continue;
}

switch (prop.key.name) {
case 'valid':
checkValidTest(prop.value);
break;

case 'invalid':
for (const element of prop.value.elements) {
if (element.type === AST_NODE_TYPES.ObjectExpression) {
checkInvalidTest(element);
}
}
break;
}
}
return;
}

if (/^ValidTestCase\b/.test(typeString)) {
checkInvalidTest(node);
return;
}

if (/^InvalidTestCase\b/.test(typeString)) {
checkInvalidTest(node);
for (const testProp of node.properties) {
if (
testProp.type === AST_NODE_TYPES.SpreadElement ||
testProp.computed ||
testProp.key.type !== AST_NODE_TYPES.Identifier ||
testProp.key.name !== 'errors' ||
testProp.value.type !== AST_NODE_TYPES.ArrayExpression
) {
continue;
}

for (const errorElement of testProp.value.elements) {
if (errorElement.type !== AST_NODE_TYPES.ObjectExpression) {
continue;
}

checkInvalidTest(errorElement);
}
}
}
},
};
},
});
@@ -1,9 +1,11 @@
import rule from '../../src/rules/plugin-test-formatting';
import { RuleTester } from '../RuleTester';
import { getFixturesRootDir, RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: getFixturesRootDir(),
sourceType: 'module',
},
});
Expand Down Expand Up @@ -132,6 +134,44 @@ ${CODE_INDENT}const a = 1;
${CODE_INDENT}const b = 1;
${PARENT_INDENT}\``,

// random, unannotated variables aren't checked
`
const test1 = {
code: 'const badlyFormatted = "code"',
};
const test2 = {
valid: [
'const badlyFormatted = "code"',
{
code: 'const badlyFormatted = "code"',
},
],
invalid: [
{
code: 'const badlyFormatted = "code"',
errors: [],
},
],
};
`,

// TODO - figure out how to handle this pattern
`
import { TSESLint } from '@typescript-eslint/utils';
const test = [
{
code: 'const badlyFormatted = "code1"',
},
{
code: 'const badlyFormatted = "code2"',
},
].map<TSESLint.InvalidTestCase<[]>>(test => ({
code: test.code,
errors: [],
}));
`,
],
invalid: [
// Literal
Expand Down Expand Up @@ -506,5 +546,174 @@ foo
},
],
},

// annotated variables are checked
{
code: `
const test: RunTests = {
valid: [
'const badlyFormatted = "code"',
{
code: 'const badlyFormatted = "code"',
},
],
invalid: [
{
code: 'const badlyFormatted = "code"',
errors: [],
},
],
};
`,
output: `
const test: RunTests = {
valid: [
"const badlyFormatted = 'code';",
{
code: "const badlyFormatted = 'code';",
},
],
invalid: [
{
code: "const badlyFormatted = 'code';",
errors: [],
},
],
};
`,
errors: [
{
messageId: 'invalidFormatting',
},
{
messageId: 'invalidFormatting',
},
{
messageId: 'invalidFormattingErrorTest',
},
],
},
{
code: `
import { TSESLint } from '@typescript-eslint/utils';
const test: TSESLint.RunTests<'', []> = {
valid: [
'const badlyFormatted = "code"',
{
code: 'const badlyFormatted = "code"',
},
],
invalid: [
{
code: 'const badlyFormatted = "code"',
errors: [],
},
],
};
`,
output: `
import { TSESLint } from '@typescript-eslint/utils';
const test: TSESLint.RunTests<'', []> = {
valid: [
"const badlyFormatted = 'code';",
{
code: "const badlyFormatted = 'code';",
},
],
invalid: [
{
code: "const badlyFormatted = 'code';",
errors: [],
},
],
};
`,
errors: [
{
messageId: 'invalidFormatting',
},
{
messageId: 'invalidFormatting',
},
{
messageId: 'invalidFormattingErrorTest',
},
],
},
{
code: `
import { TSESLint } from '@typescript-eslint/utils';
const test: TSESLint.ValidTestCase<[]> = {
code: 'const badlyFormatted = "code"',
};
`,
output: `
import { TSESLint } from '@typescript-eslint/utils';
const test: TSESLint.ValidTestCase<[]> = {
code: "const badlyFormatted = 'code';",
};
`,
errors: [
{
messageId: 'invalidFormattingErrorTest',
},
],
},
{
code: `
import { TSESLint } from '@typescript-eslint/utils';
const test: TSESLint.InvalidTestCase<'', []> = {
code: 'const badlyFormatted = "code1"',
errors: [
{
code: 'const badlyFormatted = "code2"',
// shouldn't get fixed as per rule ignoring output
output: 'const badlyFormatted = "code3"',
suggestions: [
{
messageId: '',
// shouldn't get fixed as per rule ignoring output
output: 'const badlyFormatted = "code4"',
},
],
},
],
};
`,
output: `
import { TSESLint } from '@typescript-eslint/utils';
const test: TSESLint.InvalidTestCase<'', []> = {
code: "const badlyFormatted = 'code1';",
errors: [
{
code: "const badlyFormatted = 'code2';",
// shouldn't get fixed as per rule ignoring output
output: 'const badlyFormatted = "code3"',
suggestions: [
{
messageId: '',
// shouldn't get fixed as per rule ignoring output
output: 'const badlyFormatted = "code4"',
},
],
},
],
};
`,
errors: [
{
messageId: 'invalidFormattingErrorTest',
},
{
messageId: 'invalidFormattingErrorTest',
},
],
},
],
});

0 comments on commit f11183c

Please sign in to comment.