-
-
Notifications
You must be signed in to change notification settings - Fork 644
feat(label-has-associated-control): add option for enforcing label's htmlFor matches control's id #1042
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
michaelfaith
wants to merge
1
commit into
jsx-eslint:main
Choose a base branch
from
michaelfaith:feat/label-has-associated-control-option
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+689
−63
Open
feat(label-has-associated-control): add option for enforcing label's htmlFor matches control's id #1042
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* @flow | ||
*/ | ||
|
||
export type JSXFragmentMockType = { | ||
type: 'JSXFragment', | ||
children: Array<Node>, | ||
}; | ||
|
||
export default function JSXFragmentMock( | ||
children?: Array<Node> = [], | ||
): JSXFragmentMockType { | ||
return { | ||
type: 'JSXFragment', | ||
children, | ||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
import test from 'tape'; | ||
|
||
import getChildComponent from '../../../src/util/getChildComponent'; | ||
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock'; | ||
import JSXElementMock from '../../../__mocks__/JSXElementMock'; | ||
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock'; | ||
|
||
test('mayContainChildComponent', (t) => { | ||
t.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('span', [], []), | ||
JSXElementMock('span', [], [ | ||
JSXElementMock('span', [], []), | ||
JSXElementMock('span', [], [ | ||
JSXElementMock('span', [], []), | ||
]), | ||
]), | ||
]), | ||
JSXElementMock('span', [], []), | ||
JSXElementMock('img', [ | ||
JSXAttributeMock('src', 'some/path'), | ||
]), | ||
]), | ||
'FancyComponent', | ||
5, | ||
), | ||
undefined, | ||
'no FancyComponent returns undefined', | ||
); | ||
|
||
t.test('contains an indicated component', (st) => { | ||
const inputMock = JSXElementMock('input'); | ||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
inputMock, | ||
]), | ||
'input', | ||
), | ||
inputMock, | ||
'returns input', | ||
); | ||
|
||
const FancyComponentMock = JSXElementMock('FancyComponent'); | ||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
FancyComponentMock, | ||
]), | ||
'FancyComponent', | ||
), | ||
FancyComponentMock, | ||
'returns FancyComponent', | ||
); | ||
|
||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('FancyComponent'), | ||
]), | ||
]), | ||
'FancyComponent', | ||
), | ||
undefined, | ||
'FancyComponent is outside of default depth, should return undefined', | ||
); | ||
|
||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('div', [], [ | ||
FancyComponentMock, | ||
]), | ||
]), | ||
'FancyComponent', | ||
2, | ||
), | ||
FancyComponentMock, | ||
'FancyComponent is inside of custom depth, should return FancyComponent', | ||
); | ||
|
||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('span', [], []), | ||
JSXElementMock('span', [], [ | ||
JSXElementMock('span', [], []), | ||
JSXElementMock('span', [], [ | ||
JSXElementMock('span', [], [ | ||
JSXElementMock('span', [], [ | ||
FancyComponentMock, | ||
]), | ||
]), | ||
]), | ||
]), | ||
]), | ||
JSXElementMock('span', [], []), | ||
JSXElementMock('img', [ | ||
JSXAttributeMock('src', 'some/path'), | ||
]), | ||
]), | ||
'FancyComponent', | ||
6, | ||
), | ||
FancyComponentMock, | ||
'deep nesting, returns FancyComponent', | ||
); | ||
|
||
st.end(); | ||
}); | ||
|
||
const MysteryBox = JSXExpressionContainerMock('mysteryBox'); | ||
t.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
MysteryBox, | ||
]), | ||
'FancyComponent', | ||
), | ||
MysteryBox, | ||
'Indeterminate situations + expression container children - returns component', | ||
); | ||
|
||
t.test('Glob name matching - component name contains question mark ? - match any single character', (st) => { | ||
const FancyComponentMock = JSXElementMock('FancyComponent'); | ||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
FancyComponentMock, | ||
]), | ||
'Fanc?Co??onent', | ||
), | ||
FancyComponentMock, | ||
'returns component', | ||
); | ||
|
||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('FancyComponent'), | ||
]), | ||
'FancyComponent?', | ||
), | ||
undefined, | ||
'returns undefined', | ||
); | ||
|
||
st.test('component name contains asterisk * - match zero or more characters', (s2t) => { | ||
s2t.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
FancyComponentMock, | ||
]), | ||
'Fancy*', | ||
), | ||
FancyComponentMock, | ||
'returns component', | ||
); | ||
|
||
s2t.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
FancyComponentMock, | ||
]), | ||
'*Component', | ||
), | ||
FancyComponentMock, | ||
'returns component', | ||
); | ||
|
||
s2t.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
FancyComponentMock, | ||
]), | ||
'Fancy*C*t', | ||
), | ||
FancyComponentMock, | ||
'returns component', | ||
); | ||
|
||
s2t.end(); | ||
}); | ||
|
||
st.end(); | ||
}); | ||
|
||
t.test('using a custom elementType function', (st) => { | ||
const CustomInputMock = JSXElementMock('CustomInput'); | ||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
CustomInputMock, | ||
]), | ||
'input', | ||
2, | ||
() => 'input', | ||
), | ||
CustomInputMock, | ||
'returns the component when the custom elementType returns the proper name', | ||
); | ||
|
||
st.equal( | ||
getChildComponent( | ||
JSXElementMock('div', [], [ | ||
JSXElementMock('CustomInput'), | ||
]), | ||
'input', | ||
2, | ||
() => 'button', | ||
), | ||
undefined, | ||
'returns undefined when the custom elementType returns a wrong name', | ||
); | ||
|
||
st.end(); | ||
}); | ||
|
||
t.end(); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import test from 'tape'; | ||
|
||
import getParentElement from '../../../src/util/getParentElement'; | ||
import JSXElementMock from '../../../__mocks__/JSXElementMock'; | ||
import JSXFragmentMock from '../../../__mocks__/JSXFragmentMock'; | ||
|
||
test('getParentElement', (t) => { | ||
const MockElement = JSXElementMock('input', [], []); | ||
|
||
t.test('new context api', (st) => { | ||
st.test('no parent', (st2) => { | ||
const mockContext = { | ||
sourceCode: { | ||
getAncestors: () => [], | ||
}, | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
undefined, | ||
'no parent -> returns undefined', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.test('one parent element', (st2) => { | ||
const MockParentElement = JSXElementMock('div', [], [MockElement]); | ||
|
||
const mockContext = { | ||
sourceCode: { | ||
getAncestors: () => [MockParentElement], | ||
}, | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
MockParentElement, | ||
'one parent JSXElement -> returns parent', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.test('one parent fragment', (st2) => { | ||
const MockParentFragment = JSXFragmentMock([MockElement]); | ||
|
||
const mockContext = { | ||
sourceCode: { | ||
getAncestors: () => [MockParentFragment], | ||
}, | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
MockParentFragment, | ||
'one parent JSXFragment -> returns parent', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.test('multiple ancestors', (st2) => { | ||
const MockParentElement = JSXElementMock('div', [], [MockElement]); | ||
const MockUncleElement = JSXElementMock('div', [], []); | ||
const MockGrandparentFragment = JSXFragmentMock([MockParentElement, MockUncleElement]); | ||
|
||
const mockContext = { | ||
sourceCode: { | ||
getAncestors: () => [MockGrandparentFragment, MockParentElement], | ||
}, | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
MockParentElement, | ||
'multiple ancestors -> returns direct parent', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.end(); | ||
}); | ||
|
||
t.test('legacy context', (st) => { | ||
st.test('no parent', (st2) => { | ||
const mockContext = { | ||
getAncestors: () => [], | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
undefined, | ||
'no parent -> returns undefined', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.test('one parent element', (st2) => { | ||
const MockParentElement = JSXElementMock('div', [], [MockElement]); | ||
|
||
const mockContext = { | ||
getAncestors: () => [MockParentElement], | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
MockParentElement, | ||
'one parent JSXElement -> returns parent', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.test('one parent fragment', (st2) => { | ||
const MockParentFragment = JSXFragmentMock([MockElement]); | ||
|
||
const mockContext = { | ||
getAncestors: () => [MockParentFragment], | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
MockParentFragment, | ||
'one parent JSXFragment -> returns parent', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.test('multiple ancestors', (st2) => { | ||
const MockParentElement = JSXElementMock('div', [], [MockElement]); | ||
const MockUncleElement = JSXElementMock('div', [], []); | ||
const MockGrandparentFragment = JSXFragmentMock([MockParentElement, MockUncleElement]); | ||
|
||
const mockContext = { | ||
getAncestors: () => [MockGrandparentFragment, MockParentElement], | ||
}; | ||
|
||
st2.equal( | ||
getParentElement( | ||
MockElement, | ||
mockContext, | ||
), | ||
MockParentElement, | ||
'multiple ancestors -> returns direct parent', | ||
); | ||
|
||
st2.end(); | ||
}); | ||
|
||
st.end(); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,9 +3,24 @@ | |
*/ | ||
import type { | ||
JSXAttribute, | ||
JSXElement, | ||
JSXOpeningElement, | ||
} from 'ast-types-flow'; | ||
|
||
export type ESLintJSXAttribute = { | ||
parent: JSXOpeningElement | ||
} & JSXAttribute; | ||
|
||
type JSXOpeningFragment = { | ||
type: 'JSXOpeningFragment', | ||
}; | ||
|
||
type JSXClosingFragment = { | ||
type: 'JSXClosingFragment', | ||
}; | ||
|
||
export type JSXFragment = JSXElement & { | ||
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. The |
||
type: 'JSXFragment', | ||
openingFragment: JSXOpeningFragment, | ||
closingFragment: JSXClosingFragment, | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/** | ||
* @flow | ||
*/ | ||
|
||
import type { | ||
JSXElement, JSXExpressionContainer, JSXOpeningElement, Node, | ||
} from 'ast-types-flow'; | ||
import { elementType as rawElementType } from 'jsx-ast-utils'; | ||
import minimatch from 'minimatch'; | ||
|
||
export default function getChildComponent( | ||
root: Node, | ||
componentName: string, | ||
maxDepth: number = 1, | ||
elementType: ((node: JSXOpeningElement) => string) = rawElementType, | ||
): JSXElement | JSXExpressionContainer | void { | ||
function traverseChildren( | ||
node: Node, | ||
depth: number, | ||
): JSXElement | JSXExpressionContainer | void { | ||
// Bail when maxDepth is exceeded. | ||
if (depth > maxDepth) { | ||
return undefined; | ||
} | ||
if (node.children) { | ||
/* $FlowFixMe */ | ||
for (let i = 0; i < node.children.length; i += 1) { | ||
/* $FlowFixMe */ | ||
const childNode = node.children[i]; | ||
// Assume an expression container satisfies our conditions. It is the best we can | ||
// do in this case. | ||
if (childNode.type === 'JSXExpressionContainer') { | ||
return childNode; | ||
} | ||
// Check for components with the provided name. | ||
if ( | ||
childNode.type === 'JSXElement' | ||
&& childNode.openingElement | ||
&& minimatch(elementType(childNode.openingElement), componentName) | ||
) { | ||
return childNode; | ||
} | ||
const grandChild = traverseChildren(childNode, depth + 1); | ||
if (grandChild) { | ||
return grandChild; | ||
} | ||
} | ||
} | ||
return undefined; | ||
} | ||
return traverseChildren(root, 1); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* Given a node and the eslint context, return the node's parent element, | ||
* but only if it's a JSXElement or JSXFragment. | ||
* | ||
* @flow | ||
*/ | ||
|
||
import type { JSXElement, Node } from 'ast-types-flow'; | ||
|
||
import type { ESLintContext } from '../../flow/eslint'; | ||
import type { JSXFragment } from '../../flow/eslint-jsx'; | ||
|
||
const getParentElement = (node: Node, context: ESLintContext): JSXElement | JSXFragment | void => { | ||
const ancestors = context.sourceCode?.getAncestors?.(node) || context.getAncestors?.() || []; | ||
if (ancestors.length === 0) { | ||
return undefined; | ||
} | ||
const parent = ancestors[ancestors.length - 1]; | ||
if (parent.type === 'JSXElement' || parent.type === 'JSXFragment') { | ||
return parent; | ||
} | ||
return undefined; | ||
}; | ||
|
||
export default getParentElement; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,19 @@ | ||
/** | ||
* Returns true if it can positively determine that the element lacks an | ||
* accessible label. If no determination is possible, it returns false. Treat | ||
* false as an unknown value. The element might still have an accessible label, | ||
* but this module cannot determine it positively. | ||
* Returns true if it can positively determine that the element has a | ||
* child element matching the elementType matching function. | ||
* | ||
* @flow | ||
*/ | ||
|
||
import type { JSXOpeningElement, Node } from 'ast-types-flow'; | ||
import { elementType as rawElementType } from 'jsx-ast-utils'; | ||
import minimatch from 'minimatch'; | ||
import getChildComponent from './getChildComponent'; | ||
|
||
export default function mayContainChildComponent( | ||
root: Node, | ||
componentName: string, | ||
maxDepth: number = 1, | ||
elementType: ((node: JSXOpeningElement) => string) = rawElementType, | ||
): boolean { | ||
function traverseChildren( | ||
node: Node, | ||
depth: number, | ||
): boolean { | ||
// Bail when maxDepth is exceeded. | ||
if (depth > maxDepth) { | ||
return false; | ||
} | ||
if (node.children) { | ||
/* $FlowFixMe */ | ||
for (let i = 0; i < node.children.length; i += 1) { | ||
/* $FlowFixMe */ | ||
const childNode = node.children[i]; | ||
// Assume an expression container renders a label. It is the best we can | ||
// do in this case. | ||
if (childNode.type === 'JSXExpressionContainer') { | ||
return true; | ||
} | ||
// Check for components with the provided name. | ||
if ( | ||
childNode.type === 'JSXElement' | ||
&& childNode.openingElement | ||
&& minimatch(elementType(childNode.openingElement), componentName) | ||
) { | ||
return true; | ||
} | ||
if (traverseChildren(childNode, depth + 1)) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
return traverseChildren(root, 1); | ||
return !!getChildComponent(root, componentName, maxDepth, elementType); | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
It would have been better to use
Omit
here to removeopeningElement
andclosingElement
fromJSXElement
, but this version of flow doesn't haveOmit
(that was introduced in 0.211.0). I'd be up for upgrading the version of flow in a separate change if you'd like?