forked from desktop/desktop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbuttonGroupOrderRule.ts
131 lines (111 loc) · 3.72 KB
/
buttonGroupOrderRule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* button-group-order
*
* This custom tslint rule is highly specific to GitHub Desktop and attempts
* to enforce a consistent order for buttons inside of a <ButtonGroup>
* component.
*
* Example
*
* <ButtonGroup>
* <Button>Cancel</Button>
* <Button type='submit'>Ok</Button>
* </ButtonGroup>
*
* The example above will trigger a tslint error since we want to enforce
* a consistent order of Ok/Cancel-style buttons (the button captions vary)
* such that the primary action precedes any secondary actions.
*
* See https://www.nngroup.com/articles/ok-cancel-or-cancel-ok/
*
* We've opted for using the Windows order of OK, Cancel in our codebase, the
* actual order at runtime will vary depending on platform.
*
*/
import * as ts from 'typescript'
import * as Lint from 'tslint'
class ButtonGroupOrderWalker extends Lint.RuleWalker {
/**
* Visit the node and ensure any button children are in the correct order.
*/
protected visitJsxElement(node: ts.JsxElement): void {
super.visitJsxElement(node)
if (node.openingElement.tagName.getText() !== 'ButtonGroup') {
return
}
const buttons = new Array<ts.JsxOpeningLikeElement>()
// Assert that only <Button> elements and whitespace are allowed inside
// the ButtonGroup.
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (child.kind === ts.SyntaxKind.JsxText) {
// Whitespace is okay.
if (/^\s*$/.test(child.getText())) {
continue
}
} else if (child.kind === ts.SyntaxKind.JsxElement) {
if (child.openingElement.tagName.getText() === 'Button') {
buttons.push(child.openingElement)
continue
}
} else if (child.kind === ts.SyntaxKind.JsxSelfClosingElement) {
if (child.tagName.getText() === 'Button') {
buttons.push(child)
continue
}
}
const start = child.getStart()
const width = child.getWidth()
const error = `Forbidden child content, expected <Button>.`
const explanation = 'ButtonGroups should only contain <Button> elements'
const message = `${error} ${explanation}`
this.addFailure(this.createFailure(start, width, message))
}
// If we've emitted any errors we'll bail here rather than try to emit
// any errors with button order.
if (this.getFailures().length) {
return
}
if (buttons.length < 2) {
return
}
const buttonsWithTypeAttr = buttons.map(b => {
const typeAttr = b.attributes.properties.find(
a =>
a.kind === ts.SyntaxKind.JsxAttribute && a.name.getText() === 'type'
) as ts.JsxAttribute | undefined
let value = undefined
if (
typeAttr &&
typeAttr.initializer &&
typeAttr.initializer.kind === ts.SyntaxKind.StringLiteral
) {
value = typeAttr.initializer.text
}
return [b, value]
})
const primaryButtonIx = buttonsWithTypeAttr.findIndex(
x => x[1] === 'submit'
)
if (primaryButtonIx !== -1 && primaryButtonIx !== 0) {
const start = node.getStart()
const width = node.getWidth()
const error = `Wrong button order in ButtonGroup.`
const explanation =
'ButtonGroups should have the primary button as its first child'
const message = `${error} ${explanation}`
this.addFailure(this.createFailure(start, width, message))
}
}
}
export class Rule extends Lint.Rules.AbstractRule {
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
return this.applyWithWalker(
new ButtonGroupOrderWalker(sourceFile, this.getOptions())
)
} else {
return []
}
}
}