From 0c115f5147ef781b9d752ae60983301e89a2a779 Mon Sep 17 00:00:00 2001 From: Ash Furrow Date: Sat, 13 Oct 2018 12:52:15 -0400 Subject: [PATCH] Initial, WIP import. --- package.json | 4 + src/relayOperationGenericsRule.ts | 198 ++++++++++++++++++++++++++++++ tslint.json | 5 +- yarn.lock | 17 +++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/relayOperationGenericsRule.ts diff --git a/package.json b/package.json index fe08618..dc27e36 100755 --- a/package.json +++ b/package.json @@ -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": { @@ -48,5 +49,8 @@ "yarn prettier --write", "git add" ] + }, + "dependencies": { + "graphql": "^14.0.2" } } diff --git a/src/relayOperationGenericsRule.ts b/src/relayOperationGenericsRule.ts new file mode 100644 index 0000000..1180dff --- /dev/null +++ b/src/relayOperationGenericsRule.ts @@ -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 } diff --git a/tslint.json b/tslint.json index c46b007..040c854 100755 --- a/tslint.json +++ b/tslint.json @@ -1,6 +1,7 @@ { "extends": [ - "tslint:recommended" + "tslint:recommended", + "tslint-config-prettier" ], "rules": { "arrow-parens": false, @@ -8,6 +9,7 @@ true, "never-prefix" ], + "no-console": false, "max-classes-per-file": [ false ], @@ -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, diff --git a/yarn.lock b/yarn.lock index bb2e781..d4fe177 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"