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, 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..53d06f619214 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts @@ -0,0 +1,94 @@ +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 true; + case AST_NODE_TYPES.ConditionalExpression: + return true; + case AST_NODE_TYPES.MemberExpression: + return true; + 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: + case AST_NODE_TYPES.TaggedTemplateExpression: + default: + return false; + } + } + + return { + 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') && + prop.value.type === AST_NODE_TYPES.ArrayExpression + ) { + prop.value.elements.forEach(element => { + if (element && isDynamicExpression(element)) { + context.report({ + node: element, + 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 new file mode 100644 index 000000000000..88bfe5784c70 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/no-dynamic-tests.test.ts @@ -0,0 +1,215 @@ +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', + }, + ], + }, + // Object spread + { + code: ` +ruleTester.run('test', rule, { + valid: [{ ...testConfig }], + 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: [], +}); + `, + }, + { + code: ` +ruleTester.run('test', rule, { + valid: [noFormat\`const x = 1;\`], + invalid: [], +}); + `, + }, + ], +});
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: