Skip to content

Commit

Permalink
feat(no-global-regexp-flag-in-query): Detect global RegExp in variabl…
Browse files Browse the repository at this point in the history
…e declarations (#678)

Closes #592

* fix: change function name to more informative and add JS Doc comment

* feat(no-global-regexp-flag-in-query): detect global regexp in variable declarations
  • Loading branch information
sjarva committed Oct 19, 2022
1 parent c3504a7 commit e2ea687
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 3 deletions.
75 changes: 72 additions & 3 deletions lib/rules/no-global-regexp-flag-in-query.ts
Expand Up @@ -37,7 +37,13 @@ export default createTestingLibraryRule<Options, MessageIds>({
},
defaultOptions: [],
create(context, _, helpers) {
function report(literalNode: TSESTree.Node) {
/**
* Checks if node is reportable (has a regex that contains 'g') and if it is, reports it with `context.report()`.
*
* @param literalNode Literal node under to be
* @returns {Boolean} indicatinf if literal was reported
*/
function reportLiteralWithRegex(literalNode: TSESTree.Node) {
if (
isLiteral(literalNode) &&
'regex' in literalNode &&
Expand Down Expand Up @@ -76,7 +82,37 @@ export default createTestingLibraryRule<Options, MessageIds>({
return [];
}

// Helper array to store variable nodes that have a literal with regex
// e.g. `const countRegExp = /count/gi` will be store here
const variableNodesWithRegexs: TSESTree.VariableDeclarator[] = [];

function hasRegexInVariable(
identifier: TSESTree.Identifier
): TSESTree.VariableDeclarator | undefined {
return variableNodesWithRegexs.find((varNode) => {
if (
ASTUtils.isVariableDeclarator(varNode) &&
ASTUtils.isIdentifier(varNode.id)
) {
return varNode.id.name === identifier.name;
}
return undefined;
});
}

return {
// internal helper function, helps store all variables with regex to `variableNodesWithRegexs`
// could potentially be refactored to using context.getDeclaredVariables()
VariableDeclarator(node: TSESTree.Node) {
if (
ASTUtils.isVariableDeclarator(node) &&
isLiteral(node.init) &&
'regex' in node.init &&
node.init.regex.flags.includes('g')
) {
variableNodesWithRegexs.push(node);
}
},
CallExpression(node) {
const identifierNode = getDeepestIdentifierNode(node);
if (!identifierNode || !helpers.isQuery(identifierNode)) {
Expand All @@ -85,11 +121,44 @@ export default createTestingLibraryRule<Options, MessageIds>({

const [firstArg, secondArg] = getArguments(identifierNode);

const firstArgumentHasError = report(firstArg);
const firstArgumentHasError = reportLiteralWithRegex(firstArg);
if (firstArgumentHasError) {
return;
}

// Case issue #592: a variable that has a regex is passed to testing library query

if (ASTUtils.isIdentifier(firstArg)) {
const regexVariableNode = hasRegexInVariable(firstArg);
if (regexVariableNode !== undefined) {
context.report({
node: firstArg,
messageId: 'noGlobalRegExpFlagInQuery',
fix(fixer) {
if (
ASTUtils.isVariableDeclarator(regexVariableNode) &&
isLiteral(regexVariableNode.init) &&
'regex' in regexVariableNode.init &&
regexVariableNode.init.regex.flags.includes('g')
) {
const splitter = regexVariableNode.init.raw.lastIndexOf('/');
const raw = regexVariableNode.init.raw.substring(0, splitter);
const flags = regexVariableNode.init.raw.substring(
splitter + 1
);
const flagsWithoutGlobal = flags.replace('g', '');

return fixer.replaceText(
regexVariableNode.init,
`${raw}/${flagsWithoutGlobal}`
);
}
return null;
},
});
}
}

if (isObjectExpression(secondArg)) {
const namePropertyNode = secondArg.properties.find(
(p) =>
Expand All @@ -100,7 +169,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
) as TSESTree.Property | undefined;

if (namePropertyNode) {
report(namePropertyNode.value);
reportLiteralWithRegex(namePropertyNode.value);
}
}
},
Expand Down
24 changes: 24 additions & 0 deletions tests/lib/rules/no-global-regexp-flag-in-query.test.ts
Expand Up @@ -196,5 +196,29 @@ ruleTester.run(RULE_NAME, rule, {
import { within } from '@testing-library/dom'
within(element).queryAllByText(/hello/i)`,
},
{
code: `
const countRegExp = /count/gm
const anotherRegExp = /something/mgi
expect(screen.getByText(countRegExp)).toBeInTheDocument()
expect(screen.getByAllText(anotherRegExp)).toBeInTheDocument()`,
errors: [
{
messageId: 'noGlobalRegExpFlagInQuery',
line: 4,
column: 28,
},
{
messageId: 'noGlobalRegExpFlagInQuery',
line: 5,
column: 31,
},
],
output: `
const countRegExp = /count/m
const anotherRegExp = /something/mi
expect(screen.getByText(countRegExp)).toBeInTheDocument()
expect(screen.getByAllText(anotherRegExp)).toBeInTheDocument()`,
},
],
});

0 comments on commit e2ea687

Please sign in to comment.