-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathrelayOperationGenericsRule.ts
172 lines (153 loc) · 5.83 KB
/
relayOperationGenericsRule.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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import * as GraphQL from "graphql"
import * as Lint from "tslint"
import * as ts from "typescript"
export class Rule extends Lint.Rules.AbstractRule {
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new RelayOperationGenericsWalker(sourceFile, this.getOptions()))
}
}
class RelayOperationGenericsWalker extends Lint.RuleWalker {
imports: ts.ImportDeclaration[] = []
visitImportDeclaration(node: ts.ImportDeclaration) {
this.imports.push(node)
super.visitImportDeclaration(node)
}
visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement) {
if (node.tagName.getText() === "QueryRenderer") {
for (const property of node.attributes.properties) {
if (
property.kind === ts.SyntaxKind.JsxAttribute &&
property.name.getText() === "query" &&
property.initializer
) {
const initializer = property.initializer
if (initializer.kind === ts.SyntaxKind.JsxExpression) {
const expression = initializer.expression
if (expression) {
this.visitOperationConfiguration(node, expression, node.tagName)
}
} else {
this.addFailureAtNode(initializer, "expected a graphql`…` tagged-template expression")
}
break
}
}
}
super.visitJsxSelfClosingElement(node)
}
visitCallExpression(node: ts.CallExpression) {
const functionName = node.expression as ts.Identifier
if (functionName.text === "commitMutation") {
const config = node.arguments[1] as undefined | ts.ObjectLiteralExpression
if (config && config.kind === ts.SyntaxKind.ObjectLiteralExpression) {
for (const property of config.properties) {
if (property.name && property.name.getText() === "mutation") {
if (property.kind === ts.SyntaxKind.PropertyAssignment) {
this.visitOperationConfiguration(node, property.initializer, functionName)
} else {
// TODO: Need to expand parsing if we want to support e.g.
// short-hand property assignment.
this.addFailureAtNode(property, "use traditional assignment for mutation query")
}
break
}
}
}
}
super.visitCallExpression(node)
}
visitOperationConfiguration(
node: ts.CallExpression | ts.JsxSelfClosingElement,
expression: ts.Expression,
functionOrTagName: ts.Node,
) {
const taggedTemplate = expression as ts.TaggedTemplateExpression
if (
taggedTemplate.kind === ts.SyntaxKind.TaggedTemplateExpression &&
taggedTemplate.tag.getText() === "graphql"
) {
const typeArgument = node.typeArguments && (node.typeArguments[0] as ts.TypeReferenceNode)
if (!typeArgument) {
const operationName = getOperationName(taggedTemplate)
if (operationName) {
const fixes = this.createFixes(functionOrTagName.getEnd(), 0, `<${operationName}>`, operationName)
this.addFailureAtNode(functionOrTagName, "missing operation type parameter", fixes)
}
} else {
const operationName = getOperationName(taggedTemplate)
if (
operationName &&
(typeArgument.kind !== ts.SyntaxKind.TypeReference || typeArgument.typeName.getText() !== operationName)
) {
const fixes = this.createFixes(
typeArgument.getStart(),
typeArgument.getWidth(),
operationName,
operationName,
)
this.addFailureAtNode(
typeArgument,
`expected operation type parameter to be \`${operationName}\``,
fixes,
)
}
}
} else {
this.addFailureAtNode(taggedTemplate, "expected a graphql`…` tagged-template")
}
}
createFixes(start: number, width: number, replacement: string, operationName: string): Lint.Replacement[] {
const fixes = [new Lint.Replacement(start, width, replacement)]
if (!this.hasImportForOperation(operationName)) {
fixes.push(this.importDeclarationFixForOperation(operationName))
}
return fixes
}
importPathForOperation(operationName: string) {
const options = this.getOptions()[0] || {
artifactDirectory: "__generated__",
makeRelative: false,
}
if (options.makeRelative) {
throw new Error("[relayOperationGenericsRule] Making import declarations relative is not implemented yet.")
}
return `${options.artifactDirectory}/${operationName}.graphql`
}
importDeclarationFixForOperation(operationName: string) {
const path = this.importPathForOperation(operationName)
const importDeclaration = `import { ${operationName} } from "${path}"\n`
const lastImport = this.imports[this.imports.length - 1]
let start = 0
if (lastImport) {
start = lastImport.getEnd() + 1
}
return new Lint.Replacement(start, 0, importDeclaration)
}
hasImportForOperation(operationName: string) {
const importPath = this.importPathForOperation(operationName)
return this.imports.some(node => {
const path = node.moduleSpecifier as ts.StringLiteral
if (path.text === importPath && node.importClause) {
const namedBindings = node.importClause.namedBindings as ts.NamedImports
if (namedBindings) {
return namedBindings.elements.some(element => element.name.getText() === operationName)
}
}
return false
})
}
}
function getOperationName(taggedTemplate: ts.TaggedTemplateExpression): string | null {
const template = taggedTemplate.template.getFullText()
// Strip backticks
const source = template.substring(1, template.length - 1)
const ast = GraphQL.parse(source)
let queryName = null
GraphQL.visit(ast, {
OperationDefinition(node) {
queryName = node.name.value
return GraphQL.BREAK
},
})
return queryName
}