From 03377cb7bf2a1e8d6783d72fa929c5adbead8846 Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Fri, 8 Apr 2016 14:44:43 -0700 Subject: [PATCH 1/8] Add suggestionList to return strings based on how simular they are to the input --- .../__tests__/suggestionList-test.js | 28 +++++++ src/utilities/suggestionList.js | 78 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/utilities/__tests__/suggestionList-test.js create mode 100644 src/utilities/suggestionList.js diff --git a/src/utilities/__tests__/suggestionList-test.js b/src/utilities/__tests__/suggestionList-test.js new file mode 100644 index 0000000000..ee8d3ab083 --- /dev/null +++ b/src/utilities/__tests__/suggestionList-test.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { suggestionList } from '../suggestionList'; + +describe('suggestionList', () => { + + it('Returns results when input is empty', () => { + expect(suggestionList('', [ 'a' ])).to.deep.equal([ 'a' ]); + }); + + it('Returns empty array when there are no options', () => { + expect(suggestionList('input', [])).to.deep.equal([]); + }); + + it('Returns options sorted based on simularity', () => { + expect(suggestionList('abc', [ 'a', 'ab', 'abc' ])) + .to.deep.equal([ 'abc', 'ab', 'a' ]); + }); +}); diff --git a/src/utilities/suggestionList.js b/src/utilities/suggestionList.js new file mode 100644 index 0000000000..3b08031fea --- /dev/null +++ b/src/utilities/suggestionList.js @@ -0,0 +1,78 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * Given a JavaScript value and a GraphQL type, determine if the value will be + * accepted for that type. This is primarily useful for validating the + * runtime values of query variables. + */ +export function suggestionList( + input: string, + options: Array + ): Array { + let i; + const d = {}; + const oLength = options.length; + for (i = 0; i < oLength; i++) { + d[options[i]] = lexicalDistance(input, options[i]); + } + const result = options.slice(); + return result.sort((a , b) => d[a] - d[b]); +} + +/** + * Computes the lexical distance between strings A and B. + * + * The "distance" between two strings is given by counting the minimum number + * of edits needed to transform string A into string B. An edit can be an + * insertion, deletion, or substitution of a single character, or a swap of two + * adjacent characters. + * + * This distance can be useful for detecting typos in input or sorting + * + * @param {string} a + * @param {string} b + * @return {int} distance in number of edits + */ +function lexicalDistance(a, b) { + let i; + let j; + const d = []; + const aLength = a.length; + const bLength = b.length; + + for (i = 0; i <= aLength; i++) { + d[i] = [ i ]; + } + + for (j = 1; j <= bLength; j++) { + d[0][j] = j; + } + + for (i = 1; i <= aLength; i++) { + for (j = 1; j <= bLength; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + + d[i][j] = Math.min( + d[i - 1][j] + 1, + d[i][j - 1] + 1, + d[i - 1][j - 1] + cost + ); + + if (i > 1 && j > 1 && + a[i - 1] === b[j - 2] && + a[i - 2] === b[j - 1]) { + d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost); + } + } + } + + return d[aLength][bLength]; +} From c4e619f669664973ff55a86bc698f2004b26e224 Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Fri, 8 Apr 2016 14:46:59 -0700 Subject: [PATCH 2/8] Suggests valid fields in `FieldsOnCorrectType` --- .../__tests__/FieldsOnCorrectType-test.js | 146 +++++++++++++++--- src/validation/rules/FieldsOnCorrectType.js | 42 ++++- 2 files changed, 162 insertions(+), 26 deletions(-) diff --git a/src/validation/__tests__/FieldsOnCorrectType-test.js b/src/validation/__tests__/FieldsOnCorrectType-test.js index 496eece221..b1f7c0ca37 100644 --- a/src/validation/__tests__/FieldsOnCorrectType-test.js +++ b/src/validation/__tests__/FieldsOnCorrectType-test.js @@ -16,9 +16,21 @@ import { } from '../rules/FieldsOnCorrectType'; -function undefinedField(field, type, suggestions, line, column) { +function undefinedField( + field, + type, + suggestedTypes, + suggestedFields, + line, + column +) { return { - message: undefinedFieldMessage(field, type, suggestions), + message: undefinedFieldMessage( + field, + type, + suggestedTypes, + suggestedFields + ), locations: [ { line, column } ], }; } @@ -85,8 +97,16 @@ describe('Validate: Fields on correct type', () => { } } }`, - [ undefinedField('unknown_pet_field', 'Pet', [], 3, 9), - undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ] + [ undefinedField('unknown_pet_field', 'Pet', [], [ 'name' ], 3, 9), + undefinedField( + 'unknown_cat_field', + 'Cat', + [], + [ 'nickname', 'name', 'meowVolume', 'meows', 'furColor' ], + 5, + 13 + ) + ] ); }); @@ -95,7 +115,22 @@ describe('Validate: Fields on correct type', () => { fragment fieldNotDefined on Dog { meowVolume }`, - [ undefinedField('meowVolume', 'Dog', [], 3, 9) ] + [ undefinedField( + 'meowVolume', + 'Dog', + [], + [ 'barkVolume', + 'name', + 'nickname', + 'barks', + 'doesKnowCommand', + 'isAtLocation', + 'isHousetrained', + ], + 3, + 9 + ) + ] ); }); @@ -106,7 +141,22 @@ describe('Validate: Fields on correct type', () => { deeper_unknown_field } }`, - [ undefinedField('unknown_field', 'Dog', [], 3, 9) ] + [ undefinedField( + 'unknown_field', + 'Dog', + [], + [ 'nickname', + 'name', + 'barkVolume', + 'doesKnowCommand', + 'isHousetrained', + 'isAtLocation', + 'barks', + ], + 3, + 9 + ) + ] ); }); @@ -117,7 +167,7 @@ describe('Validate: Fields on correct type', () => { unknown_field } }`, - [ undefinedField('unknown_field', 'Pet', [], 4, 11) ] + [ undefinedField('unknown_field', 'Pet', [], [ 'name' ], 4, 11) ] ); }); @@ -128,7 +178,22 @@ describe('Validate: Fields on correct type', () => { meowVolume } }`, - [ undefinedField('meowVolume', 'Dog', [], 4, 11) ] + [ undefinedField( + 'meowVolume', + 'Dog', + [], + [ 'barkVolume', + 'name', + 'nickname', + 'barks', + 'doesKnowCommand', + 'isAtLocation', + 'isHousetrained', + ], + 4, + 11 + ) + ] ); }); @@ -137,7 +202,22 @@ describe('Validate: Fields on correct type', () => { fragment aliasedFieldTargetNotDefined on Dog { volume : mooVolume }`, - [ undefinedField('mooVolume', 'Dog', [], 3, 9) ] + [ undefinedField( + 'mooVolume', + 'Dog', + [], + [ 'barkVolume', + 'name', + 'nickname', + 'barks', + 'isAtLocation', + 'doesKnowCommand', + 'isHousetrained', + ], + 3, + 9 + ) + ] ); }); @@ -146,7 +226,22 @@ describe('Validate: Fields on correct type', () => { fragment aliasedLyingFieldTargetNotDefined on Dog { barkVolume : kawVolume }`, - [ undefinedField('kawVolume', 'Dog', [], 3, 9) ] + [ undefinedField( + 'kawVolume', + 'Dog', + [], + [ 'barkVolume', + 'name', + 'nickname', + 'barks', + 'isAtLocation', + 'doesKnowCommand', + 'isHousetrained', + ], + 3, + 9 + ) + ] ); }); @@ -155,7 +250,7 @@ describe('Validate: Fields on correct type', () => { fragment notDefinedOnInterface on Pet { tailLength }`, - [ undefinedField('tailLength', 'Pet', [], 3, 9) ] + [ undefinedField('tailLength', 'Pet', [], [ 'name' ], 3, 9) ] ); }); @@ -164,7 +259,7 @@ describe('Validate: Fields on correct type', () => { fragment definedOnImplementorsButNotInterface on Pet { nickname }`, - [ undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9) ] + [ undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], [ 'name' ], 3, 9) ] ); }); @@ -181,7 +276,7 @@ describe('Validate: Fields on correct type', () => { fragment directFieldSelectionOnUnion on CatOrDog { directField }`, - [ undefinedField('directField', 'CatOrDog', [], 3, 9) ] + [ undefinedField('directField', 'CatOrDog', [], [], 3, 9) ] ); }); @@ -195,6 +290,7 @@ describe('Validate: Fields on correct type', () => { 'name', 'CatOrDog', [ 'Being', 'Pet', 'Canine', 'Cat', 'Dog' ], + [], 3, 9 ) @@ -218,25 +314,33 @@ describe('Validate: Fields on correct type', () => { describe('Fields on correct type error message', () => { it('Works with no suggestions', () => { expect( - undefinedFieldMessage('T', 'f', []) - ).to.equal('Cannot query field "T" on type "f".'); + undefinedFieldMessage('f', 'T', [], []) + ).to.equal('Cannot query field "f" on type "T".'); }); it('Works with no small numbers of suggestions', () => { expect( - undefinedFieldMessage('T', 'f', [ 'A', 'B' ]) - ).to.equal('Cannot query field "T" on type "f". ' + + undefinedFieldMessage('f', 'T', [ 'A', 'B' ], [ 'z', 'y' ]) + ).to.equal('Cannot query field "f" on type "T". ' + 'However, this field exists on "A", "B". ' + - 'Perhaps you meant to use an inline fragment?'); + 'Perhaps you meant to use an inline fragment? ' + + 'Did you mean to query "z", "y"?'); }); it('Works with lots of suggestions', () => { expect( - undefinedFieldMessage('T', 'f', [ 'A', 'B', 'C', 'D', 'E', 'F' ]) - ).to.equal('Cannot query field "T" on type "f". ' + + undefinedFieldMessage( + 'f', + 'T', + [ 'A', 'B', 'C', 'D', 'E', 'F' ], + [ 'z', 'y', 'x', 'w', 'v', 'u' ] + ) + ).to.equal('Cannot query field "f" on type "T". ' + 'However, this field exists on "A", "B", "C", "D", "E", ' + 'and 1 other types. ' + - 'Perhaps you meant to use an inline fragment?'); + 'Perhaps you meant to use an inline fragment? ' + + 'Did you mean to query "z", "y", "x", "w", "v", or 1 other field?' + ); }); }); }); diff --git a/src/validation/rules/FieldsOnCorrectType.js b/src/validation/rules/FieldsOnCorrectType.js index f2962afa68..275d24980d 100644 --- a/src/validation/rules/FieldsOnCorrectType.js +++ b/src/validation/rules/FieldsOnCorrectType.js @@ -13,13 +13,20 @@ import { GraphQLError } from '../../error'; import type { Field } from '../../language/ast'; import type { GraphQLSchema } from '../../type/schema'; import type { GraphQLAbstractType } from '../../type/definition'; -import { isAbstractType } from '../../type/definition'; +import { + isAbstractType, + GraphQLObjectType, + GraphQLInputObjectType, + GraphQLInterfaceType +} from '../../type/definition'; +import { suggestionList } from '../../utilities/suggestionList'; export function undefinedFieldMessage( fieldName: string, type: string, - suggestedTypes: Array + suggestedTypes: Array, + suggestedFields: Array ): string { let message = `Cannot query field "${fieldName}" on type "${type}".`; const MAX_LENGTH = 5; @@ -34,6 +41,18 @@ export function undefinedFieldMessage( message += ` However, this field exists on ${suggestions}.`; message += ' Perhaps you meant to use an inline fragment?'; } + if (suggestedFields.length !== 0) { + let suggestions = suggestedFields + .slice(0, MAX_LENGTH) + .map(t => `"${t}"`) + .join(', '); + if (suggestedFields.length > MAX_LENGTH) { + suggestions += `, or ${suggestedFields.length - MAX_LENGTH} other field`; + } + message += + ` Did you mean to query ${suggestions}?`; + + } return message; } @@ -50,10 +69,10 @@ export function FieldsOnCorrectType(context: ValidationContext): any { if (type) { const fieldDef = context.getFieldDef(); if (!fieldDef) { + const schema = context.getSchema(); // This isn't valid. Let's find suggestions, if any. let suggestedTypes = []; if (isAbstractType(type)) { - const schema = context.getSchema(); suggestedTypes = getSiblingInterfacesIncludingField( schema, type, @@ -63,8 +82,22 @@ export function FieldsOnCorrectType(context: ValidationContext): any { getImplementationsIncludingField(schema, type, node.name.value) ); } + let suggestedFields = []; + if (type instanceof GraphQLObjectType || + type instanceof GraphQLInterfaceType || + type instanceof GraphQLInputObjectType) { + suggestedFields = suggestionList( + node.name.value, + Object.keys(schema.getType(type.name).getFields()) + ); + } context.reportError(new GraphQLError( - undefinedFieldMessage(node.name.value, type.name, suggestedTypes), + undefinedFieldMessage( + node.name.value, + type.name, + suggestedTypes, + suggestedFields + ), [ node ] )); } @@ -113,4 +146,3 @@ function getSiblingInterfacesIncludingField( return Object.keys(suggestedInterfaces) .sort((a,b) => suggestedInterfaces[b] - suggestedInterfaces[a]); } - From e25fe3e1b1524e464775e44cad149afe1614e617 Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Tue, 12 Apr 2016 12:05:24 -0400 Subject: [PATCH 3/8] Suggest argument names --- .../__tests__/KnownArgumentNames-test.js | 26 ++++++----- src/validation/rules/KnownArgumentNames.js | 45 ++++++++++++++++--- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/src/validation/__tests__/KnownArgumentNames-test.js b/src/validation/__tests__/KnownArgumentNames-test.js index bc9fec1ec9..56df9580ef 100644 --- a/src/validation/__tests__/KnownArgumentNames-test.js +++ b/src/validation/__tests__/KnownArgumentNames-test.js @@ -16,16 +16,22 @@ import { } from '../rules/KnownArgumentNames'; -function unknownArg(argName, fieldName, typeName, line, column) { +function unknownArg(argName, fieldName, typeName, suggestedArgs, line, column) { return { - message: unknownArgMessage(argName, fieldName, typeName), + message: unknownArgMessage(argName, fieldName, typeName, suggestedArgs), locations: [ { line, column } ], }; } -function unknownDirectiveArg(argName, directiveName, line, column) { +function unknownDirectiveArg( + argName, + directiveName, + suggestedArgs, + line, + column +) { return { - message: unknownDirectiveArgMessage(argName, directiveName), + message: unknownDirectiveArgMessage(argName, directiveName, suggestedArgs), locations: [ { line, column } ], }; } @@ -103,7 +109,7 @@ describe('Validate: Known argument names', () => { dog @skip(unless: true) } `, [ - unknownDirectiveArg('unless', 'skip', 3, 19), + unknownDirectiveArg('unless', 'skip', [], 3, 19), ]); }); @@ -113,7 +119,7 @@ describe('Validate: Known argument names', () => { doesKnowCommand(unknown: true) } `, [ - unknownArg('unknown', 'doesKnowCommand', 'Dog', 3, 25), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 3, 25), ]); }); @@ -123,8 +129,8 @@ describe('Validate: Known argument names', () => { doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true) } `, [ - unknownArg('whoknows', 'doesKnowCommand', 'Dog', 3, 25), - unknownArg('unknown', 'doesKnowCommand', 'Dog', 3, 55), + unknownArg('whoknows', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 3, 25), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 3, 55), ]); }); @@ -143,8 +149,8 @@ describe('Validate: Known argument names', () => { } } `, [ - unknownArg('unknown', 'doesKnowCommand', 'Dog', 4, 27), - unknownArg('unknown', 'doesKnowCommand', 'Dog', 9, 31), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 4, 27), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 9, 31), ]); }); diff --git a/src/validation/rules/KnownArgumentNames.js b/src/validation/rules/KnownArgumentNames.js index e389d65371..4594c54d44 100644 --- a/src/validation/rules/KnownArgumentNames.js +++ b/src/validation/rules/KnownArgumentNames.js @@ -17,22 +17,42 @@ import { DIRECTIVE } from '../../language/kinds'; import type { GraphQLType } from '../../type/definition'; - +import { suggestionList } from '../../utilities/suggestionList'; export function unknownArgMessage( argName: string, fieldName: string, - type: GraphQLType + type: GraphQLType, + suggestedArgs: Array ): string { - return `Unknown argument "${argName}" on field "${fieldName}" of ` + + let message = `Unknown argument "${argName}" on field "${fieldName}" of ` + `type "${type}".`; + if (suggestedArgs.length) { + const suggestions = suggestedArgs + .map(t => `"${t}"`) + .join(', '); + message += ` Perhaps you meant ${suggestions}?`; + } else { + message += ' There is no known argument for this field.'; + } + return message; } export function unknownDirectiveArgMessage( argName: string, - directiveName: string + directiveName: string, + suggestedArgs: Array ): string { - return `Unknown argument "${argName}" on directive "@${directiveName}".`; + let message = + `Unknown argument "${argName}" on directive "@${directiveName}".`; + if (suggestedArgs.length) { + const suggestions = suggestedArgs + .map(t => `"${t}"`) + .join(', '); + message += ` Perhaps you meant ${suggestions}?`; + } else { + message += ' There is no known argument for this directive.'; + } } /** @@ -59,7 +79,11 @@ export function KnownArgumentNames(context: ValidationContext): any { unknownArgMessage( node.name.value, fieldDef.name, - parentType.name + parentType.name, + suggestionList( + node.name.value, + fieldDef.args.map(arg => arg.name) + ) ), [ node ] )); @@ -74,7 +98,14 @@ export function KnownArgumentNames(context: ValidationContext): any { ); if (!directiveArgDef) { context.reportError(new GraphQLError( - unknownDirectiveArgMessage(node.name.value, directive.name), + unknownDirectiveArgMessage( + node.name.value, + directive.name, + suggestionList( + node.name.value, + directive.args.map(arg => arg.name) + ) + ), [ node ] )); } From d460b744c39e03a6e59b1d3b0d08468a31aea7c7 Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Tue, 12 Apr 2016 13:21:11 -0400 Subject: [PATCH 4/8] Suggested valid type names --- .../__tests__/KnownTypeNames-test.js | 52 ++++++++++++++++--- src/validation/rules/KnownTypeNames.js | 29 +++++++++-- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/validation/__tests__/KnownTypeNames-test.js b/src/validation/__tests__/KnownTypeNames-test.js index e158ad3acb..a7e7b91f35 100644 --- a/src/validation/__tests__/KnownTypeNames-test.js +++ b/src/validation/__tests__/KnownTypeNames-test.js @@ -15,9 +15,9 @@ import { } from '../rules/KnownTypeNames'; -function unknownType(typeName, line, column) { +function unknownType(typeName, suggestedTypes, line, column) { return { - message: unknownTypeMessage(typeName), + message: unknownTypeMessage(typeName, suggestedTypes), locations: [ { line, column } ], }; } @@ -49,9 +49,39 @@ describe('Validate: Known type names', () => { name } `, [ - unknownType('JumbledUpLetters', 2, 23), - unknownType('Badger', 5, 25), - unknownType('Peettt', 8, 29) + unknownType( + 'JumbledUpLetters', + [ 'ComplexInput', + 'ComplicatedArgs', + 'QueryRoot', + 'Intelligent', + 'HumanOrAlien' + ], + 2, + 23 + ), + unknownType( + 'Badger', + [ 'Dog', + 'Boolean', + 'Pet', + 'Alien', + 'Being' + ], + 5, + 25 + ), + unknownType( + 'Peettt', + [ 'Pet', + 'Float', + 'Being', + 'Cat', + 'Int' + ], + 8, + 29 + ) ]); }); @@ -73,7 +103,17 @@ describe('Validate: Known type names', () => { } } `, [ - unknownType('NotInTheSchema', 12, 23), + unknownType( + 'NotInTheSchema', + [ '__Schema', + 'Intelligent', + 'HumanOrAlien', + 'Int', + 'Canine' + ], + 12, + 23 + ), ]); }); diff --git a/src/validation/rules/KnownTypeNames.js b/src/validation/rules/KnownTypeNames.js index 6ab367c3d9..701c318f22 100644 --- a/src/validation/rules/KnownTypeNames.js +++ b/src/validation/rules/KnownTypeNames.js @@ -11,10 +11,22 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import type { GraphQLType } from '../../type/definition'; +import { suggestionList } from '../../utilities/suggestionList'; - -export function unknownTypeMessage(type: GraphQLType): string { - return `Unknown type "${type}".`; +export function unknownTypeMessage( + type: GraphQLType, + suggestedTypes: Arrray +): string { + let message = `Unknown type "${type}".`; + const MAX_LENGTH = 5; + if (suggestedTypes.length) { + const suggestions = suggestedTypes + .slice(0, MAX_LENGTH) + .map(t => `"${t}"`) + .join(', '); + message += ` Perhaps you meant one of the following: ${suggestions}.`; + } + return message; } /** @@ -33,11 +45,18 @@ export function KnownTypeNames(context: ValidationContext): any { UnionTypeDefinition: () => false, InputObjectTypeDefinition: () => false, NamedType(node) { + const schema = context.getSchema(); const typeName = node.name.value; - const type = context.getSchema().getType(typeName); + const type = schema.getType(typeName); if (!type) { context.reportError( - new GraphQLError(unknownTypeMessage(typeName), [ node ]) + new GraphQLError( + unknownTypeMessage( + typeName, + suggestionList(typeName, Object.keys(schema.getTypeMap())) + ), + [ node ] + ) ); } } From 10997c5c7d3929afe77cc01ba05ca67414601823 Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Tue, 12 Apr 2016 13:30:42 -0400 Subject: [PATCH 5/8] Fix flow and unit test --- src/validation/__tests__/KnownArgumentNames-test.js | 2 +- src/validation/rules/KnownArgumentNames.js | 1 + src/validation/rules/KnownTypeNames.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/validation/__tests__/KnownArgumentNames-test.js b/src/validation/__tests__/KnownArgumentNames-test.js index 56df9580ef..922e79868b 100644 --- a/src/validation/__tests__/KnownArgumentNames-test.js +++ b/src/validation/__tests__/KnownArgumentNames-test.js @@ -109,7 +109,7 @@ describe('Validate: Known argument names', () => { dog @skip(unless: true) } `, [ - unknownDirectiveArg('unless', 'skip', [], 3, 19), + unknownDirectiveArg('unless', 'skip', [ 'if' ], 3, 19), ]); }); diff --git a/src/validation/rules/KnownArgumentNames.js b/src/validation/rules/KnownArgumentNames.js index 4594c54d44..d8578aff0b 100644 --- a/src/validation/rules/KnownArgumentNames.js +++ b/src/validation/rules/KnownArgumentNames.js @@ -53,6 +53,7 @@ export function unknownDirectiveArgMessage( } else { message += ' There is no known argument for this directive.'; } + return message; } /** diff --git a/src/validation/rules/KnownTypeNames.js b/src/validation/rules/KnownTypeNames.js index 701c318f22..dd7420b3b1 100644 --- a/src/validation/rules/KnownTypeNames.js +++ b/src/validation/rules/KnownTypeNames.js @@ -15,7 +15,7 @@ import { suggestionList } from '../../utilities/suggestionList'; export function unknownTypeMessage( type: GraphQLType, - suggestedTypes: Arrray + suggestedTypes: Array ): string { let message = `Unknown type "${type}".`; const MAX_LENGTH = 5; From 8982f88f3a411e29d8e7a159946df94021bae49d Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Tue, 12 Apr 2016 21:51:16 -0400 Subject: [PATCH 6/8] addressed comments in PR: move file, update comment, filter out more options, remove redundant warning --- .../__tests__/suggestionList-test.js | 2 +- src/{utilities => jsutils}/suggestionList.js | 14 +++-- .../__tests__/FieldsOnCorrectType-test.js | 53 ++++--------------- .../__tests__/KnownArgumentNames-test.js | 12 ++--- .../__tests__/KnownTypeNames-test.js | 28 ++-------- src/validation/rules/FieldsOnCorrectType.js | 4 +- src/validation/rules/KnownArgumentNames.js | 7 +-- src/validation/rules/KnownTypeNames.js | 3 +- 8 files changed, 35 insertions(+), 88 deletions(-) rename src/{utilities => jsutils}/__tests__/suggestionList-test.js (95%) rename src/{utilities => jsutils}/suggestionList.js (80%) diff --git a/src/utilities/__tests__/suggestionList-test.js b/src/jsutils/__tests__/suggestionList-test.js similarity index 95% rename from src/utilities/__tests__/suggestionList-test.js rename to src/jsutils/__tests__/suggestionList-test.js index ee8d3ab083..585431458c 100644 --- a/src/utilities/__tests__/suggestionList-test.js +++ b/src/jsutils/__tests__/suggestionList-test.js @@ -23,6 +23,6 @@ describe('suggestionList', () => { it('Returns options sorted based on simularity', () => { expect(suggestionList('abc', [ 'a', 'ab', 'abc' ])) - .to.deep.equal([ 'abc', 'ab', 'a' ]); + .to.deep.equal([ 'abc', 'ab' ]); }); }); diff --git a/src/utilities/suggestionList.js b/src/jsutils/suggestionList.js similarity index 80% rename from src/utilities/suggestionList.js rename to src/jsutils/suggestionList.js index 3b08031fea..cc524c17a5 100644 --- a/src/utilities/suggestionList.js +++ b/src/jsutils/suggestionList.js @@ -9,9 +9,8 @@ */ /** - * Given a JavaScript value and a GraphQL type, determine if the value will be - * accepted for that type. This is primarily useful for validating the - * runtime values of query variables. + * Given an invalid input string and a list of valid options, returns a filtered + * list of valid options sorted based on their simularity with the input. */ export function suggestionList( input: string, @@ -20,10 +19,15 @@ export function suggestionList( let i; const d = {}; const oLength = options.length; + const inputThreshold = input.length / 2; for (i = 0; i < oLength; i++) { - d[options[i]] = lexicalDistance(input, options[i]); + const distance = lexicalDistance(input, options[i]); + const threshold = Math.max(inputThreshold, options[i].length / 2, 1); + if (distance <= threshold) { + d[options[i]] = distance; + } } - const result = options.slice(); + const result = Object.keys(d); return result.sort((a , b) => d[a] - d[b]); } diff --git a/src/validation/__tests__/FieldsOnCorrectType-test.js b/src/validation/__tests__/FieldsOnCorrectType-test.js index b1f7c0ca37..b74133cd13 100644 --- a/src/validation/__tests__/FieldsOnCorrectType-test.js +++ b/src/validation/__tests__/FieldsOnCorrectType-test.js @@ -97,12 +97,12 @@ describe('Validate: Fields on correct type', () => { } } }`, - [ undefinedField('unknown_pet_field', 'Pet', [], [ 'name' ], 3, 9), + [ undefinedField('unknown_pet_field', 'Pet', [], [], 3, 9), undefinedField( 'unknown_cat_field', 'Cat', [], - [ 'nickname', 'name', 'meowVolume', 'meows', 'furColor' ], + [], 5, 13 ) @@ -119,14 +119,7 @@ describe('Validate: Fields on correct type', () => { 'meowVolume', 'Dog', [], - [ 'barkVolume', - 'name', - 'nickname', - 'barks', - 'doesKnowCommand', - 'isAtLocation', - 'isHousetrained', - ], + [ 'barkVolume' ], 3, 9 ) @@ -145,14 +138,7 @@ describe('Validate: Fields on correct type', () => { 'unknown_field', 'Dog', [], - [ 'nickname', - 'name', - 'barkVolume', - 'doesKnowCommand', - 'isHousetrained', - 'isAtLocation', - 'barks', - ], + [], 3, 9 ) @@ -167,7 +153,7 @@ describe('Validate: Fields on correct type', () => { unknown_field } }`, - [ undefinedField('unknown_field', 'Pet', [], [ 'name' ], 4, 11) ] + [ undefinedField('unknown_field', 'Pet', [], [], 4, 11) ] ); }); @@ -182,14 +168,7 @@ describe('Validate: Fields on correct type', () => { 'meowVolume', 'Dog', [], - [ 'barkVolume', - 'name', - 'nickname', - 'barks', - 'doesKnowCommand', - 'isAtLocation', - 'isHousetrained', - ], + [ 'barkVolume' ], 4, 11 ) @@ -206,14 +185,7 @@ describe('Validate: Fields on correct type', () => { 'mooVolume', 'Dog', [], - [ 'barkVolume', - 'name', - 'nickname', - 'barks', - 'isAtLocation', - 'doesKnowCommand', - 'isHousetrained', - ], + [ 'barkVolume' ], 3, 9 ) @@ -230,14 +202,7 @@ describe('Validate: Fields on correct type', () => { 'kawVolume', 'Dog', [], - [ 'barkVolume', - 'name', - 'nickname', - 'barks', - 'isAtLocation', - 'doesKnowCommand', - 'isHousetrained', - ], + [ 'barkVolume' ], 3, 9 ) @@ -250,7 +215,7 @@ describe('Validate: Fields on correct type', () => { fragment notDefinedOnInterface on Pet { tailLength }`, - [ undefinedField('tailLength', 'Pet', [], [ 'name' ], 3, 9) ] + [ undefinedField('tailLength', 'Pet', [], [], 3, 9) ] ); }); diff --git a/src/validation/__tests__/KnownArgumentNames-test.js b/src/validation/__tests__/KnownArgumentNames-test.js index 922e79868b..eeae9cef1f 100644 --- a/src/validation/__tests__/KnownArgumentNames-test.js +++ b/src/validation/__tests__/KnownArgumentNames-test.js @@ -109,7 +109,7 @@ describe('Validate: Known argument names', () => { dog @skip(unless: true) } `, [ - unknownDirectiveArg('unless', 'skip', [ 'if' ], 3, 19), + unknownDirectiveArg('unless', 'skip', [], 3, 19), ]); }); @@ -119,7 +119,7 @@ describe('Validate: Known argument names', () => { doesKnowCommand(unknown: true) } `, [ - unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 3, 25), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 3, 25), ]); }); @@ -129,8 +129,8 @@ describe('Validate: Known argument names', () => { doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true) } `, [ - unknownArg('whoknows', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 3, 25), - unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 3, 55), + unknownArg('whoknows', 'doesKnowCommand', 'Dog', [], 3, 25), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 3, 55), ]); }); @@ -149,8 +149,8 @@ describe('Validate: Known argument names', () => { } } `, [ - unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 4, 27), - unknownArg('unknown', 'doesKnowCommand', 'Dog', [ 'dogCommand' ], 9, 31), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 4, 27), + unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 9, 31), ]); }); diff --git a/src/validation/__tests__/KnownTypeNames-test.js b/src/validation/__tests__/KnownTypeNames-test.js index a7e7b91f35..eca4a0424c 100644 --- a/src/validation/__tests__/KnownTypeNames-test.js +++ b/src/validation/__tests__/KnownTypeNames-test.js @@ -51,34 +51,19 @@ describe('Validate: Known type names', () => { `, [ unknownType( 'JumbledUpLetters', - [ 'ComplexInput', - 'ComplicatedArgs', - 'QueryRoot', - 'Intelligent', - 'HumanOrAlien' - ], + [], 2, 23 ), unknownType( 'Badger', - [ 'Dog', - 'Boolean', - 'Pet', - 'Alien', - 'Being' - ], + [], 5, 25 ), unknownType( 'Peettt', - [ 'Pet', - 'Float', - 'Being', - 'Cat', - 'Int' - ], + [ 'Pet' ], 8, 29 ) @@ -105,12 +90,7 @@ describe('Validate: Known type names', () => { `, [ unknownType( 'NotInTheSchema', - [ '__Schema', - 'Intelligent', - 'HumanOrAlien', - 'Int', - 'Canine' - ], + [], 12, 23 ), diff --git a/src/validation/rules/FieldsOnCorrectType.js b/src/validation/rules/FieldsOnCorrectType.js index 275d24980d..9738ec33f2 100644 --- a/src/validation/rules/FieldsOnCorrectType.js +++ b/src/validation/rules/FieldsOnCorrectType.js @@ -10,6 +10,7 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import { suggestionList } from '../../jsutils/suggestionList'; import type { Field } from '../../language/ast'; import type { GraphQLSchema } from '../../type/schema'; import type { GraphQLAbstractType } from '../../type/definition'; @@ -19,7 +20,6 @@ import { GraphQLInputObjectType, GraphQLInterfaceType } from '../../type/definition'; -import { suggestionList } from '../../utilities/suggestionList'; export function undefinedFieldMessage( @@ -88,7 +88,7 @@ export function FieldsOnCorrectType(context: ValidationContext): any { type instanceof GraphQLInputObjectType) { suggestedFields = suggestionList( node.name.value, - Object.keys(schema.getType(type.name).getFields()) + Object.keys(type.getFields()) ); } context.reportError(new GraphQLError( diff --git a/src/validation/rules/KnownArgumentNames.js b/src/validation/rules/KnownArgumentNames.js index d8578aff0b..9f9c3dc11c 100644 --- a/src/validation/rules/KnownArgumentNames.js +++ b/src/validation/rules/KnownArgumentNames.js @@ -12,12 +12,13 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; import find from '../../jsutils/find'; import invariant from '../../jsutils/invariant'; +import { suggestionList } from '../../jsutils/suggestionList'; import { FIELD, DIRECTIVE } from '../../language/kinds'; import type { GraphQLType } from '../../type/definition'; -import { suggestionList } from '../../utilities/suggestionList'; + export function unknownArgMessage( argName: string, @@ -32,8 +33,6 @@ export function unknownArgMessage( .map(t => `"${t}"`) .join(', '); message += ` Perhaps you meant ${suggestions}?`; - } else { - message += ' There is no known argument for this field.'; } return message; } @@ -50,8 +49,6 @@ export function unknownDirectiveArgMessage( .map(t => `"${t}"`) .join(', '); message += ` Perhaps you meant ${suggestions}?`; - } else { - message += ' There is no known argument for this directive.'; } return message; } diff --git a/src/validation/rules/KnownTypeNames.js b/src/validation/rules/KnownTypeNames.js index dd7420b3b1..c8858ba15f 100644 --- a/src/validation/rules/KnownTypeNames.js +++ b/src/validation/rules/KnownTypeNames.js @@ -10,8 +10,9 @@ import type { ValidationContext } from '../index'; import { GraphQLError } from '../../error'; +import { suggestionList } from '../../jsutils/suggestionList'; import type { GraphQLType } from '../../type/definition'; -import { suggestionList } from '../../utilities/suggestionList'; + export function unknownTypeMessage( type: GraphQLType, From 6a80c4339fb5bf36aefd6ee78d3e47af215ff963 Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Mon, 18 Apr 2016 10:29:51 -0700 Subject: [PATCH 7/8] fix typos --- src/jsutils/__tests__/suggestionList-test.js | 2 +- src/jsutils/suggestionList.js | 2 +- src/validation/rules/FieldsOnCorrectType.js | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/jsutils/__tests__/suggestionList-test.js b/src/jsutils/__tests__/suggestionList-test.js index 585431458c..5e4c071be0 100644 --- a/src/jsutils/__tests__/suggestionList-test.js +++ b/src/jsutils/__tests__/suggestionList-test.js @@ -21,7 +21,7 @@ describe('suggestionList', () => { expect(suggestionList('input', [])).to.deep.equal([]); }); - it('Returns options sorted based on simularity', () => { + it('Returns options sorted based on similarity', () => { expect(suggestionList('abc', [ 'a', 'ab', 'abc' ])) .to.deep.equal([ 'abc', 'ab' ]); }); diff --git a/src/jsutils/suggestionList.js b/src/jsutils/suggestionList.js index cc524c17a5..3f9962f87d 100644 --- a/src/jsutils/suggestionList.js +++ b/src/jsutils/suggestionList.js @@ -10,7 +10,7 @@ /** * Given an invalid input string and a list of valid options, returns a filtered - * list of valid options sorted based on their simularity with the input. + * list of valid options sorted based on their similarity with the input. */ export function suggestionList( input: string, diff --git a/src/validation/rules/FieldsOnCorrectType.js b/src/validation/rules/FieldsOnCorrectType.js index 9738ec33f2..c9f87c7b7e 100644 --- a/src/validation/rules/FieldsOnCorrectType.js +++ b/src/validation/rules/FieldsOnCorrectType.js @@ -49,9 +49,7 @@ export function undefinedFieldMessage( if (suggestedFields.length > MAX_LENGTH) { suggestions += `, or ${suggestedFields.length - MAX_LENGTH} other field`; } - message += - ` Did you mean to query ${suggestions}?`; - + message +=` Did you mean to query ${suggestions}?`; } return message; } From 5a9fb1e743795f56af5b7759f4ed794e29db32ac Mon Sep 17 00:00:00 2001 From: Yuzhi Zheng Date: Tue, 19 Apr 2016 12:00:28 -0700 Subject: [PATCH 8/8] fix lint --- src/validation/rules/FieldsOnCorrectType.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/rules/FieldsOnCorrectType.js b/src/validation/rules/FieldsOnCorrectType.js index c9f87c7b7e..70a642d362 100644 --- a/src/validation/rules/FieldsOnCorrectType.js +++ b/src/validation/rules/FieldsOnCorrectType.js @@ -49,7 +49,7 @@ export function undefinedFieldMessage( if (suggestedFields.length > MAX_LENGTH) { suggestions += `, or ${suggestedFields.length - MAX_LENGTH} other field`; } - message +=` Did you mean to query ${suggestions}?`; + message += ` Did you mean to query ${suggestions}?`; } return message; }