Skip to content

feat(eslint-plugin-internal): [no-dynamic-tests] new internal Lint rule to ban dynamic syntax in generating tests #11323

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c1bc74d
test: add test cases
nayounsang Jun 20, 2025
8a2f933
feat: internal no-dynamic-tests rule
nayounsang Jun 20, 2025
e83a77d
feat: export rule
nayounsang Jun 20, 2025
5a2d020
fix: more suitable conditions
nayounsang Jun 20, 2025
6a29248
refactor: remove cmts
nayounsang Jun 24, 2025
987cd78
test: add valid tc for noFormat
nayounsang Jun 24, 2025
47396ab
Merge branch 'main' into dynamic-lint
nayounsang Jul 7, 2025
0037283
fix: should allow noFormat tag
nayounsang Jul 7, 2025
9c3ebce
fix: narrow report node
nayounsang Jul 7, 2025
ac6726f
test: add nested dynamic test
nayounsang Jul 7, 2025
dc8b568
fix: fix err column
nayounsang Jul 7, 2025
f81a9de
feat: enable object dynamic value
nayounsang Jul 9, 2025
d684378
Merge branch 'main' into dynamic-lint
nayounsang Jul 9, 2025
ba32259
feat: key to validate
nayounsang Jul 9, 2025
62d39b7
test: add test case for object value: error and code
nayounsang Jul 10, 2025
9aa122c
fix: ban direct assigned test case
nayounsang Jul 10, 2025
4b9d47a
chore: test commit to enable new rule in CI
nayounsang Jul 10, 2025
53171cd
Merge branch 'main' into dynamic-lint
nayounsang Jul 10, 2025
f2b9c1a
chore: make bulk supress file
nayounsang Jul 10, 2025
f2f6e5e
chore: utility script for update bulk suprresion
nayounsang Jul 10, 2025
9c8b49a
fix: update lock
nayounsang Jul 10, 2025
a3e8ff2
Merge branch 'main' into dynamic-lint
nayounsang Jul 16, 2025
69c8818
fix: revert weird script
nayounsang Jul 16, 2025
e668354
Merge branch 'dynamic-lint' of https://github.com/nayounsang/typescri…
nayounsang Jul 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
@@ -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
}
}
}
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ export default tseslint.config(
],
name: 'eslint-plugin-and-eslint-plugin-internal/test-files/rules',
rules: {
'@typescript-eslint/internal/no-dynamic-tests': 'error',
'@typescript-eslint/internal/plugin-test-formatting': 'error',
},
},
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"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",
Expand Down Expand Up @@ -77,7 +78,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",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-internal/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
140 changes: 140 additions & 0 deletions packages/eslint-plugin-internal/src/rules/no-dynamic-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { InvalidTestCase } from '@typescript-eslint/rule-tester';
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 reportDynamicElements(node: TSESTree.Node): void {
switch (node.type) {
case AST_NODE_TYPES.CallExpression:
case AST_NODE_TYPES.SpreadElement:
case AST_NODE_TYPES.Identifier:
case AST_NODE_TYPES.BinaryExpression:
case AST_NODE_TYPES.ConditionalExpression:
case AST_NODE_TYPES.MemberExpression:
context.report({
node,
messageId: 'noDynamicTests',
});
break;
case AST_NODE_TYPES.TemplateLiteral:
node.expressions.forEach(expr => {
reportDynamicElements(expr);
});
break;
case AST_NODE_TYPES.ArrayExpression:
node.elements.forEach(element => {
if (element) {
reportDynamicElements(element);
}
});
break;
case AST_NODE_TYPES.ObjectExpression:
node.properties.forEach(prop => {
if (prop.type === AST_NODE_TYPES.SpreadElement) {
context.report({
node: prop,
messageId: 'noDynamicTests',
});
} else {
// InvalidTestCase extends ValidTestCase
type TestCaseKey = keyof InvalidTestCase<string, []>;
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;
case AST_NODE_TYPES.TaggedTemplateExpression:
if (
!(
node.tag.type === AST_NODE_TYPES.Identifier &&
node.tag.name === 'noFormat'
)
) {
context.report({
node: node.tag,
messageId: 'noDynamicTests',
});
}
break;
case AST_NODE_TYPES.Literal:
default:
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) {
const isTestCases =
prop.type === AST_NODE_TYPES.Property &&
prop.key.type === AST_NODE_TYPES.Identifier &&
(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',
});
}
}
}
}
}
},
};
},
});
Loading
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