From 2a1ee4a54ce4c10b153e14f20ad55e343019b261 Mon Sep 17 00:00:00 2001 From: Daniel Rearden Date: Sat, 20 Jun 2020 15:14:39 -0400 Subject: [PATCH] Add NoDeprecatedCustomRule and deprecate findDeprecatedUsages --- .nycrc.yml | 1 + src/index.d.ts | 3 +- src/index.js | 3 +- .../__tests__/findDeprecatedUsages-test.js | 76 ----------- src/utilities/findDeprecatedUsages.d.ts | 10 +- src/utilities/findDeprecatedUsages.js | 54 ++------ src/utilities/index.d.ts | 2 +- src/utilities/index.js | 2 +- src/validation/ValidationContext.d.ts | 3 + src/validation/ValidationContext.js | 5 + .../__tests__/NoDeprecatedCustomRule-test.js | 119 ++++++++++++++++++ src/validation/index.d.ts | 1 + src/validation/index.js | 1 + .../rules/custom/NoDeprecatedCustomRule.d.ts | 14 +++ .../rules/custom/NoDeprecatedCustomRule.js | 51 ++++++++ 15 files changed, 222 insertions(+), 123 deletions(-) delete mode 100644 src/utilities/__tests__/findDeprecatedUsages-test.js create mode 100644 src/validation/__tests__/NoDeprecatedCustomRule-test.js create mode 100644 src/validation/rules/custom/NoDeprecatedCustomRule.d.ts create mode 100644 src/validation/rules/custom/NoDeprecatedCustomRule.js diff --git a/.nycrc.yml b/.nycrc.yml index 44aaed89e4..bb0a01a9ac 100644 --- a/.nycrc.yml +++ b/.nycrc.yml @@ -14,6 +14,7 @@ exclude: - 'src/validation/rules/UniqueFieldDefinitionNames.js' - 'src/validation/rules/UniqueTypeNames.js' - 'src/validation/rules/UniqueOperationTypes.js' + - 'src/utilities/findDeprecatedUsages.js' clean: true temp-directory: 'coverage/tests' report-dir: 'coverage/tests' diff --git a/src/index.d.ts b/src/index.d.ts index 5e16c7ec04..af81042c68 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -336,6 +336,7 @@ export { UniqueDirectiveNamesRule, PossibleTypeExtensionsRule, // Custom validation rules + NoDeprecatedCustomRule, NoSchemaIntrospectionCustomRule, ValidationRule, } from './validation/index'; @@ -416,7 +417,7 @@ export { DangerousChangeType, findBreakingChanges, findDangerousChanges, - // Report all deprecated usage within a GraphQL document. + // @deprecated: Report all deprecated usage within a GraphQL document. findDeprecatedUsages, } from './utilities/index'; diff --git a/src/index.js b/src/index.js index fc7d16bd69..432e16aefe 100644 --- a/src/index.js +++ b/src/index.js @@ -334,6 +334,7 @@ export { UniqueDirectiveNamesRule, PossibleTypeExtensionsRule, // Custom validation rules + NoDeprecatedCustomRule, NoSchemaIntrospectionCustomRule, } from './validation/index'; @@ -416,7 +417,7 @@ export { DangerousChangeType, findBreakingChanges, findDangerousChanges, - // Report all deprecated usage within a GraphQL document. + // @deprecated: Report all deprecated usage within a GraphQL document. findDeprecatedUsages, } from './utilities/index'; diff --git a/src/utilities/__tests__/findDeprecatedUsages-test.js b/src/utilities/__tests__/findDeprecatedUsages-test.js deleted file mode 100644 index f49c67589f..0000000000 --- a/src/utilities/__tests__/findDeprecatedUsages-test.js +++ /dev/null @@ -1,76 +0,0 @@ -// @flow strict - -import { expect } from 'chai'; -import { describe, it } from 'mocha'; - -import { parse } from '../../language/parser'; - -import { buildSchema } from '../buildASTSchema'; -import { findDeprecatedUsages } from '../findDeprecatedUsages'; - -describe('findDeprecatedUsages', () => { - const schema = buildSchema(` - enum EnumType { - NORMAL_VALUE - DEPRECATED_VALUE @deprecated(reason: "Some enum reason.") - } - - type Query { - normalField(enumArg: [EnumType]): String - deprecatedField: String @deprecated(reason: "Some field reason.") - } - `); - - it('should report empty set for no deprecated usages', () => { - const errors = findDeprecatedUsages( - schema, - parse('{ normalField(enumArg: [NORMAL_VALUE]) }'), - ); - - expect(errors.length).to.equal(0); - }); - - it('should ignore unknown stuff', () => { - const errors = findDeprecatedUsages( - schema, - parse(` - { - unknownField(unknownArg: UNKNOWN_VALUE) - normalField(enumArg: UNKNOWN_VALUE) - } - `), - ); - - expect(errors.length).to.equal(0); - }); - - it('should report usage of deprecated fields', () => { - const errors = findDeprecatedUsages( - schema, - parse('{ normalField, deprecatedField }'), - ); - - const errorMessages = errors.map((err) => err.message); - - expect(errorMessages).to.deep.equal([ - 'The field "Query.deprecatedField" is deprecated. Some field reason.', - ]); - }); - - it('should report usage of deprecated enums', () => { - const errors = findDeprecatedUsages( - schema, - parse(` - { - normalField(enumArg: [NORMAL_VALUE, DEPRECATED_VALUE]) - } - `), - ); - - const errorMessages = errors.map((err) => err.message); - - expect(errorMessages).to.deep.equal([ - 'The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason.', - ]); - }); -}); diff --git a/src/utilities/findDeprecatedUsages.d.ts b/src/utilities/findDeprecatedUsages.d.ts index 6beb244617..bbdf94391e 100644 --- a/src/utilities/findDeprecatedUsages.d.ts +++ b/src/utilities/findDeprecatedUsages.d.ts @@ -6,8 +6,16 @@ import { GraphQLSchema } from '../type/schema'; * A validation rule which reports deprecated usages. * * Returns a list of GraphQLError instances describing each deprecated use. + * + * @deprecated Please use `validate` with `NoDeprecatedCustomRule` instead: + * + * ``` + * import { validate, NoDeprecatedCustomRule } from 'graphql' + * + * const errors = validate(schema, document, [NoDeprecatedCustomRule]) + * ``` */ export function findDeprecatedUsages( schema: GraphQLSchema, ast: DocumentNode, -): Array; +): ReadonlyArray; diff --git a/src/utilities/findDeprecatedUsages.js b/src/utilities/findDeprecatedUsages.js index 5c0d815f46..296fb56b70 100644 --- a/src/utilities/findDeprecatedUsages.js +++ b/src/utilities/findDeprecatedUsages.js @@ -4,57 +4,27 @@ import { GraphQLError } from '../error/GraphQLError'; import type { DocumentNode } from '../language/ast'; -import { visit } from '../language/visitor'; - import type { GraphQLSchema } from '../type/schema'; -import { getNamedType } from '../type/definition'; - -import { TypeInfo, visitWithTypeInfo } from './TypeInfo'; +import { validate } from '../validation/validate'; +import { NoDeprecatedCustomRule } from '../validation/rules/custom/NoDeprecatedCustomRule'; /** * A validation rule which reports deprecated usages. * * Returns a list of GraphQLError instances describing each deprecated use. + * + * @deprecated Please use `validate` with `NoDeprecatedCustomRule` instead: + * + * ``` + * import { validate, NoDeprecatedCustomRule } from 'graphql' + * + * const errors = validate(schema, document, [NoDeprecatedCustomRule]) + * ``` */ export function findDeprecatedUsages( schema: GraphQLSchema, ast: DocumentNode, -): Array { - const errors = []; - const typeInfo = new TypeInfo(schema); - - visit( - ast, - visitWithTypeInfo(typeInfo, { - Field(node) { - const parentType = typeInfo.getParentType(); - const fieldDef = typeInfo.getFieldDef(); - if (parentType && fieldDef?.deprecationReason != null) { - errors.push( - new GraphQLError( - `The field "${parentType.name}.${fieldDef.name}" is deprecated. ` + - fieldDef.deprecationReason, - node, - ), - ); - } - }, - EnumValue(node) { - const type = getNamedType(typeInfo.getInputType()); - const enumVal = typeInfo.getEnumValue(); - if (type && enumVal?.deprecationReason != null) { - errors.push( - new GraphQLError( - `The enum value "${type.name}.${enumVal.name}" is deprecated. ` + - enumVal.deprecationReason, - node, - ), - ); - } - }, - }), - ); - - return errors; +): $ReadOnlyArray { + return validate(schema, ast, [NoDeprecatedCustomRule]); } diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index fadc54c67c..658a69ac0d 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.ts @@ -112,5 +112,5 @@ export { DangerousChange, } from './findBreakingChanges'; -// Report all deprecated usage within a GraphQL document. +// @deprecated: Report all deprecated usage within a GraphQL document. export { findDeprecatedUsages } from './findDeprecatedUsages'; diff --git a/src/utilities/index.js b/src/utilities/index.js index 994948393a..eb7c156696 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -111,5 +111,5 @@ export { } from './findBreakingChanges'; export type { BreakingChange, DangerousChange } from './findBreakingChanges'; -// Report all deprecated usage within a GraphQL document. +// @deprecated: Report all deprecated usage within a GraphQL document. export { findDeprecatedUsages } from './findDeprecatedUsages'; diff --git a/src/validation/ValidationContext.d.ts b/src/validation/ValidationContext.d.ts index 54e22fc700..e0ca546b0e 100644 --- a/src/validation/ValidationContext.d.ts +++ b/src/validation/ValidationContext.d.ts @@ -18,6 +18,7 @@ import { GraphQLCompositeType, GraphQLField, GraphQLArgument, + GraphQLEnumValue, } from '../type/definition'; import { TypeInfo } from '../utilities/TypeInfo'; @@ -90,6 +91,8 @@ export class ValidationContext extends ASTValidationContext { getDirective(): Maybe; getArgument(): Maybe; + + getEnumValue(): Maybe; } export type ValidationRule = (context: ValidationContext) => ASTVisitor; diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js index 30e0024169..585cbda662 100644 --- a/src/validation/ValidationContext.js +++ b/src/validation/ValidationContext.js @@ -25,6 +25,7 @@ import type { GraphQLCompositeType, GraphQLField, GraphQLArgument, + GraphQLEnumValue, } from '../type/definition'; import { TypeInfo, visitWithTypeInfo } from '../utilities/TypeInfo'; @@ -245,6 +246,10 @@ export class ValidationContext extends ASTValidationContext { getArgument(): ?GraphQLArgument { return this._typeInfo.getArgument(); } + + getEnumValue(): ?GraphQLEnumValue { + return this._typeInfo.getEnumValue(); + } } export type ValidationRule = (ValidationContext) => ASTVisitor; diff --git a/src/validation/__tests__/NoDeprecatedCustomRule-test.js b/src/validation/__tests__/NoDeprecatedCustomRule-test.js new file mode 100644 index 0000000000..6cf40ade17 --- /dev/null +++ b/src/validation/__tests__/NoDeprecatedCustomRule-test.js @@ -0,0 +1,119 @@ +// @flow strict + +import { describe, it } from 'mocha'; + +import { buildSchema } from '../../utilities/buildASTSchema'; + +import { NoDeprecatedCustomRule } from '../rules/custom/NoDeprecatedCustomRule'; + +import { expectValidationErrorsWithSchema } from './harness'; + +function expectErrors(queryStr) { + return expectValidationErrorsWithSchema( + schema, + NoDeprecatedCustomRule, + queryStr, + ); +} + +function expectValid(queryStr) { + expectErrors(queryStr).to.deep.equal([]); +} + +const schema = buildSchema(` + enum EnumType { + NORMAL_VALUE + DEPRECATED_VALUE @deprecated(reason: "Some enum reason.") + DEPRECATED_VALUE_WITH_NO_REASON @deprecated + } + + type Query { + normalField(enumArg: [EnumType]): String + deprecatedField: String @deprecated(reason: "Some field reason.") + deprecatedFieldWithNoReason: String @deprecated + } +`); + +describe('Validate: no deprecated', () => { + it('ignores fields and enum values that are not deprecated', () => { + expectValid(` + { + normalField(enumArg: [NORMAL_VALUE]) + } + `); + }); + + it('ignores unknown fields and enum values', () => { + expectValid(` + fragment UnknownFragment on UnknownType { + unknownField(unknownArg: UNKNOWN_VALUE) + } + + fragment QueryFragment on Query { + unknownField(unknownArg: UNKNOWN_VALUE) + normalField(enumArg: UNKNOWN_VALUE) + } + `); + }); + + it('reports error when a deprecated field is selected', () => { + expectErrors(` + { + normalField + deprecatedField + deprecatedFieldWithNoReason + } + `).to.deep.equal([ + { + message: + 'The field Query.deprecatedField is deprecated. Some field reason.', + locations: [{ line: 4, column: 9 }], + }, + { + message: + 'The field Query.deprecatedFieldWithNoReason is deprecated. No longer supported', + locations: [{ line: 5, column: 9 }], + }, + ]); + }); + + it('reports error when a deprecated enum value is used', () => { + expectErrors(` + { + normalField(enumArg: [NORMAL_VALUE, DEPRECATED_VALUE]) + normalField(enumArg: [DEPRECATED_VALUE_WITH_NO_REASON]) + } + `).to.deep.equal([ + { + message: + 'The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason.', + locations: [{ line: 3, column: 45 }], + }, + { + message: + 'The enum value "EnumType.DEPRECATED_VALUE_WITH_NO_REASON" is deprecated. No longer supported', + locations: [{ line: 4, column: 31 }], + }, + ]); + }); + + it('reports error when a deprecated field is selected or an enum value is used inside a fragment', () => { + expectErrors(` + fragment QueryFragment on Query { + deprecatedField + normalField(enumArg: [NORMAL_VALUE, DEPRECATED_VALUE]) + } + `).to.deep.equal([ + { + message: + 'The field Query.deprecatedField is deprecated. Some field reason.', + locations: [{ line: 3, column: 9 }], + }, + { + message: + 'The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason.', + locations: [{ line: 4, column: 45 }], + }, + ]); + }); +}); diff --git a/src/validation/index.d.ts b/src/validation/index.d.ts index 191d1cc58c..f049bf397e 100644 --- a/src/validation/index.d.ts +++ b/src/validation/index.d.ts @@ -92,4 +92,5 @@ export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule'; export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule'; // Optional rules not defined by the GraphQL Specification +export { NoDeprecatedCustomRule } from './rules/custom/NoDeprecatedCustomRule'; export { NoSchemaIntrospectionCustomRule } from './rules/custom/NoSchemaIntrospectionCustomRule'; diff --git a/src/validation/index.js b/src/validation/index.js index ff4968581e..4c977fa1c3 100644 --- a/src/validation/index.js +++ b/src/validation/index.js @@ -96,4 +96,5 @@ export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule'; export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule'; // Optional rules not defined by the GraphQL Specification +export { NoDeprecatedCustomRule } from './rules/custom/NoDeprecatedCustomRule'; export { NoSchemaIntrospectionCustomRule } from './rules/custom/NoSchemaIntrospectionCustomRule'; diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.d.ts b/src/validation/rules/custom/NoDeprecatedCustomRule.d.ts new file mode 100644 index 0000000000..d376cf8572 --- /dev/null +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.d.ts @@ -0,0 +1,14 @@ +import { ASTVisitor } from '../../../language/visitor'; +import { ValidationContext } from '../../ValidationContext'; + +/** + * No deprecated + * + * A GraphQL document is only valid if all selected fields and all used enum values have not been + * deprecated. + * + * Note: This rule is optional and is not part of the Validation section of the GraphQL + * Specification. The main purpose of this rule is detection of deprecated usages and not + * necessarily to forbid their use when querying a service. + */ +export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor; diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.js b/src/validation/rules/custom/NoDeprecatedCustomRule.js new file mode 100644 index 0000000000..244c18ec1a --- /dev/null +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.js @@ -0,0 +1,51 @@ +// @flow strict + +import { GraphQLError } from '../../../error/GraphQLError'; + +import type { EnumValueNode, FieldNode } from '../../../language/ast'; +import type { ASTVisitor } from '../../../language/visitor'; + +import { getNamedType } from '../../../type/definition'; + +import type { ValidationContext } from '../../ValidationContext'; + +/** + * No deprecated + * + * A GraphQL document is only valid if all selected fields and all used enum values have not been + * deprecated. + * + * Note: This rule is optional and is not part of the Validation section of the GraphQL + * Specification. The main purpose of this rule is detection of deprecated usages and not + * necessarily to forbid their use when querying a service. + */ +export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { + return { + Field(node: FieldNode) { + const fieldDef = context.getFieldDef(); + const parentType = context.getParentType(); + if (parentType && fieldDef?.deprecationReason != null) { + context.reportError( + new GraphQLError( + `The field ${parentType.name}.${fieldDef.name} is deprecated. ` + + fieldDef.deprecationReason, + node, + ), + ); + } + }, + EnumValue(node: EnumValueNode) { + const type = getNamedType(context.getInputType()); + const enumValue = context.getEnumValue(); + if (type && enumValue?.deprecationReason != null) { + context.reportError( + new GraphQLError( + `The enum value "${type.name}.${enumValue.name}" is deprecated. ` + + enumValue.deprecationReason, + node, + ), + ); + } + }, + }; +}