-
Notifications
You must be signed in to change notification settings - Fork 147
Move rules settings to ESLint shared config: part 2 - check imports #239
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
d2844a1
824150d
4b16c7b
4f8e43f
e9d7bc3
476b7f6
400d343
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 |
---|---|---|
@@ -1,7 +1,31 @@ | ||
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
|
||
export type TestingLibrarySettings = { | ||
'testing-library/module'?: string; | ||
}; | ||
|
||
export type TestingLibraryContext< | ||
TOptions extends readonly unknown[], | ||
TMessageIds extends string | ||
> = Readonly< | ||
TSESLint.RuleContext<TMessageIds, TOptions> & { | ||
settings: TestingLibrarySettings; | ||
} | ||
>; | ||
|
||
export type EnhancedRuleCreate< | ||
TOptions extends readonly unknown[], | ||
TMessageIds extends string, | ||
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener | ||
> = ( | ||
context: TestingLibraryContext<TOptions, TMessageIds>, | ||
optionsWithDefault: Readonly<TOptions>, | ||
detectionHelpers: Readonly<DetectionHelpers> | ||
) => TRuleListener; | ||
|
||
export type DetectionHelpers = { | ||
getIsImportingTestingLibrary: () => boolean; | ||
getIsTestingLibraryImported: () => boolean; | ||
canReportErrors: () => boolean; | ||
}; | ||
|
||
/** | ||
|
@@ -11,50 +35,91 @@ export function detectTestingLibraryUtils< | |
TOptions extends readonly unknown[], | ||
TMessageIds extends string, | ||
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener | ||
>( | ||
ruleCreate: ( | ||
context: Readonly<TSESLint.RuleContext<TMessageIds, TOptions>>, | ||
optionsWithDefault: Readonly<TOptions>, | ||
detectionHelpers: Readonly<DetectionHelpers> | ||
) => TRuleListener | ||
) { | ||
>(ruleCreate: EnhancedRuleCreate<TOptions, TMessageIds, TRuleListener>) { | ||
return ( | ||
context: Readonly<TSESLint.RuleContext<TMessageIds, TOptions>>, | ||
context: TestingLibraryContext<TOptions, TMessageIds>, | ||
optionsWithDefault: Readonly<TOptions> | ||
): TRuleListener => { | ||
let isImportingTestingLibrary = false; | ||
): TSESLint.RuleListener => { | ||
let isImportingTestingLibraryModule = false; | ||
let isImportingCustomModule = false; | ||
|
||
// TODO: init here options based on shared ESLint config | ||
// Init options based on shared ESLint settings | ||
const customModule = context.settings['testing-library/module']; | ||
|
||
// helpers for Testing Library detection | ||
// Helpers for Testing Library detection. | ||
const helpers: DetectionHelpers = { | ||
getIsImportingTestingLibrary() { | ||
return isImportingTestingLibrary; | ||
/** | ||
* Gets if Testing Library is considered as imported or not. | ||
* | ||
* By default, it is ALWAYS considered as imported. This is what we call | ||
* "aggressive reporting" so we don't miss TL utils reexported from | ||
* custom modules. | ||
* | ||
* However, there is a setting to customize the module where TL utils can | ||
* be imported from: "testing-library/module". If this setting is enabled, | ||
* then this method will return `true` ONLY IF a testing-library package | ||
* or custom module are imported. | ||
*/ | ||
getIsTestingLibraryImported() { | ||
Belco90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!customModule) { | ||
return true; | ||
} | ||
|
||
return isImportingTestingLibraryModule || isImportingCustomModule; | ||
}, | ||
|
||
/** | ||
* Wraps all conditions that must be met to report rules. | ||
*/ | ||
canReportErrors() { | ||
return this.getIsTestingLibraryImported(); | ||
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 been a long time since I've seen a |
||
}, | ||
}; | ||
|
||
// instructions for Testing Library detection | ||
// Instructions for Testing Library detection. | ||
const detectionInstructions: TSESLint.RuleListener = { | ||
/** | ||
* This ImportDeclaration rule listener will check if Testing Library related | ||
* modules are loaded. Since imports happen first thing in a file, it's | ||
* safe to use `isImportingTestingLibraryModule` and `isImportingCustomModule` | ||
* since they will have corresponding value already updated when reporting other | ||
* parts of the file. | ||
*/ | ||
ImportDeclaration(node: TSESTree.ImportDeclaration) { | ||
isImportingTestingLibrary = /testing-library/g.test( | ||
node.source.value as string | ||
); | ||
if (!isImportingTestingLibraryModule) { | ||
// check only if testing library import not found yet so we avoid | ||
// to override isImportingTestingLibraryModule after it's found | ||
isImportingTestingLibraryModule = /testing-library/g.test( | ||
node.source.value as string | ||
); | ||
} | ||
|
||
if (!isImportingCustomModule) { | ||
// check only if custom module import not found yet so we avoid | ||
// to override isImportingCustomModule after it's found | ||
const importName = String(node.source.value); | ||
isImportingCustomModule = importName.endsWith(customModule); | ||
} | ||
}, | ||
}; | ||
|
||
// update given rule to inject Testing Library detection | ||
const ruleInstructions = ruleCreate(context, optionsWithDefault, helpers); | ||
const enhancedRuleInstructions = Object.assign({}, ruleInstructions); | ||
const enhancedRuleInstructions: TSESLint.RuleListener = {}; | ||
|
||
const allKeys = new Set( | ||
Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)) | ||
); | ||
|
||
Object.keys(detectionInstructions).forEach((instruction) => { | ||
(enhancedRuleInstructions as TSESLint.RuleListener)[instruction] = ( | ||
node | ||
) => { | ||
// Iterate over ALL instructions keys so we can override original rule instructions | ||
// to prevent their execution if conditions to report errors are not met. | ||
allKeys.forEach((instruction) => { | ||
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. Now I iterate through all keys to be able to prevent original rule instructions. |
||
enhancedRuleInstructions[instruction] = (node) => { | ||
if (instruction in detectionInstructions) { | ||
detectionInstructions[instruction](node); | ||
} | ||
|
||
if (ruleInstructions[instruction]) { | ||
if (helpers.canReportErrors() && ruleInstructions[instruction]) { | ||
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. Here is where the plugin prevents original rule instructions to be executed if conditions are not met! 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'm a bit lost in understanding what's the difference between the method 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. in addition to that, if I understand correctly, we would just execute the rules if TL is imported, otherwise, nothing gets executed - is that correct? 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'm anticipating next PR with
Yes, but by default TL is always considered as imported due to the "aggressive reporting" mode. |
||
return ruleInstructions[instruction](node); | ||
} | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,11 +25,10 @@ export default createTestingLibraryRule<Options, MessageIds>({ | |
}, | ||
defaultOptions: [], | ||
|
||
create: (context, _, helpers) => { | ||
create(context) { | ||
function showErrorForNodeAccess(node: TSESTree.MemberExpression) { | ||
isIdentifier(node.property) && | ||
ALL_RETURNING_NODES.includes(node.property.name) && | ||
helpers.getIsImportingTestingLibrary() && | ||
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. This is not necessary anymore! ✨ |
||
context.report({ | ||
node: node, | ||
loc: node.property.loc.start, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { createRuleTester } from './lib/test-utils'; | ||
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 added this test file to check both |
||
import rule, { RULE_NAME } from './fake-rule'; | ||
|
||
const ruleTester = createRuleTester({ | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}); | ||
|
||
ruleTester.run(RULE_NAME, rule, { | ||
valid: [ | ||
{ | ||
code: ` | ||
// case: nothing related to Testing Library at all | ||
import { shallow } from 'enzyme'; | ||
|
||
const wrapper = shallow(<MyComponent />); | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
// case: render imported from other than custom module | ||
import { render } from '@somewhere/else' | ||
|
||
const utils = render(); | ||
`, | ||
settings: { | ||
'testing-library/module': 'test-utils', | ||
}, | ||
}, | ||
{ | ||
code: ` | ||
// case: prevent import which should trigger an error since it's imported | ||
// from other than custom module | ||
import { foo } from 'report-me' | ||
`, | ||
settings: { | ||
'testing-library/module': 'test-utils', | ||
}, | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
// case: import module forced to be reported | ||
import { foo } from 'report-me' | ||
`, | ||
errors: [{ line: 3, column: 7, messageId: 'fakeError' }], | ||
}, | ||
{ | ||
code: ` | ||
// case: render imported from any module by default (aggressive reporting) | ||
import { render } from '@somewhere/else' | ||
import { somethingElse } from 'another-module' | ||
|
||
const utils = render(); | ||
`, | ||
errors: [ | ||
{ | ||
line: 6, | ||
column: 21, | ||
messageId: 'fakeError', | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
// case: render imported from Testing Library module | ||
import { render } from '@testing-library/react' | ||
import { somethingElse } from 'another-module' | ||
|
||
const utils = render(); | ||
`, | ||
errors: [ | ||
{ | ||
line: 6, | ||
column: 21, | ||
messageId: 'fakeError', | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
// case: render imported from config custom module | ||
import { render } from 'test-utils' | ||
import { somethingElse } from 'another-module' | ||
|
||
const utils = render(); | ||
`, | ||
settings: { | ||
'testing-library/module': 'test-utils', | ||
}, | ||
errors: [ | ||
{ | ||
line: 6, | ||
column: 21, | ||
messageId: 'fakeError', | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
// case: render imported from Testing Library module if | ||
// custom module setup | ||
import { render } from '@testing-library/react' | ||
import { somethingElse } from 'another-module' | ||
|
||
const utils = render(); | ||
`, | ||
settings: { | ||
'testing-library/module': 'test-utils', | ||
}, | ||
errors: [ | ||
{ | ||
line: 7, | ||
column: 21, | ||
messageId: 'fakeError', | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/** | ||
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. Fake rule trying to report stuff asap to be able to run the previous test file. |
||
* @file Fake rule to be able to test createTestingLibraryRule and | ||
* detectTestingLibraryUtils properly | ||
*/ | ||
import { TSESTree } from '@typescript-eslint/experimental-utils'; | ||
import { createTestingLibraryRule } from '../lib/create-testing-library-rule'; | ||
|
||
export const RULE_NAME = 'fake-rule'; | ||
type Options = []; | ||
type MessageIds = 'fakeError'; | ||
|
||
export default createTestingLibraryRule<Options, MessageIds>({ | ||
name: RULE_NAME, | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Fake rule to test rule maker and detection helpers', | ||
category: 'Possible Errors', | ||
recommended: false, | ||
}, | ||
messages: { | ||
fakeError: 'fake error reported', | ||
}, | ||
fixable: null, | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
const reportRenderIdentifier = (node: TSESTree.Identifier) => { | ||
if (node.name === 'render') { | ||
context.report({ | ||
node, | ||
messageId: 'fakeError', | ||
}); | ||
} | ||
}; | ||
|
||
const checkImportDeclaration = (node: TSESTree.ImportDeclaration) => { | ||
// This is just to check that defining an `ImportDeclaration` doesn't | ||
// override `ImportDeclaration` from `detectTestingLibraryUtils` | ||
|
||
if (node.source.value === 'report-me') { | ||
context.report({ | ||
node, | ||
messageId: 'fakeError', | ||
}); | ||
} | ||
}; | ||
|
||
return { | ||
'CallExpression Identifier': reportRenderIdentifier, | ||
ImportDeclaration: checkImportDeclaration, | ||
}; | ||
}, | ||
}); |
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.
I've replaced returned type from
TRuleListener
toTSESLint.RuleListener
sincedetectTestingLibraryUtils
modifies the received rules listener to add or remove some so it can end being a different set of rule listener.