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

IDE friendly errors #2752

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -1471,7 +1471,7 @@ THROWN EXCEPTION:

ReaderCodeGenerator: Complex argument values (Lists or InputObjects with nested variables) are not supported.

Source: GraphQL request (3:17)
Source: GraphQL request:3:17
2: query TestQuery($date: String) {
3: items(filter: {date: $date}) {
^
Expand Down Expand Up @@ -3287,13 +3287,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Invalid use of @connection_resolver, could not generate a default label that is unique. Specify a unique 'label' as a literal string.

Source: GraphQL request (20:25)
Source: GraphQL request:20:25
19: ... {
20: comments(first: 10) @connection_resolver(resolver: "FeedbackCommentsEdgesResolver") { #error: same default label
^
21: count

Source: GraphQL request (9:23)
Source: GraphQL request:9:23
8: fragment FeedbackComments_feedback on Feedback {
9: comments(first: 10) @connection_resolver(resolver: "FeedbackCommentsEdgesResolver") {
^
Expand Down Expand Up @@ -3334,13 +3334,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Invalid use of @connection_resolver, the provided label is not unique. Specify a unique 'label' as a literal string.

Source: GraphQL request (20:89)
Source: GraphQL request:20:89
19: ... {
20: comments(first: 10) @connection_resolver(resolver: "FeedbackCommentsEdgesResolver", label: "comments") { #error: same label
^
21: count

Source: GraphQL request (9:87)
Source: GraphQL request:9:87
8: fragment FeedbackComments_feedback on Feedback {
9: comments(first: 10) @connection_resolver(resolver: "FeedbackCommentsEdgesResolver", label: "comments") {
^
Expand Down Expand Up @@ -3376,7 +3376,7 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- @connection_resolver fields must return a single value, not a list, found '[CommentsEdge]'

Source: GraphQL request (10:5)
Source: GraphQL request:10:5
9: comments(first: 10) {
10: edges @connection_resolver(resolver: "FeedbackCommentsEdgesResolver") { # error: plural
^
Expand All @@ -3403,7 +3403,7 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- The @connection_resolver direction is not supported on scalar fields, only fields returning an object/interface/union

Source: GraphQL request (9:6)
Source: GraphQL request:9:6
8: fragment FeedbackComments_feedback on Feedback {
9: id @connection_resolver(resolver: "FeedbackIDResolver") # error: scalar
^
Expand Down Expand Up @@ -4657,7 +4657,7 @@ Error: Encountered 2 error(s):
- Operation 'FeedbackQuery' references undefined variable(s):
- $commentsKey: String.

Source (derived): GraphQL request (25:26)
Source (derived): GraphQL request:25:26
24: key: "FeedbackFragment_comments"
25: dynamicKey_UNSTABLE: $commentsKey
^
Expand All @@ -4666,7 +4666,7 @@ Error: Encountered 2 error(s):
- Operation 'PaginationQuery' references undefined variable(s):
- $commentsKey: String.

Source (derived): GraphQL request (25:26)
Source (derived): GraphQL request:25:26
24: key: "FeedbackFragment_comments"
25: dynamicKey_UNSTABLE: $commentsKey
^
Expand Down Expand Up @@ -5899,31 +5899,31 @@ THROWN EXCEPTION:

Error: Found a circular reference from fragment 'Profile'.

Source (derived): GraphQL request (2:1)
Source (derived): GraphQL request:2:1
1: # expected-to-throw
2: fragment RefetchableFragment on Query @refetchable(queryName: "RefetchableFragmentQuery") @argumentDefinitions(id: {type: "ID!"}) {
^
3: node(id: $id) {

Source (derived): GraphQL request (2:1)
Source (derived): GraphQL request:2:1
1: # expected-to-throw
2: fragment RefetchableFragment on Query @refetchable(queryName: "RefetchableFragmentQuery") @argumentDefinitions(id: {type: "ID!"}) {
^
3: node(id: $id) {

Source: GraphQL request (7:7)
Source: GraphQL request:7:7
6: name
7: ...Profile @arguments(includeProfile: true)
^
8: }

Source: GraphQL request (21:9)
Source: GraphQL request:21:9
20: node {
21: ...Profile
^
22: }

Source: GraphQL request (21:9)
Source: GraphQL request:21:9
20: node {
21: ...Profile
^
Expand Down Expand Up @@ -6675,13 +6675,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Unexpected directive: @defer. This directive can only be used on fields/fragments that are fetched from the server schema, but it is used inside a client-only selection.

Source (derived): GraphQL request (12:23)
Source (derived): GraphQL request:12:23
11: emailAddresses
12: ...DeferredFragment @defer(label: "DeferredFragmentLabel")
^
13: }

Source (derived): GraphQL request (12:3)
Source (derived): GraphQL request:12:3
11: emailAddresses
12: ...DeferredFragment @defer(label: "DeferredFragmentLabel")
^
Expand Down Expand Up @@ -7082,13 +7082,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Cannot use @relay(mask: false) on fragment spreads for fragments with directives.

Source: GraphQL request (5:5)
Source: GraphQL request:5:5
4: me {
5: ...Profile @relay(mask: false)
^
6: }

Source: GraphQL request (9:26)
Source: GraphQL request:9:26
8:
9: fragment Profile on User @inline {
^
Expand Down Expand Up @@ -7482,7 +7482,7 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Variables are not yet supported inside @inline fragments.

Source: GraphQL request (10:24)
Source: GraphQL request:10:24
9: fragment Profile on User @inline {
10: profilePicture(size: $pictureSize) {
^
Expand Down Expand Up @@ -7520,7 +7520,7 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Variables are not yet supported inside @inline fragments.

Source: GraphQL request (17:24)
Source: GraphQL request:17:24
16: @inline
17: @argumentDefinitions(sizeArg: { type: "[Int]", defaultValue: [50] }) {
^
Expand Down Expand Up @@ -8329,13 +8329,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Found conflicting @module selections: use a unique alias on the parent fields.

Source: GraphQL request (22:7)
Source: GraphQL request:22:7
21: ...MarkdownUserNameRenderer_name
22: @module(name: "BarMarkdownUserNameRenderer.react")
^
23: }

Source: GraphQL request (13:7)
Source: GraphQL request:13:7
12: ...MarkdownUserNameRenderer_name
13: @module(name: "FooMarkdownUserNameRenderer.react")
^
Expand Down Expand Up @@ -8531,13 +8531,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Required argument 'capitalize: Boolean!' is missing on 'nameWithArgs' in 'TestQuery'.

Source: GraphQL request (5:7)
Source: GraphQL request:5:7
4: hometown{
5: nameWithArgs
^
6: }

Source: GraphQL request (2:1)
Source: GraphQL request:2:1
1: # expected-to-throw
2: query TestQuery {
^
Expand Down Expand Up @@ -9220,13 +9220,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- Found conflicting @module selections: use a unique alias on the parent fields.

Source: GraphQL request (25:7)
Source: GraphQL request:25:7
24: ...MarkdownUserNameRenderer_name
25: @module(name: "BarMarkdownUserNameRenderer.react")
^
26: }

Source: GraphQL request (13:7)
Source: GraphQL request:13:7
12: ...MarkdownUserNameRenderer_name
13: @module(name: "FooMarkdownUserNameRenderer.react")
^
Expand Down Expand Up @@ -15408,13 +15408,13 @@ THROWN EXCEPTION:
Error: Encountered 1 error(s):
- RelayMaskTransform: Cannot use @relay(mask: false) on fragment spread because the fragment definition uses @argumentDefinitions.

Source: GraphQL request (5:5)
Source: GraphQL request:5:5
4: me {
5: ...User_user @relay(mask: false)
^
6: }

Source: GraphQL request (10:24)
Source: GraphQL request:10:24
9: fragment User_user on User
10: @argumentDefinitions(isRelative: {type: "Boolean!", defaultValue: false}) {
^
Expand Down Expand Up @@ -15834,13 +15834,13 @@ THROWN EXCEPTION:
Error: RelayParser: Encountered 1 error(s):
- Variable @arguments values are only supported when the argument is defined with @argumentDefinitions. Check the definition of fragment 'ProfilePhoto'.

GraphQL request (16:36)
GraphQL request:16:36
15: __typename
16: ...ProfilePhoto @arguments(size: $size)
^
17: }

GraphQL request (19:1)
GraphQL request:19:1
18:
19: fragment ProfilePhoto on User {
^
Expand Down
32 changes: 30 additions & 2 deletions packages/relay-compiler/core/RelayCompilerError.js
Expand Up @@ -110,7 +110,7 @@ function createCombinedError(
`${prefix}Encountered ${errors.length} error(s):\n` +
errors
.map(error =>
String(error)
(error instanceof GraphQLError ? printError(error) : String(error))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use our own formatter for GraphQLError instances, instead of the one from graphql-js.

.split('\n')
.map((line, index) => (index === 0 ? `- ${line}` : ` ${line}`))
.join('\n'),
Expand Down Expand Up @@ -192,6 +192,34 @@ function printLocations(locations: $ReadOnlyArray<Location>): Array<string> {
return printedLocations;
}

/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*/
function printError(error: GraphQLError): string {
const printedLocations = [];
if (error.nodes) {
for (const node of error.nodes) {
if (node.loc) {
printedLocations.push(
highlightSourceAtLocation(
node.loc.source,
getLocation(node.loc.source, node.loc.start),
),
);
}
}
} else if (error.source && error.locations) {
const source = error.source;
for (const location of error.locations) {
printedLocations.push(highlightSourceAtLocation(source, location));
}
}
return printedLocations.length === 0
? error.message
: [error.message, ...printedLocations].join('\n\n') + '\n';
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All only needed so we can use our own copy of highlightSourceAtLocation.


/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
Expand All @@ -212,7 +240,7 @@ function highlightSourceAtLocation(

const lines = body.split(/\r\n|[\n\r]/g);
return (
`${source.name} (${lineNum}:${columnNum})\n` +
`${source.name}:${lineNum}:${columnNum}\n` +
printPrefixedLines([
// Lines specified like this: ["prefix", "string"],
[`${lineNum - 1}: `, lines[lineIndex - 1]],
Expand Down
8 changes: 4 additions & 4 deletions packages/relay-compiler/core/RelayFindGraphQLTags.js
Expand Up @@ -23,14 +23,14 @@ import type {
GraphQLTagFinder,
} from '../language/RelayLanguagePluginInterface';

const cache = new RelayCompilerCache('RelayFindGraphQLTags', 'v1');
const cache = new RelayCompilerCache('RelayFindGraphQLTags', 'v2');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have to bust the cache, as we should now cache the entire tag, instead of only the tag.template.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kassens This work is distinct from #2784 imo.


function memoizedFind(
tagFinder: GraphQLTagFinder,
text: string,
baseDir: string,
file: File,
): $ReadOnlyArray<string> {
): $ReadOnlyArray<GraphQLTag> {
invariant(
file.exists,
'RelayFindGraphQLTags: Called with non-existent file `%s`',
Expand All @@ -46,11 +46,11 @@ function find(
tagFinder: GraphQLTagFinder,
text: string,
absPath: string,
): $ReadOnlyArray<string> {
): $ReadOnlyArray<GraphQLTag> {
const tags = tagFinder(text, absPath);
const moduleName = getModuleName(absPath);
tags.forEach(tag => validateTemplate(tag, moduleName, absPath));
return tags.map(tag => tag.template);
return tags;
}

function validateTemplate(
Expand Down
10 changes: 7 additions & 3 deletions packages/relay-compiler/core/RelaySourceModuleParser.js
Expand Up @@ -73,14 +73,18 @@ module.exports = (

const astDefinitions = [];
const sources = [];
memoizedTagFinder(text, baseDir, file).forEach(template => {
const source = new GraphQL.Source(template, file.relPath);
memoizedTagFinder(text, baseDir, file).forEach(tag => {
const source = new GraphQL.Source(
tag.template,
path.join(path.relative(process.cwd(), baseDir), file.relPath),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Keep path short by making it relative, but in a way that the IDE can find it (i.e. relative to the working dir).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kassens This too.

tag.sourceLocationOffset,
);
const ast = parseGraphQL(source);
invariant(
ast.definitions.length,
'RelaySourceModuleParser: Expected GraphQL text to contain at least one ' +
'definition (fragment, mutation, query, subscription), got `%s`.',
template,
tag.template,
);
sources.push(source.body);
astDefinitions.push(...ast.definitions);
Expand Down
Expand Up @@ -15,7 +15,9 @@ const RelayFindGraphQLTags = require('../RelayFindGraphQLTags');

describe('RelayFindGraphQLTags', () => {
function find(text, absPath: string = '/path/to/FindGraphQLTags.js') {
return RelayFindGraphQLTags.find(FindGraphQLTags.find, text, absPath);
return RelayFindGraphQLTags.find(FindGraphQLTags.find, text, absPath).map(
tag => tag.template,
);
}

describe('query parsing', () => {
Expand Down