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" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy