Skip to content

Commit f0c6ccc

Browse files
committed
Fixes microsoft#3494: [snippets] [debt] don't allow snippet syntax in default values
1 parent c676ceb commit f0c6ccc

19 files changed

Lines changed: 227 additions & 135 deletions

File tree

extensions/json/server/src/json-toolbox/jsonSchema.ts

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,43 @@
55
'use strict';
66

77
export interface IJSONSchema {
8-
id?:string;
8+
id?: string;
99
$schema?: string;
10-
type?:any;
11-
title?:string;
12-
default?:any;
13-
definitions?:IJSONSchemaMap;
14-
description?:string;
10+
type?: string | string[];
11+
title?: string;
12+
default?: any;
13+
definitions?: IJSONSchemaMap;
14+
description?: string;
1515
properties?: IJSONSchemaMap;
16-
patternProperties?:IJSONSchemaMap;
17-
additionalProperties?:any;
18-
minProperties?:number;
19-
maxProperties?:number;
20-
dependencies?:any;
21-
items?:any;
22-
minItems?:number;
23-
maxItems?:number;
24-
uniqueItems?:boolean;
25-
additionalItems?:boolean;
26-
pattern?:string;
27-
minLength?:number;
28-
maxLength?:number;
29-
minimum?:number;
30-
maximum?:number;
31-
exclusiveMinimum?:boolean;
32-
exclusiveMaximum?:boolean;
33-
multipleOf?:number;
34-
required?:string[];
35-
$ref?:string;
36-
anyOf?:IJSONSchema[];
37-
allOf?:IJSONSchema[];
38-
oneOf?:IJSONSchema[];
39-
not?:IJSONSchema;
40-
enum?:any[];
16+
patternProperties?: IJSONSchemaMap;
17+
additionalProperties?: boolean | IJSONSchema;
18+
minProperties?: number;
19+
maxProperties?: number;
20+
dependencies?: IJSONSchemaMap | string[];
21+
items?: IJSONSchema | IJSONSchema[];
22+
minItems?: number;
23+
maxItems?: number;
24+
uniqueItems?: boolean;
25+
additionalItems?: boolean;
26+
pattern?: string;
27+
minLength?: number;
28+
maxLength?: number;
29+
minimum?: number;
30+
maximum?: number;
31+
exclusiveMinimum?: boolean;
32+
exclusiveMaximum?: boolean;
33+
multipleOf?: number;
34+
required?: string[];
35+
$ref?: string;
36+
anyOf?: IJSONSchema[];
37+
allOf?: IJSONSchema[];
38+
oneOf?: IJSONSchema[];
39+
not?: IJSONSchema;
40+
enum?: any[];
4141
format?: string;
42-
43-
errorMessage?:string; // VS code internal
42+
43+
defaultSnippets?: { label?: string; description?: string; body: any; }[]; // VSCode extension
44+
errorMessage?: string; // VSCode extension
4445
}
4546

4647
export interface IJSONSchemaMap {

extensions/json/server/src/jsonCompletion.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export class JSONCompletion {
172172
if (schemaProperties) {
173173
Object.keys(schemaProperties).forEach((key: string) => {
174174
let propertySchema = schemaProperties[key];
175-
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForProperty(key, propertySchema, addValue, isLast), documentation: propertySchema.description || '' });
175+
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getTextForProperty(key, propertySchema, addValue, isLast), documentation: propertySchema.description || '' });
176176
});
177177
}
178178
}
@@ -183,7 +183,7 @@ export class JSONCompletion {
183183
let collectSuggestionsForSimilarObject = (obj: Parser.ObjectASTNode) => {
184184
obj.properties.forEach((p) => {
185185
let key = p.key.value;
186-
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForSimilarProperty(key, p.value), documentation: '' });
186+
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getTextForSimilarProperty(key, p.value), documentation: '' });
187187
});
188188
};
189189
if (node.parent) {
@@ -206,14 +206,14 @@ export class JSONCompletion {
206206
}
207207
}
208208
if (!currentKey && currentWord.length > 0) {
209-
collector.add({ kind: CompletionItemKind.Property, label: JSON.stringify(currentWord), insertText: this.getSnippetForProperty(currentWord, null, true, isLast), documentation: '' });
209+
collector.add({ kind: CompletionItemKind.Property, label: this.getLabelForValue(currentWord), insertText: this.getTextForProperty(currentWord, null, true, isLast), documentation: '' });
210210
}
211211
}
212212

213213
private getSchemaLessValueSuggestions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: ITextDocument, collector: ISuggestionsCollector): void {
214214
let collectSuggestionsForValues = (value: Parser.ASTNode) => {
215215
if (!value.contains(offset)) {
216-
let content = this.getMatchingSnippet(value, document);
216+
let content = this.getTextForMatchingNode(value, document);
217217
collector.add({ kind: this.getSuggestionKind(value.type), label: content, insertText: content, documentation: '' });
218218
}
219219
if (value.type === 'boolean') {
@@ -347,6 +347,16 @@ export class JSONCompletion {
347347
detail: nls.localize('json.suggest.default', 'Default value'),
348348
});
349349
}
350+
if (Array.isArray(schema.defaultSnippets)) {
351+
schema.defaultSnippets.forEach(s => {
352+
collector.add({
353+
kind: CompletionItemKind.Snippet,
354+
label: this.getLabelForSnippetValue(s.body),
355+
insertText: this.getTextForSnippetValue(s.body)
356+
});
357+
});
358+
}
359+
350360
if (Array.isArray(schema.allOf)) {
351361
schema.allOf.forEach((s) => this.addDefaultSuggestion(s, collector));
352362
}
@@ -360,19 +370,33 @@ export class JSONCompletion {
360370

361371
private getLabelForValue(value: any): string {
362372
let label = JSON.stringify(value);
363-
label = label.replace('{{', '').replace('}}', '');
373+
if (label.length > 57) {
374+
return label.substr(0, 57).trim() + '...';
375+
}
376+
return label;
377+
}
378+
379+
private getLabelForSnippetValue(value: any): string {
380+
let label = JSON.stringify(value);
381+
label = label.replace(/\{\{|\}\}/g, '');
364382
if (label.length > 57) {
365383
return label.substr(0, 57).trim() + '...';
366384
}
367385
return label;
368386
}
369387

370388
private getTextForValue(value: any): string {
389+
var text = JSON.stringify(value, null, '\t');
390+
text = text.replace(/[\\\{\}]/g, '\\$&');
391+
return text;
392+
}
393+
394+
private getTextForSnippetValue(value: any): string {
371395
return JSON.stringify(value, null, '\t');
372396
}
373397

374-
private getSnippetForValue(value: any): string {
375-
let snippet = JSON.stringify(value, null, '\t');
398+
private getTextForEnumValue(value: any): string {
399+
let snippet = this.getTextForValue(value);
376400
switch (typeof value) {
377401
case 'object':
378402
if (value === null) {
@@ -405,7 +429,7 @@ export class JSONCompletion {
405429
}
406430

407431

408-
private getMatchingSnippet(node: Parser.ASTNode, document: ITextDocument): string {
432+
private getTextForMatchingNode(node: Parser.ASTNode, document: ITextDocument): string {
409433
switch (node.type) {
410434
case 'array':
411435
return '[]';
@@ -417,9 +441,9 @@ export class JSONCompletion {
417441
}
418442
}
419443

420-
private getSnippetForProperty(key: string, propertySchema: JsonSchema.IJSONSchema, addValue: boolean, isLast: boolean): string {
444+
private getTextForProperty(key: string, propertySchema: JsonSchema.IJSONSchema, addValue: boolean, isLast: boolean): string {
421445

422-
let result = '"' + key + '"';
446+
let result = this.getTextForValue(key);
423447
if (!addValue) {
424448
return result;
425449
}
@@ -428,9 +452,9 @@ export class JSONCompletion {
428452
if (propertySchema) {
429453
let defaultVal = propertySchema.default;
430454
if (typeof defaultVal !== 'undefined') {
431-
result = result + this.getSnippetForValue(defaultVal);
455+
result = result + this.getTextForEnumValue(defaultVal);
432456
} else if (propertySchema.enum && propertySchema.enum.length > 0) {
433-
result = result + this.getSnippetForValue(propertySchema.enum[0]);
457+
result = result + this.getTextForEnumValue(propertySchema.enum[0]);
434458
} else {
435459
var type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type;
436460
switch (type) {
@@ -465,8 +489,8 @@ export class JSONCompletion {
465489
return result;
466490
}
467491

468-
private getSnippetForSimilarProperty(key: string, templateValue: Parser.ASTNode): string {
469-
return '"' + key + '"';
492+
private getTextForSimilarProperty(key: string, templateValue: Parser.ASTNode): string {
493+
return this.getTextForValue(key);
470494
}
471495

472496
private getCurrentWord(document: ITextDocument, offset: number) {

extensions/json/server/src/jsonParser.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export class ASTNode {
103103
if ((<string[]>schema.type).indexOf(this.type) === -1) {
104104
validationResult.warnings.push({
105105
location: { start: this.start, end: this.end },
106-
message: nls.localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}', schema.type.join(', '))
106+
message: nls.localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}', (<string[]>schema.type).join(', '))
107107
});
108108
}
109109
}
@@ -277,14 +277,14 @@ export class ArrayASTNode extends ASTNode {
277277
super.validate(schema, validationResult, matchingSchemas, offset);
278278

279279
if (Array.isArray(schema.items)) {
280-
let subSchemas: JsonSchema.IJSONSchema[] = schema.items;
280+
let subSchemas = <JsonSchema.IJSONSchema[]> schema.items;
281281
subSchemas.forEach((subSchema, index) => {
282282
let itemValidationResult = new ValidationResult();
283283
let item = this.items[index];
284284
if (item) {
285285
item.validate(subSchema, itemValidationResult, matchingSchemas, offset);
286286
validationResult.mergePropertyMatch(itemValidationResult);
287-
} else if (this.items.length >= schema.items.length) {
287+
} else if (this.items.length >= subSchemas.length) {
288288
validationResult.propertiesValueMatches++;
289289
}
290290
});
@@ -294,8 +294,8 @@ export class ArrayASTNode extends ASTNode {
294294
location: { start: this.start, end: this.end },
295295
message: nls.localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer', subSchemas.length)
296296
});
297-
} else if (this.items.length >= schema.items.length) {
298-
validationResult.propertiesValueMatches += (this.items.length - schema.items.length);
297+
} else if (this.items.length >= subSchemas.length) {
298+
validationResult.propertiesValueMatches += (this.items.length - subSchemas.length);
299299
}
300300
}
301301
else if (schema.items) {

extensions/json/server/src/test/completion.test.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ suite('JSON Completion', () => {
2424
var matches = completions.filter(function(completion: CompletionItem) {
2525
return completion.label === label && (!documentation || completion.documentation === documentation);
2626
});
27-
assert.equal(matches.length, 1, label + " should only existing once");
27+
assert.equal(matches.length, 1, label + " should only existing once: Actual: " + completions.map(c => c.label).join(', '));
2828
if (document && resultText) {
2929
assert.equal(applyEdits(document, [ matches[0].textEdit ]), resultText);
3030
}
@@ -51,8 +51,6 @@ suite('JSON Completion', () => {
5151
})
5252
};
5353

54-
55-
5654
test('Complete keys no schema', function(testDone) {
5755
Promise.all([
5856
testSuggestionsFor('[ { "name": "John", "age": 44 }, { /**/ }', '/**/', null, result => {
@@ -478,4 +476,44 @@ suite('JSON Completion', () => {
478476
}),
479477
]).then(() => testDone(), (error) => testDone(error));
480478
});
481-
});
479+
480+
test('Escaping no schema', function(testDone) {
481+
Promise.all([
482+
testSuggestionsFor('[ { "\\\\{{}}": "John" }, { "/**/" }', '/**/', null, result => {
483+
assertSuggestion(result, '\\{{}}');
484+
}),
485+
testSuggestionsFor('[ { "\\\\{{}}": "John" }, { /**/ }', '/**/', null, (result, document) => {
486+
assertSuggestion(result, '\\{{}}', null, document, '[ { "\\\\{{}}": "John" }, { "\\\\\\\\\\{\\{\\}\\}"/**/ }');
487+
}),
488+
testSuggestionsFor('[ { "name": "\\{" }, { "name": /**/ }', '/**/', null, result => {
489+
assertSuggestion(result, '"\\{"');
490+
})
491+
]).then(() => testDone(), (error) => testDone(error));
492+
});
493+
494+
test('Escaping with schema', function(testDone) {
495+
var schema: JsonSchema.IJSONSchema = {
496+
type: 'object',
497+
properties: {
498+
'{\\}': {
499+
default: "{\\}",
500+
defaultSnippets: [ { body: "{{var}}"} ],
501+
enum: ['John{\\}']
502+
}
503+
}
504+
};
505+
506+
Promise.all([
507+
testSuggestionsFor('{ /**/ }', '/**/', schema, (result, document) => {
508+
assertSuggestion(result, '{\\}', null, document, '{ "\\{\\\\\\\\\\}": "{{\\{\\\\\\\\\\}}}"/**/ }');
509+
}),
510+
testSuggestionsFor('{ "{\\\\}": /**/ }', '/**/', schema, (result, document) => {
511+
assertSuggestion(result, '"{\\\\}"', null, document, '{ "{\\\\}": "\\{\\\\\\\\\\}"/**/ }');
512+
assertSuggestion(result, '"John{\\\\}"', null, document, '{ "{\\\\}": "John\\{\\\\\\\\\\}"/**/ }');
513+
assertSuggestion(result, '"var"', null, document, '{ "{\\\\}": "{{var}}"/**/ }');
514+
})
515+
]).then(() => testDone(), (error) => testDone(error));
516+
});
517+
518+
});
519+

src/vs/base/common/jsonSchema.ts

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,43 @@
55
'use strict';
66

77
export interface IJSONSchema {
8-
id?:string;
8+
id?: string;
99
$schema?: string;
10-
type?:any;
11-
title?:string;
12-
default?:any;
13-
definitions?:IJSONSchemaMap;
14-
description?:string;
10+
type?: string | string[];
11+
title?: string;
12+
default?: any;
13+
definitions?: IJSONSchemaMap;
14+
description?: string;
1515
properties?: IJSONSchemaMap;
16-
patternProperties?:IJSONSchemaMap;
17-
additionalProperties?:any;
18-
minProperties?:number;
19-
maxProperties?:number;
20-
dependencies?:any;
21-
items?:any;
22-
minItems?:number;
23-
maxItems?:number;
24-
uniqueItems?:boolean;
25-
additionalItems?:boolean;
26-
pattern?:string;
27-
errorMessage?: string;
28-
minLength?:number;
29-
maxLength?:number;
30-
minimum?:number;
31-
maximum?:number;
32-
exclusiveMinimum?:boolean;
33-
exclusiveMaximum?:boolean;
34-
multipleOf?:number;
35-
required?:string[];
36-
$ref?:string;
37-
anyOf?:IJSONSchema[];
38-
allOf?:IJSONSchema[];
39-
oneOf?:IJSONSchema[];
40-
not?:IJSONSchema;
41-
enum?:any[];
16+
patternProperties?: IJSONSchemaMap;
17+
additionalProperties?: boolean | IJSONSchema;
18+
minProperties?: number;
19+
maxProperties?: number;
20+
dependencies?: IJSONSchemaMap | string[];
21+
items?: IJSONSchema | IJSONSchema[];
22+
minItems?: number;
23+
maxItems?: number;
24+
uniqueItems?: boolean;
25+
additionalItems?: boolean;
26+
pattern?: string;
27+
minLength?: number;
28+
maxLength?: number;
29+
minimum?: number;
30+
maximum?: number;
31+
exclusiveMinimum?: boolean;
32+
exclusiveMaximum?: boolean;
33+
multipleOf?: number;
34+
required?: string[];
35+
$ref?: string;
36+
anyOf?: IJSONSchema[];
37+
allOf?: IJSONSchema[];
38+
oneOf?: IJSONSchema[];
39+
not?: IJSONSchema;
40+
enum?: any[];
4241
format?: string;
42+
43+
defaultSnippets?: { label?: string; description?: string; body: any; }[]; // VSCode extension
44+
errorMessage?: string; // VSCode extension
4345
}
4446

4547
export interface IJSONSchemaMap {

0 commit comments

Comments
 (0)