From c1bc74da8f820b2382d68c60bdb8672c20f5eb08 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 20 Jun 2025 21:35:13 +0900 Subject: [PATCH 01/19] test: add test cases --- .../tests/rules/no-dynamic-tests.test.ts | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts new file mode 100644 index 000000000000..de26de14f3ca --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -0,0 +1,191 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-dynamic-tests'; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: {}, + ecmaVersion: 6, + sourceType: 'module', + }, + }, +}); + +ruleTester.run('no-dynamic-tests', rule, { + invalid: [ + // Function calls in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [generateTestCases()], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [], + invalid: [...getInvalidCases()], +}); + `, + errors: [ + { + column: 13, + line: 4, + messageId: 'noDynamicTests', + }, + ], + }, + // Spread operator in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [...validTestCases], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [...validTestCases.map(t => t.code)], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Simple identifiers in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [testCase], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Template literals in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [\`\${getTest()}\`], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Binary expressions in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: ['test' + getSuffix()], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Conditional expressions in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [shouldTest ? 'test1' : 'test2'], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Member expressions in test arrays + { + code: ` +ruleTester.run('test', rule, { + valid: [testConfig.cases], + invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + ], + valid: [ + { + code: ` +ruleTester.run('test', rule, { + valid: ['const x = 1;'], + invalid: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: ['const x = 1;', 'let y = 2;'], + invalid: [ + { + code: 'var z = 3;', + errors: [{ messageId: 'error' }], + }, + ], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [{ code: 'const x = 1;' }, { code: 'let y = 2;' }], + invalid: [], +}); + `, + }, + ], +}); From 8a2f9333da8159e8cf18a2169c625b4e47e8a9cf Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 20 Jun 2025 21:35:32 +0900 Subject: [PATCH 02/19] feat: internal no-dynamic-tests rule --- .../src/rules/no-dynamic-tests.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts new file mode 100644 index 000000000000..16a92c81aff0 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -0,0 +1,109 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule } from '../util'; + +export default createRule({ + name: 'no-dynamic-tests', + meta: { + type: 'problem', + docs: { + description: 'Disallow dynamic syntax in RuleTester test arrays', + }, + messages: { + noDynamicTests: + 'Dynamic syntax is not allowed in RuleTester test arrays. Use static values only.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + function isRuleTesterCall(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.CallExpression && + node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.type === AST_NODE_TYPES.Identifier && + node.callee.object.name === 'ruleTester' && + node.callee.property.type === AST_NODE_TYPES.Identifier && + node.callee.property.name === 'run' + ); + } + + function isDynamicExpression(node: TSESTree.Node): boolean { + switch (node.type) { + case AST_NODE_TYPES.CallExpression: + return true; + case AST_NODE_TYPES.SpreadElement: + return true; + case AST_NODE_TYPES.Identifier: + return true; + case AST_NODE_TYPES.TemplateLiteral: + return node.expressions.some(expr => isDynamicExpression(expr)); + case AST_NODE_TYPES.BinaryExpression: + return ( + isDynamicExpression(node.left) || isDynamicExpression(node.right) + ); + case AST_NODE_TYPES.UnaryExpression: + return isDynamicExpression(node.argument); + case AST_NODE_TYPES.ConditionalExpression: + return ( + isDynamicExpression(node.test) || + isDynamicExpression(node.consequent) || + isDynamicExpression(node.alternate) + ); + case AST_NODE_TYPES.LogicalExpression: + return ( + isDynamicExpression(node.left) || isDynamicExpression(node.right) + ); + case AST_NODE_TYPES.MemberExpression: + return ( + isDynamicExpression(node.object) || + isDynamicExpression(node.property) + ); + case AST_NODE_TYPES.ArrayExpression: + return node.elements.some( + element => element && isDynamicExpression(element), + ); + case AST_NODE_TYPES.ObjectExpression: + return node.properties.some(prop => { + if (prop.type === AST_NODE_TYPES.SpreadElement) { + return true; + } + return isDynamicExpression(prop.value); + }); + case AST_NODE_TYPES.Literal: + default: + return false; + } + } + + return { + // Check RuleTester.run calls + CallExpression(node) { + if (isRuleTesterCall(node)) { + const testObject = node.arguments[2]; + if (testObject.type === AST_NODE_TYPES.ObjectExpression) { + for (const prop of testObject.properties) { + if ( + prop.type === AST_NODE_TYPES.Property && + prop.key.type === AST_NODE_TYPES.Identifier && + (prop.key.name === 'valid' || prop.key.name === 'invalid') && // Check each element in the array + prop.value.type === AST_NODE_TYPES.ArrayExpression + ) { + prop.value.elements.forEach(element => { + if (element && isDynamicExpression(element)) { + context.report({ + node: element, + messageId: 'noDynamicTests', + }); + } + }); + } + } + } + } + }, + }; + }, +}); From e83a77d09c5826de5c96319f80a578588450f7c5 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 20 Jun 2025 21:36:25 +0900 Subject: [PATCH 03/19] feat: export rule --- packages/eslint-plugin-internal/src/rules/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin-internal/src/rules/index.ts b/packages/eslint-plugin-internal/src/rules/index.ts index 806618f909e4..e51eafd1910f 100644 --- a/packages/eslint-plugin-internal/src/rules/index.ts +++ b/packages/eslint-plugin-internal/src/rules/index.ts @@ -2,6 +2,7 @@ import type { Linter } from '@typescript-eslint/utils/ts-eslint'; import debugNamespace from './debug-namespace'; import eqeqNullish from './eqeq-nullish'; +import noDynamicTests from './no-dynamic-tests'; import noPoorlyTypedTsProps from './no-poorly-typed-ts-props'; import noRelativePathsToInternalPackages from './no-relative-paths-to-internal-packages'; import noTypescriptDefaultImport from './no-typescript-default-import'; @@ -12,6 +13,7 @@ import preferASTTypesEnum from './prefer-ast-types-enum'; export default { 'debug-namespace': debugNamespace, 'eqeq-nullish': eqeqNullish, + 'no-dynamic-tests': noDynamicTests, 'no-poorly-typed-ts-props': noPoorlyTypedTsProps, 'no-relative-paths-to-internal-packages': noRelativePathsToInternalPackages, 'no-typescript-default-import': noTypescriptDefaultImport, From 5a2d0205a9a7fcdd063caa2fa18c817dc92bd0ef Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 20 Jun 2025 22:11:38 +0900 Subject: [PATCH 04/19] fix: more suitable conditions --- .../src/rules/no-dynamic-tests.ts | 21 +++---------------- .../tests/rules/no-dynamic-tests.test.ts | 16 ++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index 16a92c81aff0..f9784895bdfc 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -41,26 +41,11 @@ export default createRule({ case AST_NODE_TYPES.TemplateLiteral: return node.expressions.some(expr => isDynamicExpression(expr)); case AST_NODE_TYPES.BinaryExpression: - return ( - isDynamicExpression(node.left) || isDynamicExpression(node.right) - ); - case AST_NODE_TYPES.UnaryExpression: - return isDynamicExpression(node.argument); + return true; case AST_NODE_TYPES.ConditionalExpression: - return ( - isDynamicExpression(node.test) || - isDynamicExpression(node.consequent) || - isDynamicExpression(node.alternate) - ); - case AST_NODE_TYPES.LogicalExpression: - return ( - isDynamicExpression(node.left) || isDynamicExpression(node.right) - ); + return true; case AST_NODE_TYPES.MemberExpression: - return ( - isDynamicExpression(node.object) || - isDynamicExpression(node.property) - ); + return true; case AST_NODE_TYPES.ArrayExpression: return node.elements.some( element => element && isDynamicExpression(element), diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index de26de14f3ca..2f9ecc7d3cde 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -146,6 +146,22 @@ ruleTester.run('test', rule, { ruleTester.run('test', rule, { valid: [testConfig.cases], invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + // Object spread + { + code: ` +ruleTester.run('test', rule, { + valid: [{ ...testConfig }], + invalid: [], }); `, errors: [ From 6a2924821a14c2262da257ec2e6952eca74a558c Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 24 Jun 2025 16:50:36 +0900 Subject: [PATCH 05/19] refactor: remove cmts --- packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index f9784895bdfc..a4f7772ead26 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -64,7 +64,6 @@ export default createRule({ } return { - // Check RuleTester.run calls CallExpression(node) { if (isRuleTesterCall(node)) { const testObject = node.arguments[2]; @@ -73,7 +72,7 @@ export default createRule({ if ( prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier && - (prop.key.name === 'valid' || prop.key.name === 'invalid') && // Check each element in the array + (prop.key.name === 'valid' || prop.key.name === 'invalid') && prop.value.type === AST_NODE_TYPES.ArrayExpression ) { prop.value.elements.forEach(element => { From 987cd78366f8a9320cd51af48f5c26c90d529b47 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 24 Jun 2025 17:23:04 +0900 Subject: [PATCH 06/19] test: add valid tc for noFormat --- .../eslint-plugin-internal/src/rules/no-dynamic-tests.ts | 1 + .../tests/rules/no-dynamic-tests.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index a4f7772ead26..53d06f619214 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -58,6 +58,7 @@ export default createRule({ return isDynamicExpression(prop.value); }); case AST_NODE_TYPES.Literal: + case AST_NODE_TYPES.TaggedTemplateExpression: default: return false; } diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 2f9ecc7d3cde..88bfe5784c70 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -200,6 +200,14 @@ ruleTester.run('test', rule, { ruleTester.run('test', rule, { valid: [{ code: 'const x = 1;' }, { code: 'let y = 2;' }], invalid: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [noFormat\`const x = 1;\`], + invalid: [], }); `, }, From 003728344f5e7bce247ee670a838911ba8acd99c Mon Sep 17 00:00:00 2001 From: nayounsang Date: Mon, 7 Jul 2025 22:51:01 +0900 Subject: [PATCH 07/19] fix: should allow noFormat tag --- .../src/rules/no-dynamic-tests.ts | 6 +++++- .../tests/rules/no-dynamic-tests.test.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index 53d06f619214..1f3440e7dbc5 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -57,8 +57,12 @@ export default createRule({ } return isDynamicExpression(prop.value); }); - case AST_NODE_TYPES.Literal: case AST_NODE_TYPES.TaggedTemplateExpression: + return !( + node.tag.type === AST_NODE_TYPES.Identifier && + node.tag.name === 'noFormat' + ); + case AST_NODE_TYPES.Literal: default: return false; } diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 88bfe5784c70..824f1fbad114 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -162,6 +162,21 @@ ruleTester.run('test', rule, { ruleTester.run('test', rule, { valid: [{ ...testConfig }], invalid: [], +}); + `, + errors: [ + { + column: 11, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [foo\`const x = 1;\`], + invalid: [], }); `, errors: [ From 9c3ebce3c8911e370e24b25492e7fac63155b818 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Mon, 7 Jul 2025 23:24:15 +0900 Subject: [PATCH 08/19] fix: narrow report node --- .../src/rules/no-dynamic-tests.ts | 71 ++++++++++++------- .../tests/rules/no-dynamic-tests.test.ts | 4 +- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index 1f3440e7dbc5..a08920fe955c 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -30,48 +30,70 @@ export default createRule({ ); } - function isDynamicExpression(node: TSESTree.Node): boolean { + function reportDynamicElements(node: TSESTree.Node): void { switch (node.type) { case AST_NODE_TYPES.CallExpression: - return true; case AST_NODE_TYPES.SpreadElement: - return true; case AST_NODE_TYPES.Identifier: - return true; - case AST_NODE_TYPES.TemplateLiteral: - return node.expressions.some(expr => isDynamicExpression(expr)); case AST_NODE_TYPES.BinaryExpression: - return true; case AST_NODE_TYPES.ConditionalExpression: - return true; case AST_NODE_TYPES.MemberExpression: - return true; + context.report({ + node, + messageId: 'noDynamicTests', + }); + break; + case AST_NODE_TYPES.TemplateLiteral: + node.expressions.forEach(expr => { + reportDynamicElements(expr); + }); + break; case AST_NODE_TYPES.ArrayExpression: - return node.elements.some( - element => element && isDynamicExpression(element), - ); + node.elements.forEach(element => { + if (element) { + reportDynamicElements(element); + } + }); + break; case AST_NODE_TYPES.ObjectExpression: - return node.properties.some(prop => { + node.properties.forEach(prop => { if (prop.type === AST_NODE_TYPES.SpreadElement) { - return true; + context.report({ + node: prop, + messageId: 'noDynamicTests', + }); + } else { + reportDynamicElements(prop.value); } - return isDynamicExpression(prop.value); }); + break; case AST_NODE_TYPES.TaggedTemplateExpression: - return !( - node.tag.type === AST_NODE_TYPES.Identifier && - node.tag.name === 'noFormat' - ); - case AST_NODE_TYPES.Literal: + if ( + !( + node.tag.type === AST_NODE_TYPES.Identifier && + node.tag.name === 'noFormat' + ) + ) { + context.report({ + node, + messageId: 'noDynamicTests', + }); + } + break; default: - return false; + break; } } return { CallExpression(node) { if (isRuleTesterCall(node)) { + // If valid code, arg length is always 3 but we need to avoid conflict while dev + if (node.arguments.length < 3) { + return; + } const testObject = node.arguments[2]; + if (testObject.type === AST_NODE_TYPES.ObjectExpression) { for (const prop of testObject.properties) { if ( @@ -81,11 +103,8 @@ export default createRule({ prop.value.type === AST_NODE_TYPES.ArrayExpression ) { prop.value.elements.forEach(element => { - if (element && isDynamicExpression(element)) { - context.report({ - node: element, - messageId: 'noDynamicTests', - }); + if (element) { + reportDynamicElements(element); } }); } diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 824f1fbad114..95ae5232f46e 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -102,7 +102,7 @@ ruleTester.run('test', rule, { `, errors: [ { - column: 11, + column: 14, line: 3, messageId: 'noDynamicTests', }, @@ -166,7 +166,7 @@ ruleTester.run('test', rule, { `, errors: [ { - column: 11, + column: 13, line: 3, messageId: 'noDynamicTests', }, From ac6726f6cc90878b2f0ce0d8fd6e8ec1d35f6cc9 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Mon, 7 Jul 2025 23:39:26 +0900 Subject: [PATCH 09/19] test: add nested dynamic test --- .../tests/rules/no-dynamic-tests.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 95ae5232f46e..789425b8bfba 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -187,6 +187,21 @@ ruleTester.run('test', rule, { }, ], }, + { + code: ` +ruleTester.run('test', rule, { + valid: [{ code: \`foo\`, errors: [{ messageId: getMessageId() }] }], + invalid: [], +}); + `, + errors: [ + { + column: 44, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, ], valid: [ { From dc8b5687c9446afc9b0e60d1672122e43c41ba64 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Mon, 7 Jul 2025 23:43:53 +0900 Subject: [PATCH 10/19] fix: fix err column --- .../eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 789425b8bfba..9d7ef3471df5 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -196,7 +196,7 @@ ruleTester.run('test', rule, { `, errors: [ { - column: 44, + column: 48, line: 3, messageId: 'noDynamicTests', }, From f81a9dedaf879dd0079a58074f254a60cf9da004 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 9 Jul 2025 21:24:53 +0900 Subject: [PATCH 11/19] feat: enable object dynamic value --- .../src/rules/no-dynamic-tests.ts | 5 ++-- .../tests/rules/no-dynamic-tests.test.ts | 26 ++++++++----------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index a08920fe955c..39334c487761 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -62,8 +62,6 @@ export default createRule({ node: prop, messageId: 'noDynamicTests', }); - } else { - reportDynamicElements(prop.value); } }); break; @@ -75,11 +73,12 @@ export default createRule({ ) ) { context.report({ - node, + node: node.tag, messageId: 'noDynamicTests', }); } break; + case AST_NODE_TYPES.Literal: default: break; } diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 9d7ef3471df5..e3eafcf9adde 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -187,21 +187,6 @@ ruleTester.run('test', rule, { }, ], }, - { - code: ` -ruleTester.run('test', rule, { - valid: [{ code: \`foo\`, errors: [{ messageId: getMessageId() }] }], - invalid: [], -}); - `, - errors: [ - { - column: 48, - line: 3, - messageId: 'noDynamicTests', - }, - ], - }, ], valid: [ { @@ -238,6 +223,17 @@ ruleTester.run('test', rule, { ruleTester.run('test', rule, { valid: [noFormat\`const x = 1;\`], invalid: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + code: "import type { ValueOf } from './utils';", + filename: path.resolve( + PACKAGES_DIR, + 'ast-spec/src/expression/AssignmentExpression/spec.ts', + ), }); `, }, From ba3225928a516acc0f18e7dd6a2640c6e4a7f8e3 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 9 Jul 2025 21:42:36 +0900 Subject: [PATCH 12/19] feat: key to validate --- .../src/rules/no-dynamic-tests.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index 39334c487761..e0a822609fba 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -1,3 +1,4 @@ +import type { InvalidTestCase } from '@typescript-eslint/rule-tester'; import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -62,6 +63,22 @@ export default createRule({ node: prop, messageId: 'noDynamicTests', }); + } else { + // InvalidTestCase extends ValidTestCase + type TestCaseKey = keyof InvalidTestCase; + const keyToValidate: TestCaseKey[] = ['code', 'errors']; + + if ( + prop.key.type === AST_NODE_TYPES.Identifier && + keyToValidate.includes(prop.key.name as TestCaseKey) + ) { + reportDynamicElements(prop.value); + } else if ( + prop.key.type === AST_NODE_TYPES.Literal && + keyToValidate.includes(prop.key.value as TestCaseKey) + ) { + reportDynamicElements(prop.value); + } } }); break; From 62d39b7146d50127d6a98504d0351ee3ba983ded Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 10 Jul 2025 21:27:25 +0900 Subject: [PATCH 13/19] test: add test case for object value: error and code --- .../tests/rules/no-dynamic-tests.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index e3eafcf9adde..67fd5dcfe5e8 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -172,6 +172,7 @@ ruleTester.run('test', rule, { }, ], }, + // Tag { code: ` ruleTester.run('test', rule, { @@ -187,6 +188,37 @@ ruleTester.run('test', rule, { }, ], }, + // Object Value + { + code: ` +ruleTester.run('test', rule, { + valid: [{ code: foo }], + invalid: [], +}); + `, + errors: [ + { + column: 19, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [{ errors: [...getErrors()] }], + invalid: [], +}); + `, + errors: [ + { + column: 22, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, ], valid: [ { From 9aa122c3a7f46f0840df39ec239dd923b0bb4842 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 10 Jul 2025 21:58:59 +0900 Subject: [PATCH 14/19] fix: ban direct assigned test case --- .../src/rules/no-dynamic-tests.ts | 25 ++++++++++++------- .../tests/rules/no-dynamic-tests.test.ts | 16 ++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts index e0a822609fba..ba9625e1b03e 100644 --- a/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -112,17 +112,24 @@ export default createRule({ if (testObject.type === AST_NODE_TYPES.ObjectExpression) { for (const prop of testObject.properties) { - if ( + const isTestCases = prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier && - (prop.key.name === 'valid' || prop.key.name === 'invalid') && - prop.value.type === AST_NODE_TYPES.ArrayExpression - ) { - prop.value.elements.forEach(element => { - if (element) { - reportDynamicElements(element); - } - }); + (prop.key.name === 'valid' || prop.key.name === 'invalid'); + + if (isTestCases) { + if (prop.value.type === AST_NODE_TYPES.ArrayExpression) { + prop.value.elements.forEach(element => { + if (element) { + reportDynamicElements(element); + } + }); + } else { + context.report({ + node: prop.value, + messageId: 'noDynamicTests', + }); + } } } } diff --git a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts index 67fd5dcfe5e8..c530015d0eb2 100644 --- a/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -219,6 +219,22 @@ ruleTester.run('test', rule, { }, ], }, + // assign directly + { + code: ` +ruleTester.run('test', rule, { + valid: foo, + invalid: [], +}); + `, + errors: [ + { + column: 10, + line: 3, + messageId: 'noDynamicTests', + }, + ], + }, ], valid: [ { From 4b9d47a371e41fd900b37b082987c4d5815356ac Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 10 Jul 2025 22:12:38 +0900 Subject: [PATCH 15/19] chore: test commit to enable new rule in CI --- eslint.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 114cea61a45a..9fba41c66c05 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -417,6 +417,7 @@ export default tseslint.config( name: 'eslint-plugin-and-eslint-plugin-internal/test-files/rules', rules: { '@typescript-eslint/internal/plugin-test-formatting': 'error', + '@typescript-eslint/internal/no-dynamic-tests': 'error', }, }, From f2b9c1a24c17d48ecc88ecaf41ee1d1259e12d79 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 10 Jul 2025 23:39:47 +0900 Subject: [PATCH 16/19] chore: make bulk supress file --- eslint-suppressions.json | 97 ++++++++++++++++++++++++++++++++++++++++ eslint.config.mjs | 2 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 eslint-suppressions.json diff --git a/eslint-suppressions.json b/eslint-suppressions.json new file mode 100644 index 000000000000..eddbd9cba3f6 --- /dev/null +++ b/eslint-suppressions.json @@ -0,0 +1,97 @@ +{ + "packages/eslint-plugin-internal/tests/rules/plugin-test-formatting.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 26 + } + }, + "packages/eslint-plugin/tests/rules/dot-notation.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 1 + } + }, + "packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-case-insensitive-order.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 4 + } + }, + "packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 4 + } + }, + "packages/eslint-plugin/tests/rules/naming-convention/naming-convention.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 12 + } + }, + "packages/eslint-plugin/tests/rules/no-extraneous-class.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 12 + } + }, + "packages/eslint-plugin/tests/rules/no-invalid-this.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 43 + } + }, + "packages/eslint-plugin/tests/rules/no-loop-func.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 2 + } + }, + "packages/eslint-plugin/tests/rules/no-this-alias.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 14 + } + }, + "packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 44 + } + }, + "packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 1 + } + }, + "packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 2 + } + }, + "packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 154 + } + }, + "packages/eslint-plugin/tests/rules/no-useless-empty-export.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 9 + } + }, + "packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 42 + } + }, + "packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 17 + } + }, + "packages/eslint-plugin/tests/rules/return-await.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 1 + } + }, + "packages/eslint-plugin/tests/rules/sort-type-constituents.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 4 + } + }, + "packages/eslint-plugin/tests/rules/unbound-method.test.ts": { + "@typescript-eslint/internal/no-dynamic-tests": { + "count": 3 + } + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 9fba41c66c05..671119672a0d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -416,8 +416,8 @@ export default tseslint.config( ], name: 'eslint-plugin-and-eslint-plugin-internal/test-files/rules', rules: { - '@typescript-eslint/internal/plugin-test-formatting': 'error', '@typescript-eslint/internal/no-dynamic-tests': 'error', + '@typescript-eslint/internal/plugin-test-formatting': 'error', }, }, From f2f6e5e45d1423f9bef52a20a99d9b88b446dc32 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 10 Jul 2025 23:55:39 +0900 Subject: [PATCH 17/19] chore: utility script for update bulk suprresion --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 866837f95da8..774693f877ca 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,14 @@ "lint-markdown-fix": "yarn lint-markdown --fix", "lint-markdown": "markdownlint \"**/*.md\" --config=.markdownlint.json --ignore-path=.markdownlintignore", "lint-stylelint": "nx lint website stylelint", + "lint-prune-suppressions": "nx run-many -t lint --projects=eslint-plugin-internal,eslint-plugin --prune-suppressions", "lint": "nx run-many -t lint", "postinstall": "tsx tools/scripts/postinstall.mts", "pre-commit": "lint-staged", "release": "tsx tools/release/release.mts", "start": "nx run website:start", "test": "nx run-many -t test --exclude integration-tests website website-eslint", + "test-tmp": "vitest packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts", "test-integration": "nx run integration-tests:test", "typecheck": "nx run-many -t typecheck" }, @@ -77,7 +79,7 @@ "console-fail-test": "^0.5.0", "cross-fetch": "^4.0.0", "cspell": "^9.0.0", - "eslint": "^9.26.0", + "eslint": "^9.30.1", "eslint-plugin-eslint-plugin": "^6.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.5.0", From 9c8b49a5f73cb0513742a09b60466a57f301320c Mon Sep 17 00:00:00 2001 From: nayounsang Date: Thu, 10 Jul 2025 23:58:10 +0900 Subject: [PATCH 18/19] fix: update lock --- yarn.lock | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index a4ac6c4b95ec..d55059765c91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3935,6 +3935,17 @@ __metadata: languageName: node linkType: hard +"@eslint/config-array@npm:^0.21.0": + version: 0.21.0 + resolution: "@eslint/config-array@npm:0.21.0" + dependencies: + "@eslint/object-schema": ^2.1.6 + debug: ^4.3.1 + minimatch: ^3.1.2 + checksum: 84d3ae7cb755af94dc158a74389f4c560757b13f2bb908f598f927b87b70a38e8152015ea2e9557c1b4afc5130ee1356f6cad682050d67aae0468bbef98bc3a8 + languageName: node + linkType: hard + "@eslint/config-helpers@npm:^0.2.1": version: 0.2.2 resolution: "@eslint/config-helpers@npm:0.2.2" @@ -3942,6 +3953,13 @@ __metadata: languageName: node linkType: hard +"@eslint/config-helpers@npm:^0.3.0": + version: 0.3.0 + resolution: "@eslint/config-helpers@npm:0.3.0" + checksum: d4fe8242ef580806ddaa88309f4bb2d3e6be5524cc6d6197675106c6d048f766a3f9cdc2e8e33bbc97a123065792cac8314fc85ac2b3cf72610e8df59301d63a + languageName: node + linkType: hard + "@eslint/core@npm:^0.13.0": version: 0.13.0 resolution: "@eslint/core@npm:0.13.0" @@ -3951,6 +3969,24 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^0.14.0": + version: 0.14.0 + resolution: "@eslint/core@npm:0.14.0" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: d68b8282b6f38c5145234f812f18f491d12d716240875591bd54bf5ac32858d979bdf6d38e521997a6e01f2c4223a3e66049714151da7278d0a95ff15b5d46c8 + languageName: node + linkType: hard + +"@eslint/core@npm:^0.15.1": + version: 0.15.1 + resolution: "@eslint/core@npm:0.15.1" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: 9215f00466d60764453466604443a491b0ea8263c148836fef723354d6ef1d550991e931d3df2780c99cee2cab14c4f41f97d5341ab12a8443236c961bb6f664 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.2.0, @eslint/eslintrc@npm:^3.3.1": version: 3.3.1 resolution: "@eslint/eslintrc@npm:3.3.1" @@ -3975,6 +4011,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:9.30.1": + version: 9.30.1 + resolution: "@eslint/js@npm:9.30.1" + checksum: 596adcd4336f098121b4f3f336169dabe86ca8d34b9fb4e30c9c44ccbb10def931bdbbd92cd910776c4030a05ae614fbc89fc8d09f69f5bad2795cd7157678e8 + languageName: node + linkType: hard + "@eslint/object-schema@npm:^2.1.6": version: 2.1.6 resolution: "@eslint/object-schema@npm:2.1.6" @@ -3992,6 +4035,16 @@ __metadata: languageName: node linkType: hard +"@eslint/plugin-kit@npm:^0.3.1": + version: 0.3.3 + resolution: "@eslint/plugin-kit@npm:0.3.3" + dependencies: + "@eslint/core": ^0.15.1 + levn: ^0.4.1 + checksum: c9dc7b83ed011dce35ccc66dc53aaaa87e9fb2bd7c8a11231f7624334d82c9a53552e4b1a1cb60b74073fcc49a2661be874e503aae14cf2f6ac6b1c7faeb7080 + languageName: node + linkType: hard + "@gerrit0/mini-shiki@npm:^3.2.2": version: 3.3.0 resolution: "@gerrit0/mini-shiki@npm:3.3.0" @@ -6177,7 +6230,7 @@ __metadata: console-fail-test: ^0.5.0 cross-fetch: ^4.0.0 cspell: ^9.0.0 - eslint: ^9.26.0 + eslint: ^9.30.1 eslint-plugin-eslint-plugin: ^6.3.1 eslint-plugin-import: ^2.31.0 eslint-plugin-jsdoc: ^50.5.0 @@ -6673,6 +6726,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.15.0": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" + bin: + acorn: bin/acorn + checksum: 309c6b49aedf1a2e34aaf266de06de04aab6eb097c02375c66fdeb0f64556a6a823540409914fb364d9a11bc30d79d485a2eba29af47992d3490e9886c4391c3 + languageName: node + linkType: hard + "address@npm:^1.0.1, address@npm:^1.1.2": version: 1.1.2 resolution: "address@npm:1.1.2" @@ -10249,6 +10311,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" + dependencies: + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: cf88f42cd5e81490d549dc6d350fe01e6fe420f9d9ea34f134bb359b030e3c4ef888d36667632e448937fe52449f7181501df48c08200e3d3b0fee250d05364e + languageName: node + linkType: hard + "eslint-visitor-keys@npm:^2.1.0": version: 2.1.0 resolution: "eslint-visitor-keys@npm:2.1.0" @@ -10277,7 +10349,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:*, eslint@npm:^9.15.0, eslint@npm:^9.26.0": +"eslint@npm:*, eslint@npm:^9.15.0": version: 9.26.0 resolution: "eslint@npm:9.26.0" dependencies: @@ -10329,6 +10401,56 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^9.30.1": + version: 9.30.1 + resolution: "eslint@npm:9.30.1" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.12.1 + "@eslint/config-array": ^0.21.0 + "@eslint/config-helpers": ^0.3.0 + "@eslint/core": ^0.14.0 + "@eslint/eslintrc": ^3.3.1 + "@eslint/js": 9.30.1 + "@eslint/plugin-kit": ^0.3.1 + "@humanfs/node": ^0.16.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@humanwhocodes/retry": ^0.4.2 + "@types/estree": ^1.0.6 + "@types/json-schema": ^7.0.15 + ajv: ^6.12.4 + chalk: ^4.0.0 + cross-spawn: ^7.0.6 + debug: ^4.3.2 + escape-string-regexp: ^4.0.0 + eslint-scope: ^8.4.0 + eslint-visitor-keys: ^4.2.1 + espree: ^10.4.0 + esquery: ^1.5.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^8.0.0 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + ignore: ^5.2.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + json-stable-stringify-without-jsonify: ^1.0.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.3 + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: e6723b98ba19ff17cf0cacb29c3c0ea5c7c6b6fb648136b2d009e7e2da4980a2562c9523623b0faf449750e890f3b274b20bee11fa9c8f43362d235485ba2f91 + languageName: node + linkType: hard + "espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0": version: 10.3.0 resolution: "espree@npm:10.3.0" @@ -10340,6 +10462,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" + dependencies: + acorn: ^8.15.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^4.2.1 + checksum: 5f9d0d7c81c1bca4bfd29a55270067ff9d575adb8c729a5d7f779c2c7b910bfc68ccf8ec19b29844b707440fc159a83868f22c8e87bbf7cbcb225ed067df6c85 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" From 69c88181655d10571afc5cd8cd2265e96fd16654 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 16 Jul 2025 16:23:05 +0900 Subject: [PATCH 19/19] fix: revert weird script --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 774693f877ca..c175445fb42b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "release": "tsx tools/release/release.mts", "start": "nx run website:start", "test": "nx run-many -t test --exclude integration-tests website website-eslint", - "test-tmp": "vitest packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts", "test-integration": "nx run integration-tests:test", "typecheck": "nx run-many -t typecheck" },