From 45bb6e57f3371ddbcca8d5714d7190695d1f1512 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 8 May 2026 09:04:57 +0300 Subject: [PATCH] feat(validation): reject directive definition cycles --- src/index.ts | 1 + src/utilities/__tests__/extendSchema-test.ts | 2 +- .../NoDirectiveDefinitionCyclesRule-test.ts | 432 ++++++++++++++++++ src/validation/index.ts | 1 + .../rules/NoDirectiveDefinitionCyclesRule.ts | 319 +++++++++++++ src/validation/specifiedRules.ts | 3 + 6 files changed, 757 insertions(+), 1 deletion(-) create mode 100644 src/validation/__tests__/NoDirectiveDefinitionCyclesRule-test.ts create mode 100644 src/validation/rules/NoDirectiveDefinitionCyclesRule.ts diff --git a/src/index.ts b/src/index.ts index 219478ad42..8b83c799ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -476,6 +476,7 @@ export { UniqueArgumentDefinitionNamesRule, UniqueDirectiveNamesRule, PossibleTypeExtensionsRule, + NoDirectiveDefinitionCyclesRule, // Custom validation rules NoDeprecatedCustomRule, NoSchemaIntrospectionCustomRule, diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index da3fe77b2e..9e5c900fc8 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -316,7 +316,7 @@ describe('extendSchema', () => { someScalar(arg: SomeScalar): SomeScalar } - directive @foo(arg: SomeScalar) on SCALAR + directive @foo on SCALAR input FooInput { foo: SomeScalar diff --git a/src/validation/__tests__/NoDirectiveDefinitionCyclesRule-test.ts b/src/validation/__tests__/NoDirectiveDefinitionCyclesRule-test.ts new file mode 100644 index 0000000000..78aef76b22 --- /dev/null +++ b/src/validation/__tests__/NoDirectiveDefinitionCyclesRule-test.ts @@ -0,0 +1,432 @@ +import { describe, it } from 'node:test'; + +import { expectJSON } from '../../__testUtils__/expectJSON.ts'; + +import { parse } from '../../language/parser.ts'; + +import type { GraphQLSchema } from '../../type/schema.ts'; + +import { buildSchema } from '../../utilities/buildASTSchema.ts'; + +import { NoDirectiveDefinitionCyclesRule } from '../rules/NoDirectiveDefinitionCyclesRule.ts'; +import { validateSDL } from '../validate.ts'; + +function expectErrors( + sdlStr: string, + schema?: GraphQLSchema, + parseOptions?: { experimentalDirectivesOnDirectiveDefinitions?: boolean }, +) { + const doc = parse(sdlStr, parseOptions); + const errors = validateSDL(doc, schema, [NoDirectiveDefinitionCyclesRule]); + return expectJSON(errors); +} + +function expectValid( + sdlStr: string, + schema?: GraphQLSchema, + parseOptions?: { experimentalDirectivesOnDirectiveDefinitions?: boolean }, +) { + expectErrors(sdlStr, schema, parseOptions).toDeepEqual([]); +} + +describe('Validate: No directive definition cycles', () => { + it('single reference is valid', () => { + expectValid(` + directive @a(arg: String @b) on FIELD_DEFINITION + directive @b on ARGUMENT_DEFINITION + `); + }); + + it('does not false positive on unknown directive', () => { + expectValid(` + directive @a(arg: String @unknown) on FIELD_DEFINITION + `); + }); + + it('rejects a self-referential directive definition', () => { + expectErrors(` + directive @self(arg: String @self) on FIELD_DEFINITION + `).toDeepEqual([ + { + message: 'Cannot reference directive "@self" within itself.', + locations: [{ line: 2, column: 35 }], + }, + ]); + }); + + it('rejects directives applied to their own definitions', () => { + expectErrors( + ` + directive @self @self on DIRECTIVE_DEFINITION + `, + undefined, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: 'Cannot reference directive "@self" within itself.', + locations: [{ line: 2, column: 25 }], + }, + ]); + }); + + it('rejects directive definitions with circular references', () => { + expectErrors(` + directive @a(arg: String @b) on FIELD_DEFINITION + directive @b(arg: String @a) on FIELD_DEFINITION + `).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@a".', + locations: [ + { line: 2, column: 32 }, + { line: 3, column: 32 }, + ], + }, + ]); + }); + + it('rejects directive definitions with overlapping circular references', () => { + expectErrors(` + directive @a(arg: String @b) on FIELD_DEFINITION + directive @b(arg: String @c) on FIELD_DEFINITION + directive @c(first: String @a, second: String @d) on FIELD_DEFINITION + directive @d(arg: String @b) on FIELD_DEFINITION + `).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@c", "@a".', + locations: [ + { line: 2, column: 32 }, + { line: 3, column: 32 }, + { line: 4, column: 34 }, + ], + }, + { + message: + 'Cannot reference directive "@b" within itself through a series of directive applications: "@c", "@d", "@b".', + locations: [ + { line: 3, column: 32 }, + { line: 4, column: 53 }, + { line: 5, column: 32 }, + ], + }, + ]); + }); + + it('rejects directive definitions with multiple cycles through the same directive', () => { + expectErrors(` + directive @a(first: String @b, second: String @c) on FIELD_DEFINITION + directive @b(arg: String @a) on FIELD_DEFINITION + directive @c(arg: String @a) on FIELD_DEFINITION + `).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@a".', + locations: [ + { line: 2, column: 34 }, + { line: 3, column: 32 }, + ], + }, + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@c", "@a".', + locations: [ + { line: 2, column: 53 }, + { line: 4, column: 32 }, + ], + }, + ]); + }); + + it('rejects directive definitions that recurse through a directive on a referenced type', () => { + expectErrors(` + directive @a(arg: InputObject) on INPUT_OBJECT + + input InputObject @a { + value: String + } + `).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@a".', + locations: [ + { line: 2, column: 25 }, + { line: 4, column: 25 }, + ], + }, + ]); + }); + + it('rejects directive definitions that recurse through a referenced type', () => { + expectErrors(` + directive @a(arg: InputObject) on FIELD_DEFINITION + + input InputObject { + value: String @a + } + `).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@a".', + locations: [ + { line: 2, column: 25 }, + { line: 5, column: 23 }, + ], + }, + ]); + }); + + it('does not duplicate cycles through recursive referenced types', () => { + expectErrors(` + directive @a(arg: InputObject) on INPUT_FIELD_DEFINITION + input InputObject { + self: InputObject @a + } + `).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@a".', + locations: [ + { line: 2, column: 25 }, + { line: 4, column: 27 }, + ], + }, + ]); + }); + + it('rejects type extensions that create cycles with existing directives', () => { + const schema = buildSchema( + ` + directive @a(arg: InputObject) on INPUT_FIELD_DEFINITION + input InputObject { + value: String + } + `, + { noLocation: true }, + ); + + expectErrors( + ` + extend input InputObject { + recursive: String @a + } + `, + schema, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@a".', + locations: [{ line: 3, column: 29 }], + }, + ]); + }); + + it('rejects directives on directive definitions when the syntax exists', () => { + expectErrors( + ` + directive @a @b on DIRECTIVE_DEFINITION + directive @b @a on DIRECTIVE_DEFINITION + `, + undefined, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@a".', + locations: [ + { line: 2, column: 22 }, + { line: 3, column: 22 }, + ], + }, + ]); + }); + + it('rejects directive extensions with circular references', () => { + const schema = buildSchema( + ` + directive @a on DIRECTIVE_DEFINITION + directive @b on DIRECTIVE_DEFINITION + `, + { + noLocation: true, + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ); + + expectErrors( + ` + extend directive @a @b + extend directive @b @a + `, + schema, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@a".', + locations: [ + { line: 2, column: 29 }, + { line: 3, column: 29 }, + ], + }, + ]); + }); + + it('rejects directive extensions that close cycles through stored directive definitions', () => { + const schema = buildSchema( + ` + directive @a @b on DIRECTIVE_DEFINITION + directive @b on DIRECTIVE_DEFINITION + `, + { + noLocation: true, + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ); + + expectErrors( + ` + extend directive @b @a + `, + schema, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@a".', + locations: [{ line: 2, column: 29 }], + }, + ]); + }); + + it('rejects directive extensions that close cycles through stored directive extensions', () => { + const schema = buildSchema( + ` + directive @a on DIRECTIVE_DEFINITION + directive @b on DIRECTIVE_DEFINITION + extend directive @a @b + `, + { + noLocation: true, + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ); + + expectErrors( + ` + extend directive @b @a + `, + schema, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of directive applications: "@b", "@a".', + locations: [{ line: 2, column: 29 }], + }, + ]); + }); + + it('rejects directive extensions that close cycles through stored type definitions', () => { + const schema = buildSchema( + ` + directive @a(arg: InputObject) on INPUT_FIELD_DEFINITION + input InputObject { + field: String @b + } + directive @b on INPUT_FIELD_DEFINITION + `, + { noLocation: true }, + ); + + expectErrors( + ` + extend directive @b @a + `, + schema, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@b", "@a".', + locations: [{ line: 2, column: 29 }], + }, + ]); + }); + + it('rejects directive extensions that close cycles through stored type extensions', () => { + const schema = buildSchema( + ` + directive @a(arg: InputObject) on DIRECTIVE_DEFINITION + input InputObject { + value: String + } + extend input InputObject @b { + field: String + } + directive @b on INPUT_OBJECT + `, + { noLocation: true }, + ); + + expectErrors( + ` + extend directive @b @a + `, + schema, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@b", "@a".', + locations: [{ line: 2, column: 29 }], + }, + ]); + }); + + it('rejects directive extensions that close cycles through stored input object extension fields', () => { + const schema = buildSchema( + ` + directive @a(arg: InputObject) on DIRECTIVE_DEFINITION + input InputObject { + value: String + } + extend input InputObject { + field: String @b + } + directive @b on INPUT_FIELD_DEFINITION + `, + { noLocation: true }, + ); + + expectErrors( + ` + extend directive @b @a + `, + schema, + { + experimentalDirectivesOnDirectiveDefinitions: true, + }, + ).toDeepEqual([ + { + message: + 'Cannot reference directive "@a" within itself through a series of references: "InputObject", "@b", "@a".', + locations: [{ line: 2, column: 29 }], + }, + ]); + }); +}); diff --git a/src/validation/index.ts b/src/validation/index.ts index a7882a1f29..095875cbdd 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -118,6 +118,7 @@ export { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNam export { UniqueArgumentDefinitionNamesRule } from './rules/UniqueArgumentDefinitionNamesRule.ts'; export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule.ts'; export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule.ts'; +export { NoDirectiveDefinitionCyclesRule } from './rules/NoDirectiveDefinitionCyclesRule.ts'; // Optional rules not defined by the GraphQL Specification export { NoDeprecatedCustomRule } from './rules/custom/NoDeprecatedCustomRule.ts'; diff --git a/src/validation/rules/NoDirectiveDefinitionCyclesRule.ts b/src/validation/rules/NoDirectiveDefinitionCyclesRule.ts new file mode 100644 index 0000000000..ea3a883fef --- /dev/null +++ b/src/validation/rules/NoDirectiveDefinitionCyclesRule.ts @@ -0,0 +1,319 @@ +/** @category Validation Rules */ + +import type { ObjMap } from '../../jsutils/ObjMap.ts'; + +import { GraphQLError } from '../../error/GraphQLError.ts'; + +import type { + ConstDirectiveNode, + DirectiveDefinitionNode, + DirectiveExtensionNode, + EnumValueDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, + NamedTypeNode, + TypeDefinitionNode, + TypeExtensionNode, + TypeNode, +} from '../../language/ast.ts'; +import { Kind } from '../../language/kinds.ts'; +import type { ASTVisitor } from '../../language/visitor.ts'; + +import type { SDLValidationContext } from '../ValidationContext.ts'; + +type ReferenceNode = ConstDirectiveNode | NamedTypeNode; +type ReferenceOwnerNode = + | DirectiveDefinitionNode + | DirectiveExtensionNode + | TypeDefinitionNode + | TypeExtensionNode; + +interface Reference { + readonly key: string; + readonly node: ReferenceNode; + readonly isFromDocument: boolean; +} + +/** + * No directive definition cycles + * + * The graph of directives used within directive definitions must not form any + * cycles including referencing itself. This includes directives used on + * directive arguments and, when the experimental syntax is enabled, directives + * applied directly to directive definitions and extensions. + * + * See https://spec.graphql.org/draft/#sec-Type-System.Directives + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { NoDirectiveDefinitionCyclesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * directive @a(arg: String @b) on ARGUMENT_DEFINITION + * directive @b(arg: String @a) on ARGUMENT_DEFINITION + * type Query { name: String } + * `; + * + * NoDirectiveDefinitionCyclesRule.name; // => 'NoDirectiveDefinitionCyclesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * directive @a(arg: String @b) on FIELD_DEFINITION + * directive @b on ARGUMENT_DEFINITION + * type Query { name: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` + */ +export function NoDirectiveDefinitionCyclesRule( + context: SDLValidationContext, +): ASTVisitor { + const visitedDirectives: ObjMap = Object.create(null); + const referencePath: Array = []; + const referencePathIndexByKey: ObjMap = + Object.create(null); + + const referencesByKey: ObjMap> = Object.create(null); + + const schema = context.getSchema(); + if (schema != null) { + for (const directive of schema.getDirectives()) { + const key = '@' + directive.name; + for (const node of [directive.astNode, ...directive.extensionASTNodes]) { + if (node != null) { + addReferenceOwnerReferences(key, node, false); + } + } + } + + for (const type of Object.values(schema.getTypeMap())) { + for (const node of [type.astNode, ...type.extensionASTNodes]) { + if (node != null) { + addReferenceOwnerReferences(type.name, node, false); + } + } + } + } + + return { + DirectiveDefinition: collectReferenceOwnerReferences, + DirectiveExtension: collectReferenceOwnerReferences, + ScalarTypeDefinition: collectReferenceOwnerReferences, + ScalarTypeExtension: collectReferenceOwnerReferences, + ObjectTypeDefinition: collectReferenceOwnerReferences, + ObjectTypeExtension: collectReferenceOwnerReferences, + InterfaceTypeDefinition: collectReferenceOwnerReferences, + InterfaceTypeExtension: collectReferenceOwnerReferences, + UnionTypeDefinition: collectReferenceOwnerReferences, + UnionTypeExtension: collectReferenceOwnerReferences, + EnumTypeDefinition: collectReferenceOwnerReferences, + EnumTypeExtension: collectReferenceOwnerReferences, + InputObjectTypeDefinition: collectReferenceOwnerReferences, + InputObjectTypeExtension: collectReferenceOwnerReferences, + Document: { + leave() { + for (const key of Object.keys(referencesByKey)) { + if (key.startsWith('@')) { + detectCycleRecursive(key); + } + } + }, + }, + }; + + function collectReferenceOwnerReferences(node: ReferenceOwnerNode): false { + const key = + node.kind === Kind.DIRECTIVE_DEFINITION || + node.kind === Kind.DIRECTIVE_EXTENSION + ? '@' + node.name.value + : node.name.value; + + addReferenceOwnerReferences(key, node, true); + return false; + } + + function detectCycleRecursive(key: string): void { + if (key.startsWith('@')) { + if (visitedDirectives[key]) { + return; + } + visitedDirectives[key] = true; + } + + referencePathIndexByKey[key] = referencePath.length; + + for (const reference of referencesByKey[key] ?? []) { + const cycleIndex = referencePathIndexByKey[reference.key]; + + referencePath.push(reference); + if (cycleIndex === undefined) { + detectCycleRecursive(reference.key); + } else if (reference.key.startsWith('@')) { + const cyclePath = referencePath.slice(cycleIndex); + if (cyclePath.some((cycleReference) => cycleReference.isFromDocument)) { + reportCycle( + reference.key.slice(1), + cyclePath.map((cycleReference) => cycleReference.node), + ); + } + } + referencePath.pop(); + } + + referencePathIndexByKey[key] = undefined; + } + + function addReferenceOwnerReferences( + key: string, + node: ReferenceOwnerNode, + isFromDocument: boolean, + ): void { + addDirectiveReferences(key, node.directives, isFromDocument); + + switch (node.kind) { + case Kind.DIRECTIVE_DEFINITION: + addInputValueDefinitionReferences(key, node.arguments, isFromDocument); + break; + + case Kind.DIRECTIVE_EXTENSION: + case Kind.SCALAR_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_EXTENSION: + break; + + case Kind.OBJECT_TYPE_DEFINITION: + case Kind.OBJECT_TYPE_EXTENSION: + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.INTERFACE_TYPE_EXTENSION: + addNamedTypeReferences(key, node.interfaces, isFromDocument); + addFieldDefinitionReferences(key, node.fields, isFromDocument); + break; + + case Kind.UNION_TYPE_DEFINITION: + case Kind.UNION_TYPE_EXTENSION: + addNamedTypeReferences(key, node.types, isFromDocument); + break; + + case Kind.ENUM_TYPE_DEFINITION: + case Kind.ENUM_TYPE_EXTENSION: + addEnumValueDefinitionReferences(key, node.values, isFromDocument); + break; + + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + addInputValueDefinitionReferences(key, node.fields, isFromDocument); + break; + } + } + + function addFieldDefinitionReferences( + key: string, + fields: ReadonlyArray | undefined, + isFromDocument: boolean, + ): void { + for (const field of fields ?? []) { + addDirectiveReferences(key, field.directives, isFromDocument); + addInputValueDefinitionReferences(key, field.arguments, isFromDocument); + addTypeReference(key, field.type, isFromDocument); + } + } + + function addInputValueDefinitionReferences( + key: string, + inputValues: ReadonlyArray | undefined, + isFromDocument: boolean, + ): void { + for (const inputValue of inputValues ?? []) { + addDirectiveReferences(key, inputValue.directives, isFromDocument); + addTypeReference(key, inputValue.type, isFromDocument); + } + } + + function addEnumValueDefinitionReferences( + key: string, + enumValues: ReadonlyArray | undefined, + isFromDocument: boolean, + ): void { + for (const enumValue of enumValues ?? []) { + addDirectiveReferences(key, enumValue.directives, isFromDocument); + } + } + + function addDirectiveReferences( + key: string, + directives: ReadonlyArray | undefined, + isFromDocument: boolean, + ): void { + for (const directive of directives ?? []) { + addReference(key, directive, isFromDocument); + } + } + + function addNamedTypeReferences( + key: string, + nodes: ReadonlyArray | undefined, + isFromDocument: boolean, + ): void { + for (const node of nodes ?? []) { + addReference(key, node, isFromDocument); + } + } + + function addTypeReference( + key: string, + typeNode: TypeNode, + isFromDocument: boolean, + ): void { + let namedType = typeNode; + while ( + namedType.kind === Kind.LIST_TYPE || + namedType.kind === Kind.NON_NULL_TYPE + ) { + namedType = namedType.type; + } + + addReference(key, namedType, isFromDocument); + } + + function addReference( + key: string, + node: ReferenceNode, + isFromDocument: boolean, + ): void { + const referenceKey = + node.kind === Kind.DIRECTIVE ? '@' + node.name.value : node.name.value; + + referencesByKey[key] ??= []; + referencesByKey[key].push({ key: referenceKey, node, isFromDocument }); + } + + function reportCycle( + directiveName: string, + cyclePath: ReadonlyArray, + ): void { + const viaPath = cyclePath.slice(0, -1).map(formatReference).join(', '); + const referencesDescription = cyclePath.some( + (referenceNode) => referenceNode.kind === Kind.NAMED_TYPE, + ) + ? ' through a series of references' + : ' through a series of directive applications'; + + context.reportError( + new GraphQLError( + `Cannot reference directive "@${directiveName}" within itself` + + (viaPath !== '' + ? `${referencesDescription}: ${viaPath}, "@${directiveName}".` + : '.'), + { nodes: cyclePath }, + ), + ); + } + + function formatReference(referenceNode: ReferenceNode): string { + return referenceNode.kind === Kind.DIRECTIVE + ? '"@' + referenceNode.name.value + '"' + : '"' + referenceNode.name.value + '"'; + } +} diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index ebbda0df47..a3ec803664 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -31,6 +31,8 @@ import { LoneAnonymousOperationRule } from './rules/LoneAnonymousOperationRule.t import { LoneSchemaDefinitionRule } from './rules/LoneSchemaDefinitionRule.ts'; // TODO: Spec Section import { MaxIntrospectionDepthRule } from './rules/MaxIntrospectionDepthRule.ts'; +// Spec Section: "Directives" +import { NoDirectiveDefinitionCyclesRule } from './rules/NoDirectiveDefinitionCyclesRule.ts'; // Spec Section: "Fragments must not form cycles" import { NoFragmentCyclesRule } from './rules/NoFragmentCyclesRule.ts'; // Spec Section: "All Variable Used Defined" @@ -148,4 +150,5 @@ export const specifiedSDLRules: ReadonlyArray = UniqueArgumentNamesRule, UniqueInputFieldNamesRule, ProvidedRequiredArgumentsOnDirectivesRule, + NoDirectiveDefinitionCyclesRule, ]);