diff --git a/lib/rules/no-node-access.ts b/lib/rules/no-node-access.ts index 9bc3a713..c57f903e 100644 --- a/lib/rules/no-node-access.ts +++ b/lib/rules/no-node-access.ts @@ -1,20 +1,15 @@ -import { - DefinitionType, - type ScopeVariable, -} from '@typescript-eslint/scope-manager'; import { TSESTree, ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { getDeepestIdentifierNode, - getPropertyIdentifierNode, - isCallExpression, + isLiteral, isMemberExpression, } from '../node-utils'; import { + ALL_QUERIES_COMBINATIONS, ALL_RETURNING_NODES, EVENT_HANDLER_METHODS, - getScope, resolveToTestingLibraryFn, } from '../utils'; @@ -60,8 +55,6 @@ export default createTestingLibraryRule({ ], create(context, [{ allowContainerFirstChild = false }], helpers) { - const userEventInstanceNames = new Set(); - function showErrorForNodeAccess(node: TSESTree.MemberExpression) { // This rule is so aggressive that can cause tons of false positives outside test files when Aggressive Reporting // is enabled. Because of that, this rule will skip this mechanism and report only if some Testing Library package @@ -99,94 +92,44 @@ export default createTestingLibraryRule({ } } - function detectTestingLibraryFn( - node: TSESTree.CallExpression, - variable: ScopeVariable | null + function getProperty( + node: TSESTree.PrivateIdentifier | TSESTree.Expression ) { - if (variable && variable.defs.length > 0) { - const def = variable.defs[0]; - if ( - def.type === DefinitionType.Variable && - isCallExpression(def.node.init) - ) { - return resolveToTestingLibraryFn(def.node.init, context); - } + if (isLiteral(node)) { + return node; } - return resolveToTestingLibraryFn(node, context); + return getDeepestIdentifierNode(node); } return { CallExpression(node: TSESTree.CallExpression) { - const property = getDeepestIdentifierNode(node); - const identifier = getPropertyIdentifierNode(node); - - const isEventHandlerMethod = EVENT_HANDLER_METHODS.some( - (method) => method === property?.name - ); - const hasUserEventInstanceName = userEventInstanceNames.has( - identifier?.name ?? '' - ); - - const variable = identifier - ? ASTUtils.findVariable(getScope(context, node), identifier) - : null; - const testingLibraryFn = detectTestingLibraryFn(node, variable); + if (!isMemberExpression(node.callee)) return; + const { callee } = node; if ( - !testingLibraryFn && - isEventHandlerMethod && - !hasUserEventInstanceName + !EVENT_HANDLER_METHODS.some( + (method) => method === ASTUtils.getPropertyName(callee) + ) ) { - context.report({ - node, - loc: property?.loc.start, - messageId: 'noNodeAccess', - }); - } - }, - VariableDeclarator(node: TSESTree.VariableDeclarator) { - const { init, id } = node; - - if (!isCallExpression(init)) { return; } + const identifier = getDeepestIdentifierNode(callee.object); if ( - !isMemberExpression(init.callee) || - !ASTUtils.isIdentifier(init.callee.object) + !identifier || + !ALL_QUERIES_COMBINATIONS.includes(identifier.name) ) { return; } - const testingLibraryFn = resolveToTestingLibraryFn(init, context); - if ( - init.callee.object.name === testingLibraryFn?.local && - ASTUtils.isIdentifier(init.callee.property) && - init.callee.property.name === 'setup' && - ASTUtils.isIdentifier(id) - ) { - userEventInstanceNames.add(id.name); - } - }, - AssignmentExpression(node: TSESTree.AssignmentExpression) { - if ( - ASTUtils.isIdentifier(node.left) && - isCallExpression(node.right) && - isMemberExpression(node.right.callee) && - ASTUtils.isIdentifier(node.right.callee.object) - ) { - const testingLibraryFn = resolveToTestingLibraryFn( - node.right, - context - ); - if ( - node.right.callee.object.name === testingLibraryFn?.local && - ASTUtils.isIdentifier(node.right.callee.property) && - node.right.callee.property.name === 'setup' - ) { - userEventInstanceNames.add(node.left.name); - } + if (resolveToTestingLibraryFn(node, context)) { + const property = getProperty(callee.property); + context.report({ + node, + loc: property?.loc.start, + messageId: 'noNodeAccess', + }); } }, 'ExpressionStatement MemberExpression': showErrorForNodeAccess, diff --git a/tests/lib/rules/no-node-access.test.ts b/tests/lib/rules/no-node-access.test.ts index 41c29b16..9b988d6e 100644 --- a/tests/lib/rules/no-node-access.test.ts +++ b/tests/lib/rules/no-node-access.test.ts @@ -341,6 +341,34 @@ ruleTester.run(RULE_NAME, rule, { }); `, }, + { + code: ` + import { screen } from '${testingFramework}'; + + class Hoge { + submit() {} + click() {} + } + + test('...', () => { + const pm = new Hoge(); + pm.click(); + pm.submit(); + });`, + }, + { + code: ` + import { user } from 'hoge' + import { screen } from '${testingFramework}'; + + test('...', () => { + const button = screen.getByRole('button'); + user.click(button) + user.select(button) + user.submit(button) + }) + `, + }, ] ), invalid: SUPPORTED_TESTING_FRAMEWORKS.flatMap((testingFramework) => [ @@ -584,38 +612,670 @@ ruleTester.run(RULE_NAME, rule, { code: ` import { screen } from '${testingFramework}'; - const button = document.getElementById('submit-btn').${method}(); + const dom = screen.getByLabelText('foo')['${method}'](); `, errors: [ { line: 4, - column: 33, + column: 50, messageId: 'noNodeAccess', }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByLabelText('foo').${method}(); + `, + errors: [ { line: 4, - column: 62, + column: 50, messageId: 'noNodeAccess', }, ], }, { - settings: { 'testing-library/utils-module': 'test-utils' }, code: ` - // case: custom module set but not imported using ${testingFramework} (aggressive reporting limited) - import { screen } from 'test-utils'; + import { screen } from '${testingFramework}'; + + const dom = screen.getByPlaceholderText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 56, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByPlaceholderText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 56, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 45, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 45, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByAltText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 48, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByAltText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 48, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByTitle('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 46, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByTitle('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 46, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByDisplayValue('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 53, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByDisplayValue('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 53, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByRole('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 45, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByRole('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 45, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByTestId('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.getByTestId('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByLabelText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 51, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByLabelText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 51, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByPlaceholderText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 57, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByPlaceholderText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 57, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; - const button = document.getElementById('submit-btn').${method}(); + const dom = screen.findByText('foo')['${method}'](); `, errors: [ { - line: 5, - column: 33, + line: 4, + column: 46, messageId: 'noNodeAccess', }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByText('foo').${method}(); + `, + errors: [ { - line: 5, - column: 62, + line: 4, + column: 46, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByAltText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 49, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByAltText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 49, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByTitle('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByTitle('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByDisplayValue('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 54, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByDisplayValue('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 54, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByRole('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 46, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByRole('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 46, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByTestId('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 48, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.findByTestId('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 48, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByLabelText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 52, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByLabelText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 52, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByPlaceholderText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 58, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByPlaceholderText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 58, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByAltText('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 50, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByAltText('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 50, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByTitle('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 48, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByTitle('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 48, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByDisplayValue('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 55, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByDisplayValue('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 55, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByRole('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByRole('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 47, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByTestId('foo')['${method}'](); + `, + errors: [ + { + line: 4, + column: 49, + messageId: 'noNodeAccess', + }, + ], + }, + { + code: ` + import { screen } from '${testingFramework}'; + + const dom = screen.queryByTestId('foo').${method}(); + `, + errors: [ + { + line: 4, + column: 49, messageId: 'noNodeAccess', }, ], diff --git a/tests/lib/utils/resolve-to-testing-library-fn.test.ts b/tests/lib/utils/resolve-to-testing-library-fn.test.ts index c8c026a9..92d0d645 100644 --- a/tests/lib/utils/resolve-to-testing-library-fn.test.ts +++ b/tests/lib/utils/resolve-to-testing-library-fn.test.ts @@ -60,6 +60,16 @@ ruleTester.run('esm', rule, { code: ` import { userEvent } from './test-utils'; + function userClick() { + userEvent.click(document.body); + } + userClick() + `, + }, + { + code: ` + import { userEvent } from './test-utils'; + function userClick() { userEvent.click(document.body); }