-
Notifications
You must be signed in to change notification settings - Fork 147
Move rules settings to ESLint shared config: refactor no-debug rule #293
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
Changes from all commits
a86ddf0
bf84c07
02a8a7b
1593c38
9861eb4
06e5986
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -392,7 +392,7 @@ export function getPropertyIdentifierNode( | |
} | ||
|
||
/** | ||
* Gets the deepest identifier node from a given node. | ||
* Gets the deepest identifier node in the expression from a given node. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarifying the scope for getting the deepest identifier. |
||
* | ||
* Opposite of {@link getReferenceNode} | ||
* | ||
|
@@ -416,11 +416,15 @@ export function getDeepestIdentifierNode( | |
return getDeepestIdentifierNode(node.callee); | ||
} | ||
|
||
if (ASTUtils.isAwaitExpression(node)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I discovered this case thanks to existing tests from |
||
return getDeepestIdentifierNode(node.argument); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Gets the farthest node from a given node. | ||
* Gets the farthest node in the expression from a given node. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarifying the scope for getting the farthest identifier. |
||
* | ||
* Opposite of {@link getDeepestIdentifierNode} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,18 @@ | ||
import { | ||
ESLintUtils, | ||
TSESTree, | ||
ASTUtils, | ||
} from '@typescript-eslint/experimental-utils'; | ||
import { | ||
getDocsUrl, | ||
LIBRARY_MODULES, | ||
hasTestingLibraryImportModule, | ||
} from '../utils'; | ||
import { | ||
getDeepestIdentifierNode, | ||
getPropertyIdentifierNode, | ||
getReferenceNode, | ||
isObjectPattern, | ||
isProperty, | ||
isCallExpression, | ||
isLiteral, | ||
isMemberExpression, | ||
isImportSpecifier, | ||
isRenderVariableDeclarator, | ||
} from '../node-utils'; | ||
import { createTestingLibraryRule } from '../create-testing-library-rule'; | ||
import { ASTUtils, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
|
||
export const RULE_NAME = 'no-debug'; | ||
export type MessageIds = 'noDebug'; | ||
type Options = [{ renderFunctions?: string[] }]; | ||
type Options = []; | ||
|
||
export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({ | ||
export default createTestingLibraryRule<Options, MessageIds>({ | ||
name: RULE_NAME, | ||
meta: { | ||
type: 'problem', | ||
|
@@ -46,154 +36,67 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({ | |
}, | ||
], | ||
}, | ||
defaultOptions: [ | ||
{ | ||
renderFunctions: [], | ||
}, | ||
], | ||
|
||
create(context, [options]) { | ||
let hasDestructuredDebugStatement = false; | ||
const renderVariableDeclarators: TSESTree.VariableDeclarator[] = []; | ||
|
||
const { renderFunctions } = options; | ||
defaultOptions: [], | ||
|
||
let hasImportedScreen = false; | ||
let wildcardImportName: string = null; | ||
create(context, [], helpers) { | ||
const suspiciousDebugVariableNames: string[] = []; | ||
const suspiciousReferenceNodes: TSESTree.Identifier[] = []; | ||
|
||
return { | ||
VariableDeclarator(node) { | ||
if (isRenderVariableDeclarator(node, ['render', ...renderFunctions])) { | ||
if ( | ||
isObjectPattern(node.id) && | ||
node.id.properties.some( | ||
(property) => | ||
isProperty(property) && | ||
ASTUtils.isIdentifier(property.key) && | ||
property.key.name === 'debug' | ||
) | ||
) { | ||
hasDestructuredDebugStatement = true; | ||
} | ||
|
||
if (node.id.type === 'Identifier') { | ||
renderVariableDeclarators.push(node); | ||
} | ||
} | ||
}, | ||
[`VariableDeclarator > CallExpression > Identifier[name="require"]`]( | ||
node: TSESTree.Identifier | ||
) { | ||
const { arguments: args } = node.parent as TSESTree.CallExpression; | ||
const initIdentifierNode = getDeepestIdentifierNode(node.init); | ||
|
||
const literalNodeScreenModuleName = args.find( | ||
(args) => | ||
isLiteral(args) && | ||
typeof args.value === 'string' && | ||
LIBRARY_MODULES.includes(args.value) | ||
); | ||
|
||
if (!literalNodeScreenModuleName) { | ||
if (!helpers.isRenderUtil(initIdentifierNode)) { | ||
return; | ||
} | ||
|
||
const declaratorNode = node.parent | ||
.parent as TSESTree.VariableDeclarator; | ||
|
||
hasImportedScreen = | ||
isObjectPattern(declaratorNode.id) && | ||
declaratorNode.id.properties.some( | ||
(property) => | ||
// find debug obtained from render and save their name, like: | ||
// const { debug } = render(); | ||
if (isObjectPattern(node.id)) { | ||
for (const property of node.id.properties) { | ||
if ( | ||
isProperty(property) && | ||
ASTUtils.isIdentifier(property.key) && | ||
property.key.name === 'screen' | ||
); | ||
}, | ||
// checks if import has shape: | ||
// import { screen } from '@testing-library/dom'; | ||
ImportDeclaration(node: TSESTree.ImportDeclaration) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's really pleasant to remove this unnecessary checks 😌 |
||
if (!hasTestingLibraryImportModule(node)) { | ||
return; | ||
} | ||
|
||
hasImportedScreen = node.specifiers.some( | ||
(s) => isImportSpecifier(s) && s.imported.name === 'screen' | ||
); | ||
}, | ||
// checks if import has shape: | ||
// import * as dtl from '@testing-library/dom'; | ||
'ImportDeclaration ImportNamespaceSpecifier'( | ||
node: TSESTree.ImportNamespaceSpecifier | ||
) { | ||
const importDeclarationNode = node.parent as TSESTree.ImportDeclaration; | ||
if (!hasTestingLibraryImportModule(importDeclarationNode)) { | ||
return; | ||
property.key.name === 'debug' | ||
) { | ||
suspiciousDebugVariableNames.push( | ||
getDeepestIdentifierNode(property.value).name | ||
); | ||
} | ||
} | ||
} | ||
|
||
wildcardImportName = node.local && node.local.name; | ||
}, | ||
[`CallExpression > Identifier[name="debug"]`](node: TSESTree.Identifier) { | ||
if (hasDestructuredDebugStatement) { | ||
context.report({ | ||
node, | ||
messageId: 'noDebug', | ||
}); | ||
// find utils kept from render and save their node, like: | ||
// const utils = render(); | ||
if (ASTUtils.isIdentifier(node.id)) { | ||
suspiciousReferenceNodes.push(node.id); | ||
} | ||
}, | ||
[`CallExpression > MemberExpression > Identifier[name="debug"]`]( | ||
node: TSESTree.Identifier | ||
) { | ||
const memberExpression = node.parent as TSESTree.MemberExpression; | ||
const identifier = memberExpression.object as TSESTree.Identifier; | ||
const memberExpressionName = identifier.name; | ||
/* | ||
check if `debug` used following the pattern: | ||
|
||
import { screen } from '@testing-library/dom'; | ||
... | ||
screen.debug(); | ||
*/ | ||
const isScreenDebugUsed = | ||
hasImportedScreen && memberExpressionName === 'screen'; | ||
|
||
/* | ||
check if `debug` used following the pattern: | ||
|
||
import * as dtl from '@testing-library/dom'; | ||
... | ||
dtl.debug(); | ||
*/ | ||
const isNamespaceDebugUsed = | ||
wildcardImportName && memberExpressionName === wildcardImportName; | ||
CallExpression(node) { | ||
const callExpressionIdentifier = getDeepestIdentifierNode(node); | ||
const referenceNode = getReferenceNode(node); | ||
const referenceIdentifier = getPropertyIdentifierNode(referenceNode); | ||
|
||
const isDebugUtil = helpers.isDebugUtil(callExpressionIdentifier); | ||
const isDeclaredDebugVariable = suspiciousDebugVariableNames.includes( | ||
callExpressionIdentifier.name | ||
); | ||
const isChainedReferenceDebug = suspiciousReferenceNodes.some( | ||
(suspiciousReferenceIdentifier) => { | ||
return ( | ||
callExpressionIdentifier.name === 'debug' && | ||
suspiciousReferenceIdentifier.name === referenceIdentifier.name | ||
); | ||
} | ||
); | ||
|
||
if (isScreenDebugUsed || isNamespaceDebugUsed) { | ||
if (isDebugUtil || isDeclaredDebugVariable || isChainedReferenceDebug) { | ||
context.report({ | ||
node, | ||
node: callExpressionIdentifier, | ||
messageId: 'noDebug', | ||
}); | ||
} | ||
}, | ||
'Program:exit'() { | ||
renderVariableDeclarators.forEach((renderVar) => { | ||
const renderVarReferences = context | ||
.getDeclaredVariables(renderVar)[0] | ||
.references.slice(1); | ||
renderVarReferences.forEach((ref) => { | ||
const parent = ref.identifier.parent; | ||
if ( | ||
isMemberExpression(parent) && | ||
ASTUtils.isIdentifier(parent.property) && | ||
parent.property.name === 'debug' && | ||
isCallExpression(parent.parent) | ||
) { | ||
context.report({ | ||
node: parent.property, | ||
messageId: 'noDebug', | ||
}); | ||
} | ||
}); | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM, I just wonder if we should mention that it can be configured via settings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm leaving all the documentation stuff for the last step after I've migrated all the rules. I'll mention the custom render settings in the README, but not sure if we should mention it on each rule depending on
render
(which is what I understand you are suggesting).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, correct. While I was writing it I was also considering if it's worth it to mention in for every rule or not 😅.
I think having a global doc is good.