Skip to content

Commit 5bc1b25

Browse files
yuzhileebyron
authored andcommitted
Include possible field, argument, type names when validation fails (#355)
* Add suggestionList to return strings based on how simular they are to the input * Suggests valid fields in `FieldsOnCorrectType` * Suggest argument names * Suggested valid type names * Fix flow and unit test * addressed comments in PR: move file, update comment, filter out more options, remove redundant warning * fix typos * fix lint
1 parent 4b08c36 commit 5bc1b25

File tree

8 files changed

+336
-52
lines changed

8 files changed

+336
-52
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
import { expect } from 'chai';
11+
import { describe, it } from 'mocha';
12+
import { suggestionList } from '../suggestionList';
13+
14+
describe('suggestionList', () => {
15+
16+
it('Returns results when input is empty', () => {
17+
expect(suggestionList('', [ 'a' ])).to.deep.equal([ 'a' ]);
18+
});
19+
20+
it('Returns empty array when there are no options', () => {
21+
expect(suggestionList('input', [])).to.deep.equal([]);
22+
});
23+
24+
it('Returns options sorted based on similarity', () => {
25+
expect(suggestionList('abc', [ 'a', 'ab', 'abc' ]))
26+
.to.deep.equal([ 'abc', 'ab' ]);
27+
});
28+
});

src/jsutils/suggestionList.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* @flow */
2+
/**
3+
* Copyright (c) 2015, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
11+
/**
12+
* Given an invalid input string and a list of valid options, returns a filtered
13+
* list of valid options sorted based on their similarity with the input.
14+
*/
15+
export function suggestionList(
16+
input: string,
17+
options: Array<string>
18+
): Array<string> {
19+
let i;
20+
const d = {};
21+
const oLength = options.length;
22+
const inputThreshold = input.length / 2;
23+
for (i = 0; i < oLength; i++) {
24+
const distance = lexicalDistance(input, options[i]);
25+
const threshold = Math.max(inputThreshold, options[i].length / 2, 1);
26+
if (distance <= threshold) {
27+
d[options[i]] = distance;
28+
}
29+
}
30+
const result = Object.keys(d);
31+
return result.sort((a , b) => d[a] - d[b]);
32+
}
33+
34+
/**
35+
* Computes the lexical distance between strings A and B.
36+
*
37+
* The "distance" between two strings is given by counting the minimum number
38+
* of edits needed to transform string A into string B. An edit can be an
39+
* insertion, deletion, or substitution of a single character, or a swap of two
40+
* adjacent characters.
41+
*
42+
* This distance can be useful for detecting typos in input or sorting
43+
*
44+
* @param {string} a
45+
* @param {string} b
46+
* @return {int} distance in number of edits
47+
*/
48+
function lexicalDistance(a, b) {
49+
let i;
50+
let j;
51+
const d = [];
52+
const aLength = a.length;
53+
const bLength = b.length;
54+
55+
for (i = 0; i <= aLength; i++) {
56+
d[i] = [ i ];
57+
}
58+
59+
for (j = 1; j <= bLength; j++) {
60+
d[0][j] = j;
61+
}
62+
63+
for (i = 1; i <= aLength; i++) {
64+
for (j = 1; j <= bLength; j++) {
65+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
66+
67+
d[i][j] = Math.min(
68+
d[i - 1][j] + 1,
69+
d[i][j - 1] + 1,
70+
d[i - 1][j - 1] + cost
71+
);
72+
73+
if (i > 1 && j > 1 &&
74+
a[i - 1] === b[j - 2] &&
75+
a[i - 2] === b[j - 1]) {
76+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
77+
}
78+
}
79+
}
80+
81+
return d[aLength][bLength];
82+
}

src/validation/__tests__/FieldsOnCorrectType-test.js

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,21 @@ import {
1616
} from '../rules/FieldsOnCorrectType';
1717

1818

19-
function undefinedField(field, type, suggestions, line, column) {
19+
function undefinedField(
20+
field,
21+
type,
22+
suggestedTypes,
23+
suggestedFields,
24+
line,
25+
column
26+
) {
2027
return {
21-
message: undefinedFieldMessage(field, type, suggestions),
28+
message: undefinedFieldMessage(
29+
field,
30+
type,
31+
suggestedTypes,
32+
suggestedFields
33+
),
2234
locations: [ { line, column } ],
2335
};
2436
}
@@ -85,8 +97,16 @@ describe('Validate: Fields on correct type', () => {
8597
}
8698
}
8799
}`,
88-
[ undefinedField('unknown_pet_field', 'Pet', [], 3, 9),
89-
undefinedField('unknown_cat_field', 'Cat', [], 5, 13) ]
100+
[ undefinedField('unknown_pet_field', 'Pet', [], [], 3, 9),
101+
undefinedField(
102+
'unknown_cat_field',
103+
'Cat',
104+
[],
105+
[],
106+
5,
107+
13
108+
)
109+
]
90110
);
91111
});
92112

@@ -95,7 +115,15 @@ describe('Validate: Fields on correct type', () => {
95115
fragment fieldNotDefined on Dog {
96116
meowVolume
97117
}`,
98-
[ undefinedField('meowVolume', 'Dog', [], 3, 9) ]
118+
[ undefinedField(
119+
'meowVolume',
120+
'Dog',
121+
[],
122+
[ 'barkVolume' ],
123+
3,
124+
9
125+
)
126+
]
99127
);
100128
});
101129

@@ -106,7 +134,15 @@ describe('Validate: Fields on correct type', () => {
106134
deeper_unknown_field
107135
}
108136
}`,
109-
[ undefinedField('unknown_field', 'Dog', [], 3, 9) ]
137+
[ undefinedField(
138+
'unknown_field',
139+
'Dog',
140+
[],
141+
[],
142+
3,
143+
9
144+
)
145+
]
110146
);
111147
});
112148

@@ -117,7 +153,7 @@ describe('Validate: Fields on correct type', () => {
117153
unknown_field
118154
}
119155
}`,
120-
[ undefinedField('unknown_field', 'Pet', [], 4, 11) ]
156+
[ undefinedField('unknown_field', 'Pet', [], [], 4, 11) ]
121157
);
122158
});
123159

@@ -128,7 +164,15 @@ describe('Validate: Fields on correct type', () => {
128164
meowVolume
129165
}
130166
}`,
131-
[ undefinedField('meowVolume', 'Dog', [], 4, 11) ]
167+
[ undefinedField(
168+
'meowVolume',
169+
'Dog',
170+
[],
171+
[ 'barkVolume' ],
172+
4,
173+
11
174+
)
175+
]
132176
);
133177
});
134178

@@ -137,7 +181,15 @@ describe('Validate: Fields on correct type', () => {
137181
fragment aliasedFieldTargetNotDefined on Dog {
138182
volume : mooVolume
139183
}`,
140-
[ undefinedField('mooVolume', 'Dog', [], 3, 9) ]
184+
[ undefinedField(
185+
'mooVolume',
186+
'Dog',
187+
[],
188+
[ 'barkVolume' ],
189+
3,
190+
9
191+
)
192+
]
141193
);
142194
});
143195

@@ -146,7 +198,15 @@ describe('Validate: Fields on correct type', () => {
146198
fragment aliasedLyingFieldTargetNotDefined on Dog {
147199
barkVolume : kawVolume
148200
}`,
149-
[ undefinedField('kawVolume', 'Dog', [], 3, 9) ]
201+
[ undefinedField(
202+
'kawVolume',
203+
'Dog',
204+
[],
205+
[ 'barkVolume' ],
206+
3,
207+
9
208+
)
209+
]
150210
);
151211
});
152212

@@ -155,7 +215,7 @@ describe('Validate: Fields on correct type', () => {
155215
fragment notDefinedOnInterface on Pet {
156216
tailLength
157217
}`,
158-
[ undefinedField('tailLength', 'Pet', [], 3, 9) ]
218+
[ undefinedField('tailLength', 'Pet', [], [], 3, 9) ]
159219
);
160220
});
161221

@@ -164,7 +224,7 @@ describe('Validate: Fields on correct type', () => {
164224
fragment definedOnImplementorsButNotInterface on Pet {
165225
nickname
166226
}`,
167-
[ undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], 3, 9) ]
227+
[ undefinedField('nickname', 'Pet', [ 'Cat', 'Dog' ], [ 'name' ], 3, 9) ]
168228
);
169229
});
170230

@@ -181,7 +241,7 @@ describe('Validate: Fields on correct type', () => {
181241
fragment directFieldSelectionOnUnion on CatOrDog {
182242
directField
183243
}`,
184-
[ undefinedField('directField', 'CatOrDog', [], 3, 9) ]
244+
[ undefinedField('directField', 'CatOrDog', [], [], 3, 9) ]
185245
);
186246
});
187247

@@ -195,6 +255,7 @@ describe('Validate: Fields on correct type', () => {
195255
'name',
196256
'CatOrDog',
197257
[ 'Being', 'Pet', 'Canine', 'Cat', 'Dog' ],
258+
[],
198259
3,
199260
9
200261
)
@@ -218,25 +279,33 @@ describe('Validate: Fields on correct type', () => {
218279
describe('Fields on correct type error message', () => {
219280
it('Works with no suggestions', () => {
220281
expect(
221-
undefinedFieldMessage('T', 'f', [])
222-
).to.equal('Cannot query field "T" on type "f".');
282+
undefinedFieldMessage('f', 'T', [], [])
283+
).to.equal('Cannot query field "f" on type "T".');
223284
});
224285

225286
it('Works with no small numbers of suggestions', () => {
226287
expect(
227-
undefinedFieldMessage('T', 'f', [ 'A', 'B' ])
228-
).to.equal('Cannot query field "T" on type "f". ' +
288+
undefinedFieldMessage('f', 'T', [ 'A', 'B' ], [ 'z', 'y' ])
289+
).to.equal('Cannot query field "f" on type "T". ' +
229290
'However, this field exists on "A", "B". ' +
230-
'Perhaps you meant to use an inline fragment?');
291+
'Perhaps you meant to use an inline fragment? ' +
292+
'Did you mean to query "z", "y"?');
231293
});
232294

233295
it('Works with lots of suggestions', () => {
234296
expect(
235-
undefinedFieldMessage('T', 'f', [ 'A', 'B', 'C', 'D', 'E', 'F' ])
236-
).to.equal('Cannot query field "T" on type "f". ' +
297+
undefinedFieldMessage(
298+
'f',
299+
'T',
300+
[ 'A', 'B', 'C', 'D', 'E', 'F' ],
301+
[ 'z', 'y', 'x', 'w', 'v', 'u' ]
302+
)
303+
).to.equal('Cannot query field "f" on type "T". ' +
237304
'However, this field exists on "A", "B", "C", "D", "E", ' +
238305
'and 1 other types. ' +
239-
'Perhaps you meant to use an inline fragment?');
306+
'Perhaps you meant to use an inline fragment? ' +
307+
'Did you mean to query "z", "y", "x", "w", "v", or 1 other field?'
308+
);
240309
});
241310
});
242311
});

src/validation/__tests__/KnownArgumentNames-test.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@ import {
1616
} from '../rules/KnownArgumentNames';
1717

1818

19-
function unknownArg(argName, fieldName, typeName, line, column) {
19+
function unknownArg(argName, fieldName, typeName, suggestedArgs, line, column) {
2020
return {
21-
message: unknownArgMessage(argName, fieldName, typeName),
21+
message: unknownArgMessage(argName, fieldName, typeName, suggestedArgs),
2222
locations: [ { line, column } ],
2323
};
2424
}
2525

26-
function unknownDirectiveArg(argName, directiveName, line, column) {
26+
function unknownDirectiveArg(
27+
argName,
28+
directiveName,
29+
suggestedArgs,
30+
line,
31+
column
32+
) {
2733
return {
28-
message: unknownDirectiveArgMessage(argName, directiveName),
34+
message: unknownDirectiveArgMessage(argName, directiveName, suggestedArgs),
2935
locations: [ { line, column } ],
3036
};
3137
}
@@ -103,7 +109,7 @@ describe('Validate: Known argument names', () => {
103109
dog @skip(unless: true)
104110
}
105111
`, [
106-
unknownDirectiveArg('unless', 'skip', 3, 19),
112+
unknownDirectiveArg('unless', 'skip', [], 3, 19),
107113
]);
108114
});
109115

@@ -113,7 +119,7 @@ describe('Validate: Known argument names', () => {
113119
doesKnowCommand(unknown: true)
114120
}
115121
`, [
116-
unknownArg('unknown', 'doesKnowCommand', 'Dog', 3, 25),
122+
unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 3, 25),
117123
]);
118124
});
119125

@@ -123,8 +129,8 @@ describe('Validate: Known argument names', () => {
123129
doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true)
124130
}
125131
`, [
126-
unknownArg('whoknows', 'doesKnowCommand', 'Dog', 3, 25),
127-
unknownArg('unknown', 'doesKnowCommand', 'Dog', 3, 55),
132+
unknownArg('whoknows', 'doesKnowCommand', 'Dog', [], 3, 25),
133+
unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 3, 55),
128134
]);
129135
});
130136

@@ -143,8 +149,8 @@ describe('Validate: Known argument names', () => {
143149
}
144150
}
145151
`, [
146-
unknownArg('unknown', 'doesKnowCommand', 'Dog', 4, 27),
147-
unknownArg('unknown', 'doesKnowCommand', 'Dog', 9, 31),
152+
unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 4, 27),
153+
unknownArg('unknown', 'doesKnowCommand', 'Dog', [], 9, 31),
148154
]);
149155
});
150156

0 commit comments

Comments
 (0)