Skip to content

Commit

Permalink
Add autofix for undefined variables
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown committed Aug 1, 2020
1 parent 810e307 commit d4a220a
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-eyes-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ts-gql/eslint-plugin": minor
---

Add autofix for undefined variables
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { gql } from "@ts-gql/tag";

// prettier-ignore
gql`
query Thing($other: String,,) {
optional(thing: $thing)
other: optional(thing: $other)
}
`;
211 changes: 195 additions & 16 deletions packages/eslint-plugin/src/__snapshots__/test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,47 @@ gql\`
exports[`missing-variable.ts 1`] = `
Object {
"fixedOutput": "import { gql } from \\"@ts-gql/tag\\";
gql\`
query Thing($thing: String) {
optional(thing: $thing)
}
\`as import(\\"../__generated__/ts-gql/Thing\\").type;
",
"lintMessages": Array [
Object {
"column": 21,
"column": 1,
"endColumn": 2,
"endLine": 7,
"fix": Object {
"range": Array [
90,
90,
],
"text": "as import(\\"../__generated__/ts-gql/Thing\\").type",
},
"line": 3,
"message": "You must cast gql tags with the generated type",
"messageId": "mustUseAs",
"nodeType": "TaggedTemplateExpression",
"ruleId": "ts-gql",
"severity": 2,
},
Object {
"column": 22,
"endColumn": 23,
"endLine": 5,
"fix": Object {
"range": Array [
54,
54,
],
"text": "($thing: String)",
},
"line": 5,
"message": "Variable \\"$thing\\" is not defined by operation \\"Thing\\".",
"nodeType": "TaggedTemplateExpression",
"nodeType": null,
"ruleId": "ts-gql",
"severity": 2,
},
Expand All @@ -139,26 +174,64 @@ Object {
import { gql } from \\"@ts-gql/tag\\";
gql\`
~~~~ [You must cast gql tags with the generated type]
query Thing {
~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
optional(thing: $thing)
~~~~~~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
}
~~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~~~ [You must cast gql tags with the generated type]
\`;
~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]",
~ [You must cast gql tags with the generated type]
",
}
`;
exports[`missing-variable-with-another-variable.ts 1`] = `
Object {
"fixedOutput": "import { gql } from \\"@ts-gql/tag\\";
gql\`
query Thing($other: String, $thing: String) {
optional(thing: $thing)
other: optional(thing: $other)
}
\`as import(\\"../__generated__/ts-gql/Thing\\").type;
",
"lintMessages": Array [
Object {
"column": 21,
"column": 1,
"endColumn": 2,
"endLine": 8,
"fix": Object {
"range": Array [
141,
141,
],
"text": "as import(\\"../__generated__/ts-gql/Thing\\").type",
},
"line": 3,
"message": "You must cast gql tags with the generated type",
"messageId": "mustUseAs",
"nodeType": "TaggedTemplateExpression",
"ruleId": "ts-gql",
"severity": 2,
},
Object {
"column": 22,
"endColumn": 23,
"endLine": 5,
"fix": Object {
"range": Array [
69,
69,
],
"text": ", $thing: String",
},
"line": 5,
"message": "Variable \\"$thing\\" is not defined by operation \\"Thing\\".",
"nodeType": "TaggedTemplateExpression",
"nodeType": null,
"ruleId": "ts-gql",
"severity": 2,
},
Expand All @@ -167,25 +240,131 @@ Object {
import { gql } from \\"@ts-gql/tag\\";
gql\`
~~~~ [You must cast gql tags with the generated type]
query Thing($other: String) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
optional(thing: $thing)
~~~~~~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
other: optional(thing: $other)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
}
~~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~~~ [You must cast gql tags with the generated type]
\`;
~~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
~ [You must cast gql tags with the generated type]
",
}
`;
~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]",
exports[`missing-variable-with-another-variable-with-comma-after.ts 1`] = `
Object {
"fixedOutput": "import { gql } from \\"@ts-gql/tag\\";
// prettier-ignore
gql\`
query Thing($other: String, $thing: String,,) {
optional(thing: $thing)
other: optional(thing: $other)
}
\`as import(\\"../__generated__/ts-gql/Thing\\").type;
",
"lintMessages": Array [
Object {
"column": 1,
"endColumn": 2,
"endLine": 9,
"fix": Object {
"range": Array [
162,
162,
],
"text": "as import(\\"../__generated__/ts-gql/Thing\\").type",
},
"line": 4,
"message": "You must cast gql tags with the generated type",
"messageId": "mustUseAs",
"nodeType": "TaggedTemplateExpression",
"ruleId": "ts-gql",
"severity": 2,
},
Object {
"column": 22,
"endColumn": 23,
"endLine": 6,
"fix": Object {
"range": Array [
88,
88,
],
"text": ", $thing: String",
},
"line": 6,
"message": "Variable \\"$thing\\" is not defined by operation \\"Thing\\".",
"nodeType": null,
"ruleId": "ts-gql",
"severity": 2,
},
],
"snapshot": "
import { gql } from \\"@ts-gql/tag\\";
// prettier-ignore
gql\`
~~~~ [You must cast gql tags with the generated type]
query Thing($other: String,,) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
optional(thing: $thing)
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
~ [Variable \\"$thing\\" is not defined by operation \\"Thing\\".]
other: optional(thing: $other)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [You must cast gql tags with the generated type]
}
~~~ [You must cast gql tags with the generated type]
\`;
~ [You must cast gql tags with the generated type]
",
}
`;
exports[`multiple-validation-errors.ts 1`] = `
Object {
"lintMessages": Array [],
"lintMessages": Array [
Object {
"column": 5,
"line": 5,
"message": "Cannot query field \\"a\\" on type \\"Query\\".",
"nodeType": "TaggedTemplateExpression",
"ruleId": "ts-gql",
"severity": 2,
},
Object {
"column": 5,
"line": 6,
"message": "Cannot query field \\"b\\" on type \\"Query\\".",
"nodeType": "TaggedTemplateExpression",
"ruleId": "ts-gql",
"severity": 2,
},
],
"snapshot": "
",
import { gql } from \\"@ts-gql/tag\\";
gql\`
query Thing {
a
~ [Cannot query field \\"a\\" on type \\"Query\\".]
b
~~~~~ [Cannot query field \\"a\\" on type \\"Query\\".]
~ [Cannot query field \\"b\\" on type \\"Query\\".]
}
~~~ [Cannot query field \\"a\\" on type \\"Query\\".]
~~~ [Cannot query field \\"b\\" on type \\"Query\\".]
\`;
~~ [Cannot query field \\"a\\" on type \\"Query\\".]
~~ [Cannot query field \\"b\\" on type \\"Query\\".]
~ [Cannot query field \\"a\\" on type \\"Query\\".]
~ [Cannot query field \\"b\\" on type \\"Query\\".]",
}
`;
Expand Down
75 changes: 73 additions & 2 deletions packages/eslint-plugin/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
GraphQLNonNull,
GraphQLList,
ASTNode,
NoUndefinedVariablesRule,
ValidationRule,
GraphQLError,
} from "graphql";
import { VariableUsage } from "graphql/validation/ValidationContext";

// loosely based on https://github.com/apollographql/eslint-plugin-graphql/blob/master/src/createRule.js

Expand All @@ -27,7 +31,8 @@ let rules = specifiedRules.filter(
(x) =>
x !== NoUnusedFragmentsRule &&
x !== NoUnusedVariablesRule &&
x !== KnownFragmentNamesRule
x !== KnownFragmentNamesRule &&
x !== NoUndefinedVariablesRule
);

function replaceExpressions(
Expand Down Expand Up @@ -119,8 +124,12 @@ export function handleTemplateTag(
});
return;
}
const validationErrors = validate(schema, ast, rules);

const validationErrors = validate(
schema,
ast,
rules.concat(createNoUndefinedVariablesRule(node, report))
);
validationErrors.forEach((error) => {
report({
node,
Expand Down Expand Up @@ -231,3 +240,65 @@ export function handleTemplateTag(
);
return { ast, document: text };
}

function createNoUndefinedVariablesRule(
taggedTemplateNode: TSESTree.TaggedTemplateExpression,
report: TSESLint.RuleContext<MessageId, any>["report"]
): ValidationRule {
return (context) => {
let variableNameDefined = Object.create(null);

return {
OperationDefinition: {
enter() {
variableNameDefined = Object.create(null);
},
leave(operation) {
const usages: VariableUsage[] = (context as any).getRecursiveVariableUsages(
operation
);

for (const { node, type } of usages) {
const varName = node.name.value;

if (variableNameDefined[varName] !== true) {
let message = operation.name
? `Variable "$${varName}" is not defined by operation "${operation.name.value}".`
: `Variable "$${varName}" is not defined.`;
if (operation.name && type) {
let printedType = type.toString();
let add = taggedTemplateNode.quasi.range[0] + 1;
let fixPosition =
(operation.variableDefinitions?.length
? operation.variableDefinitions[
operation.variableDefinitions.length - 1
].loc!.end
: operation.name.loc!.end) + add;
let fixString = operation.variableDefinitions?.length
? `, $${varName}: ${printedType}`
: `($${varName}: ${printedType})`;
report({
// @ts-ignore
message,
fix(fixer) {
return fixer.insertTextAfterRange(
[fixPosition, fixPosition],
fixString
);
},
loc: locFromGraphQLNode(taggedTemplateNode, node),
});
} else
context.reportError(
new GraphQLError(message, [node, operation])
);
}
}
},
},
VariableDefinition(node) {
variableNameDefined[node.variable.name.value] = true;
},
};
};
}
9 changes: 4 additions & 5 deletions test-app/pages/apollo.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { gql } from "@ts-gql/tag";
import { useQuery, useMutation } from "@ts-gql/apollo";
import { useMutation } from "@ts-gql/apollo";

const query2 = gql`
query MyQueryApollo {
__typename
hello
another
query MyQueryApollo($thng: String, $x: Something!) {
optional(thing: $thng)
oneMore(other: $x)
}
` as import("../__generated__/ts-gql/MyQueryApollo").type;

Expand Down

0 comments on commit d4a220a

Please sign in to comment.