Skip to content
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

Import relayOperationGenericsRule #1

Merged
merged 7 commits into from
Oct 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.editorconfig
.travis.yml
node_modules/
src/
rules/
dangerfile.ts
tsconfig.json
tslint.json
Expand Down
15 changes: 10 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
"name": "tslint-plugin-relay",
"version": "0.0.1",
"description": "",
"main": "build/index.js",
"main": "build/tslint-base.json",
"author": "Ash Furrow <ash@ashfurrow.com> & Art.sy Inc",
"license": "MIT",
"peerDependencies": {
"graphql": "^14.0.2"
},
"devDependencies": {
"@types/jest": "^23.3.2",
"@types/node": "^10.11.0",
Expand All @@ -17,12 +20,14 @@
"ts-jest": "^23.10.1",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^3.0.3"
"tslint-config-prettier": "^1.15.0",
"typescript": "^3.0.3",
"graphql": "^14.0.2"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to put this in devDependencies because peer deps aren't installed by default.

},
"scripts": {
"type-check": "tsc --noEmit",
"build": "tsc",
"lint": "tslint 'src/**/*.{ts,tsx}'",
"build": "tsc && cp tslint-*.json build/",
"lint": "tslint 'rules/**/*.{ts,tsx}'",
"release": "release-it"
},
"jest": {
Expand All @@ -32,7 +37,7 @@
"printWidth": 115,
"semi": false,
"singleQuote": false,
"trailingComma": "es5",
"trailingComma": "all",
"bracketSpacing": true,
"proseWrap": "always"
},
Expand Down
173 changes: 173 additions & 0 deletions rules/relayOperationGenericsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import * as GraphQL from "graphql"
import * as Lint from "tslint"
import * as ts from "typescript"

export class Rule extends Lint.Rules.AbstractRule {
apply(sourceFile) {
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) {
// 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: 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
}
7 changes: 0 additions & 7 deletions src/_tests/index.test.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/index.ts

This file was deleted.

6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"rootDir": "src",
"rootDir": "rules",
"outDir": "build",
"allowJs": false,
"pretty": true,
Expand All @@ -11,8 +11,8 @@
},
"lib":["es2017"],
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"rules/**/*.ts",
"rules/**/*.tsx",
"dangerfile.ts"
],
"exclude": [
Expand Down
3 changes: 3 additions & 0 deletions tslint-base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rulesDirectory": "./build"
}
6 changes: 6 additions & 0 deletions tslint-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./tslint-base.json",
"rules": {
"tslint-plugin-relay": true
}
}
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
10 changes: 5 additions & 5 deletions wallaby.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ module.exports = function(wallaby) {
return {
files: [
"tsconfig.json",
"src/**/*.ts?(x)",
"src/**/*.snap",
"src/**/*.json",
"!src/**/*.test.ts?(x)",
"rules/**/*.ts?(x)",
"rules/**/*.snap",
"rules/**/*.json",
"!rules/**/*.test.ts?(x)",
],
tests: ["src/**/*.test.ts?(x)"],
tests: ["rules/**/*.test.ts?(x)"],

env: {
type: "node",
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