diff --git a/packages/relay-compiler/codegen/RelayFileWriter.js b/packages/relay-compiler/codegen/RelayFileWriter.js index e9acc5951fafe..175a947ecd3b4 100644 --- a/packages/relay-compiler/codegen/RelayFileWriter.js +++ b/packages/relay-compiler/codegen/RelayFileWriter.js @@ -15,7 +15,6 @@ const CodegenDirectory = require('./CodegenDirectory'); const CompilerContext = require('../core/GraphQLCompilerContext'); const Profiler = require('../core/GraphQLCompilerProfiler'); const RelayParser = require('../core/RelayParser'); -const RelayValidator = require('../core/RelayValidator'); const compileRelayArtifacts = require('./compileRelayArtifacts'); const crypto = require('crypto'); @@ -103,14 +102,12 @@ function compileAll({ |}) { // Verify using local and global rules, can run global verifications here // because all files are processed together - let validationRules = RelayValidator.GLOBAL_RULES; - if (extraValidationRules) { - validationRules = [ - ...validationRules, - ...(extraValidationRules.LOCAL_RULES || []), - ...(extraValidationRules.GLOBAL_RULES || []), - ]; - } + const validationRules = extraValidationRules + ? [ + ...(extraValidationRules.LOCAL_RULES || []), + ...(extraValidationRules.GLOBAL_RULES || []), + ] + : []; const definitions = ASTConvert.convertASTDocumentsWithBase( schema, diff --git a/packages/relay-compiler/codegen/__tests__/__snapshots__/compileRelayArtifacts-test.js.snap b/packages/relay-compiler/codegen/__tests__/__snapshots__/compileRelayArtifacts-test.js.snap index 73d3d2d50312e..8f5bd6b919eda 100644 --- a/packages/relay-compiler/codegen/__tests__/__snapshots__/compileRelayArtifacts-test.js.snap +++ b/packages/relay-compiler/codegen/__tests__/__snapshots__/compileRelayArtifacts-test.js.snap @@ -376,7 +376,7 @@ type Foo { exports[`compileRelayArtifacts matches expected output: client-fields-on-roots.graphql 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ -query FooQuery($id: ID!, $arg: String) { +query FooQuery($id: ID!) { client_root_field node(id: $id) { @@ -424,12 +424,6 @@ extend type Subscription { "name": "id", "type": "ID!", "defaultValue": null - }, - { - "kind": "LocalArgument", - "name": "arg", - "type": "String", - "defaultValue": null } ], "selections": [ @@ -480,12 +474,6 @@ extend type Subscription { "name": "id", "type": "ID!", "defaultValue": null - }, - { - "kind": "LocalArgument", - "name": "arg", - "type": "String", - "defaultValue": null } ], "selections": [ @@ -4299,7 +4287,7 @@ fragment FeedbackComments_feedback on Feedback { exports[`compileRelayArtifacts matches expected output: connection-resolver-field-stream-if-default-true.graphql 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ -query QueryWithConnectionField($id: ID!, $enableStream: Boolean) { +query QueryWithConnectionField($id: ID!) { feedback: node(id: $id) { ...FeedbackComments_feedback } @@ -4336,12 +4324,6 @@ fragment FeedbackComments_feedback on Feedback { "name": "id", "type": "ID!", "defaultValue": null - }, - { - "kind": "LocalArgument", - "name": "enableStream", - "type": "Boolean", - "defaultValue": null } ], "selections": [ @@ -4378,12 +4360,6 @@ fragment FeedbackComments_feedback on Feedback { "name": "id", "type": "ID!", "defaultValue": null - }, - { - "kind": "LocalArgument", - "name": "enableStream", - "type": "Boolean", - "defaultValue": null } ], "selections": [ @@ -4678,7 +4654,7 @@ fragment FeedbackComments_feedback on Feedback { exports[`compileRelayArtifacts matches expected output: connection-resolver-field-stream-if-explicit-true.graphql 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ -query QueryWithConnectionField($id: ID!, $enableStream: Boolean) { +query QueryWithConnectionField($id: ID!) { feedback: node(id: $id) { ...FeedbackComments_feedback } @@ -4716,12 +4692,6 @@ fragment FeedbackComments_feedback on Feedback { "name": "id", "type": "ID!", "defaultValue": null - }, - { - "kind": "LocalArgument", - "name": "enableStream", - "type": "Boolean", - "defaultValue": null } ], "selections": [ @@ -4758,12 +4728,6 @@ fragment FeedbackComments_feedback on Feedback { "name": "id", "type": "ID!", "defaultValue": null - }, - { - "kind": "LocalArgument", - "name": "enableStream", - "type": "Boolean", - "defaultValue": null } ], "selections": [ @@ -19865,7 +19829,7 @@ query UnionQuery { exports[`compileRelayArtifacts matches expected output: unknown-root-variable-in-fragment.invalid.graphql 1`] = ` ~~~~~~~~~~ INPUT ~~~~~~~~~~ # expected-to-throw -query TestQuery($id: ID!, $pictureSize: [Int] = [128]) { +query TestQuery($id: ID!) { node(id: $id) { id ...Profile @relay(mask: false) @@ -19894,7 +19858,7 @@ Error: Variable '$includeFriends' is not in scope. Source: GraphQL request (2:1) 1: # expected-to-throw -2: query TestQuery($id: ID!, $pictureSize: [Int] = [128]) { +2: query TestQuery($id: ID!) { ^ 3: node(id: $id) { diff --git a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/client-fields-on-roots.graphql b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/client-fields-on-roots.graphql index 8d65960fe8db3..04e15c7d725d2 100644 --- a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/client-fields-on-roots.graphql +++ b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/client-fields-on-roots.graphql @@ -1,4 +1,4 @@ -query FooQuery($id: ID!, $arg: String) { +query FooQuery($id: ID!) { client_root_field node(id: $id) { diff --git a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-default-true.graphql b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-default-true.graphql index b3e042e9cc5d1..5501ae22e3c5d 100644 --- a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-default-true.graphql +++ b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-default-true.graphql @@ -1,4 +1,4 @@ -query QueryWithConnectionField($id: ID!, $enableStream: Boolean) { +query QueryWithConnectionField($id: ID!) { feedback: node(id: $id) { ...FeedbackComments_feedback } diff --git a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-explicit-true.graphql b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-explicit-true.graphql index edfdd8487b3c3..64f35e21c81a3 100644 --- a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-explicit-true.graphql +++ b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/connection-resolver-field-stream-if-explicit-true.graphql @@ -1,4 +1,4 @@ -query QueryWithConnectionField($id: ID!, $enableStream: Boolean) { +query QueryWithConnectionField($id: ID!) { feedback: node(id: $id) { ...FeedbackComments_feedback } diff --git a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/unknown-root-variable-in-fragment.invalid.graphql b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/unknown-root-variable-in-fragment.invalid.graphql index 52d88c287dca9..d9be2e2a85fcc 100644 --- a/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/unknown-root-variable-in-fragment.invalid.graphql +++ b/packages/relay-compiler/codegen/__tests__/fixtures/compileRelayArtifacts/unknown-root-variable-in-fragment.invalid.graphql @@ -1,5 +1,5 @@ # expected-to-throw -query TestQuery($id: ID!, $pictureSize: [Int] = [128]) { +query TestQuery($id: ID!) { node(id: $id) { id ...Profile @relay(mask: false) diff --git a/packages/relay-compiler/core/RelayIRTransforms.js b/packages/relay-compiler/core/RelayIRTransforms.js index 0ec18d7e244a2..7e0f043b8836f 100644 --- a/packages/relay-compiler/core/RelayIRTransforms.js +++ b/packages/relay-compiler/core/RelayIRTransforms.js @@ -36,6 +36,7 @@ const SkipRedundantNodesTransform = require('../transforms/SkipRedundantNodesTra const SkipUnreachableNodeTransform = require('../transforms/SkipUnreachableNodeTransform'); const SkipUnusedVariablesTransform = require('../transforms/SkipUnusedVariablesTransform'); const ValidateGlobalVariablesTransform = require('../transforms/ValidateGlobalVariablesTransform'); +const ValidateUnusedVariablesTransform = require('../transforms/ValidateUnusedVariablesTransform'); import type {IRTransform} from './GraphQLCompilerContext'; @@ -49,6 +50,7 @@ const relaySchemaExtensions: $ReadOnlyArray = [ RelayTestOperationTransform.SCHEMA_EXTENSION, InlineDataFragmentTransform.SCHEMA_EXTENSION, RelayFlowGenerator.SCHEMA_EXTENSION, + ValidateUnusedVariablesTransform.SCHEMA_EXTENSION, ]; // Transforms applied to both operations and fragments for both reading and @@ -76,6 +78,7 @@ const relayFragmentTransforms: $ReadOnlyArray = [ // Transforms applied to queries/mutations/subscriptions that are used for // fetching data from the server and parsing those responses. const relayQueryTransforms: $ReadOnlyArray = [ + ValidateUnusedVariablesTransform.transform, RelayApplyFragmentArgumentTransform.transform, ValidateGlobalVariablesTransform.transform, RelayGenerateIDFieldTransform.transform, diff --git a/packages/relay-compiler/core/RelayValidator.js b/packages/relay-compiler/core/RelayValidator.js index 35d8686c26bc9..fe1dfd2f524aa 100644 --- a/packages/relay-compiler/core/RelayValidator.js +++ b/packages/relay-compiler/core/RelayValidator.js @@ -14,7 +14,7 @@ const Profiler = require('./GraphQLCompilerProfiler'); const util = require('util'); -const {NoUnusedVariablesRule, formatError} = require('graphql'); +const {formatError} = require('graphql'); import type {Schema} from './Schema'; import type {DocumentNode, ValidationRule} from 'graphql'; @@ -41,23 +41,6 @@ function validateOrThrow( } module.exports = { - GLOBAL_RULES: [ - /* Some rules are not enabled (potentially non-exhaustive) - * - * - KnownFragmentNamesRule: RelayClassic generates fragments at runtime, - * so RelayCompat queries might reference fragments unknown in build time. - * - NoFragmentCyclesRule: Because of @argumentDefinitions, this validation - * incorrectly flags a subset of fragments using @include/@skip as - * recursive. - * - NoUndefinedVariablesRule: Because of @argumentDefinitions, this - * validation incorrectly marks some fragment variables as undefined. - * - NoUnusedFragmentsRule: Queries generated dynamically with RelayCompat - * might use unused fragments. - * - OverlappingFieldsCanBeMergedRule: RelayClassic auto-resolves - * overlapping fields by generating aliases. - */ - NoUnusedVariablesRule, - ], validate: (Profiler.instrument(validateOrThrow, 'RelayValidator.validate'): ( schema: Schema, document: DocumentNode, diff --git a/packages/relay-compiler/core/inferRootArgumentDefinitions.js b/packages/relay-compiler/core/inferRootArgumentDefinitions.js index 9e2fe61b525ba..71b5df66c17ae 100644 --- a/packages/relay-compiler/core/inferRootArgumentDefinitions.js +++ b/packages/relay-compiler/core/inferRootArgumentDefinitions.js @@ -197,14 +197,10 @@ function visit( // Merge any root variables referenced by the spread fragment // into this (parent) fragment's arguments. for (const argDef of referencedFragmentArguments.values()) { - if ( - argDef.kind === 'RootArgumentDefinition' && - !argumentDefinitions.has(argDef.name) - ) { + if (argDef.kind === 'RootArgumentDefinition') { argumentDefinitions.set(argDef.name, argDef); } } - return false; }, Argument(argument: Argument) { if (argument.value.kind !== 'Variable') { diff --git a/packages/relay-compiler/transforms/ValidateUnusedVariablesTransform.js b/packages/relay-compiler/transforms/ValidateUnusedVariablesTransform.js new file mode 100644 index 0000000000000..e4f920b9a9be3 --- /dev/null +++ b/packages/relay-compiler/transforms/ValidateUnusedVariablesTransform.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +const inferRootArgumentDefinitions = require('../core/inferRootArgumentDefinitions'); + +const { + createCombinedError, + createUserError, + eachWithErrors, +} = require('../core/RelayCompilerError'); + +import type GraphQLCompilerContext from '../core/GraphQLCompilerContext'; +import type {ArgumentDefinition} from '../core/GraphQLIR'; + +const SCHEMA_EXTENSION = + 'directive @DEPRECATED__relay_ignore_unused_variables_error on QUERY | MUTATION | SUBSCRIPTION'; + +/** + * Validates that there are no unused variables in the operation. + * former `graphql-js`` NoUnusedVariablesRule + */ +function validateUnusedVariablesTransform( + context: GraphQLCompilerContext, +): GraphQLCompilerContext { + const contextWithUsedArguments = inferRootArgumentDefinitions(context); + const errors = eachWithErrors(context.documents(), node => { + if (node.kind !== 'Root') { + return; + } + const rootArgumentLocations = new Map( + node.argumentDefinitions.map(arg => [arg.name, arg.loc]), + ); + const nodeWithUsedArguments = contextWithUsedArguments.getRoot(node.name); + const usedArguments = argumentDefinitionsToMap( + nodeWithUsedArguments.argumentDefinitions, + ); + for (const usedArgumentName of usedArguments.keys()) { + rootArgumentLocations.delete(usedArgumentName); + } + + const ignoreErrorDirective = node.directives.find( + ({name}) => name === 'DEPRECATED__relay_ignore_unused_variables_error', + ); + if (rootArgumentLocations.size > 0 && !ignoreErrorDirective) { + const isPlural = rootArgumentLocations.size > 1; + throw createUserError( + `Variable${isPlural ? 's' : ''} '$${Array.from( + rootArgumentLocations.keys(), + ).join("', '$")}' ${isPlural ? 'are' : 'is'} never used in operation '${ + node.name + }'.`, + Array.from(rootArgumentLocations.values()), + ); + } + if (rootArgumentLocations.size === 0 && ignoreErrorDirective) { + throw createUserError( + "Invalid usage of '@DEPRECATED__relay_ignore_unused_variables_error.'" + + `No unused variables found in the query '${node.name}'`, + [ignoreErrorDirective.loc], + ); + } + }); + if (errors != null && errors.length !== 0) { + throw createCombinedError(errors); + } + return context; +} + +function argumentDefinitionsToMap( + argDefs: $ReadOnlyArray, +): Map { + const map = new Map(); + for (const argDef of argDefs) { + map.set(argDef.name, argDef); + } + return map; +} + +module.exports = { + transform: validateUnusedVariablesTransform, + SCHEMA_EXTENSION, +}; diff --git a/packages/relay-compiler/transforms/__tests__/ValidateUnusedVariablesTransform-test.js b/packages/relay-compiler/transforms/__tests__/ValidateUnusedVariablesTransform-test.js new file mode 100644 index 0000000000000..9481a1a410269 --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/ValidateUnusedVariablesTransform-test.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @emails oncall+relay + */ + +'use strict'; + +const GraphQLCompilerContext = require('../../core/GraphQLCompilerContext'); +const Schema = require('../../core/Schema'); +const ValidateUnusedVariablesTransform = require('../ValidateUnusedVariablesTransform'); + +const {transformASTSchema} = require('../../core/ASTConvert'); +const { + TestSchema, + generateTestsFromFixtures, + parseGraphQLText, +} = require('relay-test-utils-internal'); + +generateTestsFromFixtures( + `${__dirname}/fixtures/ValidateUnusedVariablesTransform`, + text => { + const extendedSchema = transformASTSchema(TestSchema, [ + ValidateUnusedVariablesTransform.SCHEMA_EXTENSION, + ]); + const {definitions} = parseGraphQLText(extendedSchema, text); + const compilerSchema = Schema.DEPRECATED__create( + TestSchema, + extendedSchema, + ); + return new GraphQLCompilerContext(compilerSchema) + .addAll(definitions) + .applyTransforms([ValidateUnusedVariablesTransform.transform]) + .documents() + .map(doc => `${doc.name}: NO ERRORS.`) + .join('\n'); + }, +); diff --git a/packages/relay-compiler/transforms/__tests__/__snapshots__/ValidateUnusedVariablesTransform-test.js.snap b/packages/relay-compiler/transforms/__tests__/__snapshots__/ValidateUnusedVariablesTransform-test.js.snap new file mode 100644 index 0000000000000..414feae1bfb38 --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/__snapshots__/ValidateUnusedVariablesTransform-test.js.snap @@ -0,0 +1,243 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matches expected output: fragment-with-root-arguments.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +query QueryWithId($id: ID) { + node(id: $id) { + ... on User { + name + } + } +} + +query QueryWithCondition($shouldIncludeName: Boolean!) { + node { + ... on User { + name @include(if: $shouldIncludeName) + } + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +QueryWithId: NO ERRORS. +QueryWithCondition: NO ERRORS. +`; + +exports[`matches expected output: practically-unused-but-actually-used-variables.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +query QueryWithUnusedVariables($id: ID!, $unusedFirst: Int, $unusedAfter: ID) { + node(id: $id) { + id + ...ConnectionFragment @arguments(fetchConnection: false) + } +} + +fragment ConnectionFragment on User + @argumentDefinitions( + fetchConnection: {type: "Boolean", defaultValue: false} + ) { + # this branch will be excluded after all transforms + # and variables $unusedFirst and $unusedAfter will be skipped, eventually + # But this transform should not report them as unused + ... @include(if: $fetchConnection) { + friends(after: $unusedAfter, first: $unusedFirst) { + edges { + node { + id + } + } + } + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +QueryWithUnusedVariables: NO ERRORS. +ConnectionFragment: NO ERRORS. +`; + +exports[`matches expected output: query-with-invalid-error-suppression.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +# expected-to-throw +query QueryWithId($id: ID) @DEPRECATED__relay_ignore_unused_variables_error { + node(id: $id) { + __typename + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +THROWN EXCEPTION: + +Error: Encountered 1 error(s): +- Invalid usage of '@DEPRECATED__relay_ignore_unused_variables_error.'No unused variables found in the query 'QueryWithId' + + Source: GraphQL request (2:28) + 1: # expected-to-throw + 2: query QueryWithId($id: ID) @DEPRECATED__relay_ignore_unused_variables_error { + ^ + 3: node(id: $id) { + +`; + +exports[`matches expected output: query-with-unused-root-variable-shadowed-by-local.invalid.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +# expected-to-throw +query QueryWithId($id: ID, $foo: Boolean!) { + node(id: $id) { + ...User_data + } +} + +fragment User_data on User { + ...User_data_with_args +} + +fragment User_data_with_args on User + @argumentDefinitions(foo: {type: "Boolean!"}) { + name + username @include(if: $foo) +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +THROWN EXCEPTION: + +Error: Encountered 1 error(s): +- Variable '$foo' is never used in operation 'QueryWithId'. + + Source: GraphQL request (2:28) + 1: # expected-to-throw + 2: query QueryWithId($id: ID, $foo: Boolean!) { + ^ + 3: node(id: $id) { + +`; + +exports[`matches expected output: query-with-unused-variable.invalid.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +#expected-to-throw +query QueryWithUnusedVariable($unused: ID) { + node { + __typename + } +} + +query QueryWithUnusedVariables($unused: ID, $antother_unused: String) { + node { + __typename + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +THROWN EXCEPTION: + +Error: Encountered 2 error(s): +- Variable '$unused' is never used in operation 'QueryWithUnusedVariable'. + + Source: GraphQL request (2:31) + 1: #expected-to-throw + 2: query QueryWithUnusedVariable($unused: ID) { + ^ + 3: node { + +- Variables '$unused', '$antother_unused' are never used in operation 'QueryWithUnusedVariables'. + + Source: GraphQL request (8:32) + 7: + 8: query QueryWithUnusedVariables($unused: ID, $antother_unused: String) { + ^ + 9: node { + + Source: GraphQL request (8:45) + 7: + 8: query QueryWithUnusedVariables($unused: ID, $antother_unused: String) { + ^ + 9: node { + +`; + +exports[`matches expected output: query-with-unused-variable-error-suppressed.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +query QueryWithUnusedVariable($unused: ID) + @DEPRECATED__relay_ignore_unused_variables_error { + node { + __typename + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +QueryWithUnusedVariable: NO ERRORS. +`; + +exports[`matches expected output: query-with-variables-shadowed-by-local-variable-and-used-as-root-variable.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +query QueryWithIdAndFoo($id: ID, $foo: Boolean!) { + node(id: $id) { + ...User_data + } +} + +fragment User_data on User { + ...User_data_with_args +} + +# Here $foo is local argument, it's defined in the @argDefs +fragment User_data_with_args on User + @argumentDefinitions(foo: {type: "Boolean!"}) { + name + username @include(if: $foo) + ...AnotherUser_data +} + +# Here $foo is root argument, because \`AnotherUser_data\` doesn't have @argDefs +fragment AnotherUser_data on User { + profile_picture @skip(if: $foo) { + uri + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +QueryWithIdAndFoo: NO ERRORS. +User_data: NO ERRORS. +User_data_with_args: NO ERRORS. +AnotherUser_data: NO ERRORS. +`; + +exports[`matches expected output: variable-in-the-complex-object-list.invalid.graphql 1`] = ` +~~~~~~~~~~ INPUT ~~~~~~~~~~ +#expected-to-throw +query Q1($arg: String) { + items(filter: {date: $arg}) { + date + } +} + +query Q2($size: Int) { + node { + ... on User { + profilePicture(size: [$size]) { + uri + } + } + } +} + +~~~~~~~~~~ OUTPUT ~~~~~~~~~~ +THROWN EXCEPTION: + +Error: RelayParser: Encountered 2 error(s): +- Complex argument values (Lists or InputObjects with nested variables) are not supported. + + GraphQL request (3:24) + 2: query Q1($arg: String) { + 3: items(filter: {date: $arg}) { + ^ + 4: date + +- Complex argument values (Lists or InputObjects with nested variables) are not supported. + + GraphQL request (11:29) + 10: ... on User { + 11: profilePicture(size: [$size]) { + ^ + 12: uri + +`; diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/fragment-with-root-arguments.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/fragment-with-root-arguments.graphql new file mode 100644 index 0000000000000..9084318adc9ee --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/fragment-with-root-arguments.graphql @@ -0,0 +1,15 @@ +query QueryWithId($id: ID) { + node(id: $id) { + ... on User { + name + } + } +} + +query QueryWithCondition($shouldIncludeName: Boolean!) { + node { + ... on User { + name @include(if: $shouldIncludeName) + } + } +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/practically-unused-but-actually-used-variables.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/practically-unused-but-actually-used-variables.graphql new file mode 100644 index 0000000000000..246ddf2494b4d --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/practically-unused-but-actually-used-variables.graphql @@ -0,0 +1,24 @@ +query QueryWithUnusedVariables($id: ID!, $unusedFirst: Int, $unusedAfter: ID) { + node(id: $id) { + id + ...ConnectionFragment @arguments(fetchConnection: false) + } +} + +fragment ConnectionFragment on User + @argumentDefinitions( + fetchConnection: {type: "Boolean", defaultValue: false} + ) { + # this branch will be excluded after all transforms + # and variables $unusedFirst and $unusedAfter will be skipped, eventually + # But this transform should not report them as unused + ... @include(if: $fetchConnection) { + friends(after: $unusedAfter, first: $unusedFirst) { + edges { + node { + id + } + } + } + } +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-invalid-error-suppression.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-invalid-error-suppression.graphql new file mode 100644 index 0000000000000..74b2586664528 --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-invalid-error-suppression.graphql @@ -0,0 +1,6 @@ +# expected-to-throw +query QueryWithId($id: ID) @DEPRECATED__relay_ignore_unused_variables_error { + node(id: $id) { + __typename + } +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-root-variable-shadowed-by-local.invalid.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-root-variable-shadowed-by-local.invalid.graphql new file mode 100644 index 0000000000000..7b1fb5bb901f9 --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-root-variable-shadowed-by-local.invalid.graphql @@ -0,0 +1,16 @@ +# expected-to-throw +query QueryWithId($id: ID, $foo: Boolean!) { + node(id: $id) { + ...User_data + } +} + +fragment User_data on User { + ...User_data_with_args +} + +fragment User_data_with_args on User + @argumentDefinitions(foo: {type: "Boolean!"}) { + name + username @include(if: $foo) +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-variable-error-suppressed.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-variable-error-suppressed.graphql new file mode 100644 index 0000000000000..8010701d75cad --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-variable-error-suppressed.graphql @@ -0,0 +1,6 @@ +query QueryWithUnusedVariable($unused: ID) + @DEPRECATED__relay_ignore_unused_variables_error { + node { + __typename + } +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-variable.invalid.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-variable.invalid.graphql new file mode 100644 index 0000000000000..10304a67135dc --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-unused-variable.invalid.graphql @@ -0,0 +1,12 @@ +#expected-to-throw +query QueryWithUnusedVariable($unused: ID) { + node { + __typename + } +} + +query QueryWithUnusedVariables($unused: ID, $antother_unused: String) { + node { + __typename + } +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-variables-shadowed-by-local-variable-and-used-as-root-variable.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-variables-shadowed-by-local-variable-and-used-as-root-variable.graphql new file mode 100644 index 0000000000000..16205536a5b82 --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/query-with-variables-shadowed-by-local-variable-and-used-as-root-variable.graphql @@ -0,0 +1,24 @@ +query QueryWithIdAndFoo($id: ID, $foo: Boolean!) { + node(id: $id) { + ...User_data + } +} + +fragment User_data on User { + ...User_data_with_args +} + +# Here $foo is local argument, it's defined in the @argDefs +fragment User_data_with_args on User + @argumentDefinitions(foo: {type: "Boolean!"}) { + name + username @include(if: $foo) + ...AnotherUser_data +} + +# Here $foo is root argument, because `AnotherUser_data` doesn't have @argDefs +fragment AnotherUser_data on User { + profile_picture @skip(if: $foo) { + uri + } +} diff --git a/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/variable-in-the-complex-object-list.invalid.graphql b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/variable-in-the-complex-object-list.invalid.graphql new file mode 100644 index 0000000000000..0e98c4af3ab16 --- /dev/null +++ b/packages/relay-compiler/transforms/__tests__/fixtures/ValidateUnusedVariablesTransform/variable-in-the-complex-object-list.invalid.graphql @@ -0,0 +1,16 @@ +#expected-to-throw +query Q1($arg: String) { + items(filter: {date: $arg}) { + date + } +} + +query Q2($size: Int) { + node { + ... on User { + profilePicture(size: [$size]) { + uri + } + } + } +} diff --git a/packages/relay-experimental/__tests__/FragmentResource-test.js b/packages/relay-experimental/__tests__/FragmentResource-test.js index fd301aeb3625e..cd985eadbf69e 100644 --- a/packages/relay-experimental/__tests__/FragmentResource-test.js +++ b/packages/relay-experimental/__tests__/FragmentResource-test.js @@ -351,6 +351,7 @@ describe('FragmentResource', () => { query UserQuery($id: ID!, $foo: Boolean!) { node(id: $id) { __typename + name @include(if: $foo) ...UserFragment } } diff --git a/packages/relay-experimental/__tests__/useBlockingPaginationFragment-test.js b/packages/relay-experimental/__tests__/useBlockingPaginationFragment-test.js index c448e5b69f9dd..c1911050b6930 100644 --- a/packages/relay-experimental/__tests__/useBlockingPaginationFragment-test.js +++ b/packages/relay-experimental/__tests__/useBlockingPaginationFragment-test.js @@ -225,8 +225,6 @@ describe('useBlockingPaginationFragment', () => { $first: Int $before: ID $last: Int - $orderby: [String] - $isViewerFriend: Boolean ) { node(id: $id) { ...UserFragment @arguments(isViewerFriendLocal: true, orderby: ["name"]) diff --git a/packages/relay-experimental/__tests__/useFragment-test.js b/packages/relay-experimental/__tests__/useFragment-test.js index 019683d3fcdae..e41572cd8c6d0 100644 --- a/packages/relay-experimental/__tests__/useFragment-test.js +++ b/packages/relay-experimental/__tests__/useFragment-test.js @@ -104,7 +104,7 @@ describe('useFragment', () => { ...NestedUserFragment } - query UsersQuery($ids: [ID!]!, $scale: Int!) { + query UsersQuery($ids: [ID!]!) { nodes(ids: $ids) { ...UsersFragment } diff --git a/packages/relay-experimental/__tests__/useLegacyPaginationFragment-test.js b/packages/relay-experimental/__tests__/useLegacyPaginationFragment-test.js index b2fc38e6964bb..e44cc89839760 100644 --- a/packages/relay-experimental/__tests__/useLegacyPaginationFragment-test.js +++ b/packages/relay-experimental/__tests__/useLegacyPaginationFragment-test.js @@ -226,8 +226,6 @@ describe('useLegacyPaginationFragment', () => { $first: Int $before: ID $last: Int - $orderby: [String] - $isViewerFriend: Boolean ) { node(id: $id) { ...UserFragment @arguments(isViewerFriendLocal: true, orderby: ["name"]) diff --git a/packages/relay-runtime/mutations/__tests__/validateMutation-test.js b/packages/relay-runtime/mutations/__tests__/validateMutation-test.js index 24eaa996e8f14..e4d347bad9990 100644 --- a/packages/relay-runtime/mutations/__tests__/validateMutation-test.js +++ b/packages/relay-runtime/mutations/__tests__/validateMutation-test.js @@ -333,8 +333,7 @@ describe('validateOptimisticResponse', () => { name: 'Handles Lists', mutation: generateAndCompile(` mutation ChangeNameMutation( - $input: ActorNameChangeInput!, - $myVar: Boolean!, + $input: ActorNameChangeInput! ) { actorNameChange(input: $input) { actor { diff --git a/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js b/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js index c471f6980c5c0..002b443e45c05 100644 --- a/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReferenceMarker-test.js @@ -415,7 +415,7 @@ describe('RelayReferenceMarker', () => { source = RelayRecordSource.create(data); const {FooQuery} = generateAndCompile( ` - query FooQuery($id: ID, $size: [Int]) { + query FooQuery($id: ID) { node(id: $id) { id __typename diff --git a/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js b/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js index c6390b4b264ff..0f186537fa312 100644 --- a/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js +++ b/packages/relay-runtime/store/__tests__/RelayResponseNormalizer-test.js @@ -1460,7 +1460,7 @@ const {ROOT_ID, ROOT_TYPE} = require('../RelayStoreUtils'); describe('Client Extensions', () => { const {StrippedQuery} = generateAndCompile( ` - query StrippedQuery($id: ID, $size: [Int]) { + query StrippedQuery($id: ID) { node(id: $id) { id __typename diff --git a/packages/relay-test-utils/__tests__/RelayMockEnvironmentWithComponents-test.js b/packages/relay-test-utils/__tests__/RelayMockEnvironmentWithComponents-test.js index 62920095d77e8..289fca080b8f6 100644 --- a/packages/relay-test-utils/__tests__/RelayMockEnvironmentWithComponents-test.js +++ b/packages/relay-test-utils/__tests__/RelayMockEnvironmentWithComponents-test.js @@ -43,7 +43,7 @@ describe('ReactRelayTestMocker with Containers', () => { beforeEach(() => { const {TestQuery} = generateAndCompile(` - query TestQuery($id: ID = "", $scale: Float = 1) { + query TestQuery($id: ID = "") { user: node(id: $id) { id name @@ -79,7 +79,6 @@ describe('ReactRelayTestMocker with Containers', () => { expect(operation.request.node.operation.name).toBe('TestQuery'); expect(operation.request.variables).toEqual({ id: '', - scale: 1, }); }); @@ -443,7 +442,7 @@ describe('ReactRelayTestMocker with Containers', () => { beforeEach(() => { const {UserQuery, PageQuery, PageFragment} = generateAndCompile(` - query UserQuery($id: ID = "", $first: Int = 5, $cursor: String = "") { + query UserQuery($id: ID = "") { user: node(id: $id) { id name