Skip to content

Commit

Permalink
Initial, WIP import.
Browse files Browse the repository at this point in the history
  • Loading branch information
ashfurrow committed Oct 13, 2018
1 parent ea9ca70 commit 0c115f5
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 1 deletion.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"ts-jest": "^23.10.1",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"typescript": "^3.0.3"
},
"scripts": {
Expand Down Expand Up @@ -48,5 +49,8 @@
"yarn prettier --write",
"git add"
]
},
"dependencies": {
"graphql": "^14.0.2"
}
}
198 changes: 198 additions & 0 deletions src/relayOperationGenericsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { BREAK, parse, visit } from "graphql"
import * as Lint from "tslint"
import * as ts from "typescript"

class Rule extends Lint.Rules.AbstractRule {
/**
* @param {ts.SourceFile} sourceFile
*/
apply(sourceFile) {
return this.applyWithWalker(new RelayOperationGenericsWalker(sourceFile, this.getOptions()))
}
}

class RelayOperationGenericsWalker extends Lint.RuleWalker {
_imports: ts.ImportDeclaration[] = []
getImports() {
return this._imports
}

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) {
this.visitOperationConfiguration(node, initializer.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) {
// any = this.visitOperationConfiguration(node, config, functionName)
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: any,
) {
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)
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")
}
}

/**
* @param {number} start
* @param {number} width
* @param {string} replacement
* @param {string} operationName
* @returns {Lint.Replacement[]}
*/
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
}

/**
* @param {string} operationName
*/
importPathForOperation(operationName) {
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) {
const path = this.importPathForOperation(operationName)
const importDeclaration = `import { ${operationName} } from "${path}"\n`

const imports = this.getImports()
const lastImport = imports[imports.length - 1]

let start = 0
if (lastImport) {
start = lastImport.getEnd() + 1
}

return new Lint.Replacement(start, 0, importDeclaration)
}

/**
* @param {string} operationName
*/
hasImportForOperation(operationName) {
// TODO: So many hoops to jump through without TS :/
/** @type {any} */
let asdf

const importPath = this.importPathForOperation(operationName)

return this.getImports().some(node => {
asdf = node.moduleSpecifier
/** @type {ts.StringLiteral} */
const path = asdf
if (path.text === importPath && node.importClause) {
asdf = node.importClause.namedBindings
/** @type {ts.NamedImports} */
const namedBindings = asdf
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 = parse(source)
let queryName = null
visit(ast, {
OperationDefinition(node) {
queryName = node.name.value
return BREAK
},
})

return queryName
}

module.exports = { Rule }
5 changes: 4 additions & 1 deletion tslint.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"extends": [
"tslint:recommended"
"tslint:recommended",
"tslint-config-prettier"
],
"rules": {
"arrow-parens": false,
"interface-name": [
true,
"never-prefix"
],
"no-console": false,
"max-classes-per-file": [
false
],
Expand All @@ -16,6 +18,7 @@
"check-accessor",
"check-constructor"
],
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"],
// Disabled till there’s an auto-fixer for this.
// https://github.com/palantir/tslint/blob/master/src/rules/objectLiteralSortKeysRule.ts
"object-literal-sort-keys": false,
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,13 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=

graphql@^14.0.2:
version "14.0.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650"
integrity sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw==
dependencies:
iterall "^1.2.2"

growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
Expand Down Expand Up @@ -3212,6 +3219,11 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"

iterall@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==

jest-changed-files@^23.4.2:
version "23.4.2"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83"
Expand Down Expand Up @@ -5946,6 +5958,11 @@ tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==

tslint-config-prettier@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz#76b9714399004ab6831fdcf76d89b73691c812cf"
integrity sha512-06CgrHJxJmNYVgsmeMoa1KXzQRoOdvfkqnJth6XUkNeOz707qxN0WfxfhYwhL5kXHHbYJRby2bqAPKwThlZPhw==

tslint@^5.11.0:
version "5.11.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed"
Expand Down

0 comments on commit 0c115f5

Please sign in to comment.