Fixes #3494: [snippets] [debt] don't allow snippet syntax in default values

This commit is contained in:
Martin Aeschlimann
2016-02-29 22:29:09 +01:00
parent c676ceb733
commit f0c6ccccce
19 changed files with 227 additions and 135 deletions

View File

@@ -5,42 +5,43 @@
'use strict';
export interface IJSONSchema {
id?:string;
id?: string;
$schema?: string;
type?:any;
title?:string;
default?:any;
definitions?:IJSONSchemaMap;
description?:string;
type?: string | string[];
title?: string;
default?: any;
definitions?: IJSONSchemaMap;
description?: string;
properties?: IJSONSchemaMap;
patternProperties?:IJSONSchemaMap;
additionalProperties?:any;
minProperties?:number;
maxProperties?:number;
dependencies?:any;
items?:any;
minItems?:number;
maxItems?:number;
uniqueItems?:boolean;
additionalItems?:boolean;
pattern?:string;
minLength?:number;
maxLength?:number;
minimum?:number;
maximum?:number;
exclusiveMinimum?:boolean;
exclusiveMaximum?:boolean;
multipleOf?:number;
required?:string[];
$ref?:string;
anyOf?:IJSONSchema[];
allOf?:IJSONSchema[];
oneOf?:IJSONSchema[];
not?:IJSONSchema;
enum?:any[];
patternProperties?: IJSONSchemaMap;
additionalProperties?: boolean | IJSONSchema;
minProperties?: number;
maxProperties?: number;
dependencies?: IJSONSchemaMap | string[];
items?: IJSONSchema | IJSONSchema[];
minItems?: number;
maxItems?: number;
uniqueItems?: boolean;
additionalItems?: boolean;
pattern?: string;
minLength?: number;
maxLength?: number;
minimum?: number;
maximum?: number;
exclusiveMinimum?: boolean;
exclusiveMaximum?: boolean;
multipleOf?: number;
required?: string[];
$ref?: string;
anyOf?: IJSONSchema[];
allOf?: IJSONSchema[];
oneOf?: IJSONSchema[];
not?: IJSONSchema;
enum?: any[];
format?: string;
errorMessage?:string; // VS code internal
defaultSnippets?: { label?: string; description?: string; body: any; }[]; // VSCode extension
errorMessage?: string; // VSCode extension
}
export interface IJSONSchemaMap {

View File

@@ -172,7 +172,7 @@ export class JSONCompletion {
if (schemaProperties) {
Object.keys(schemaProperties).forEach((key: string) => {
let propertySchema = schemaProperties[key];
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForProperty(key, propertySchema, addValue, isLast), documentation: propertySchema.description || '' });
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getTextForProperty(key, propertySchema, addValue, isLast), documentation: propertySchema.description || '' });
});
}
}
@@ -183,7 +183,7 @@ export class JSONCompletion {
let collectSuggestionsForSimilarObject = (obj: Parser.ObjectASTNode) => {
obj.properties.forEach((p) => {
let key = p.key.value;
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getSnippetForSimilarProperty(key, p.value), documentation: '' });
collector.add({ kind: CompletionItemKind.Property, label: key, insertText: this.getTextForSimilarProperty(key, p.value), documentation: '' });
});
};
if (node.parent) {
@@ -206,14 +206,14 @@ export class JSONCompletion {
}
}
if (!currentKey && currentWord.length > 0) {
collector.add({ kind: CompletionItemKind.Property, label: JSON.stringify(currentWord), insertText: this.getSnippetForProperty(currentWord, null, true, isLast), documentation: '' });
collector.add({ kind: CompletionItemKind.Property, label: this.getLabelForValue(currentWord), insertText: this.getTextForProperty(currentWord, null, true, isLast), documentation: '' });
}
}
private getSchemaLessValueSuggestions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: ITextDocument, collector: ISuggestionsCollector): void {
let collectSuggestionsForValues = (value: Parser.ASTNode) => {
if (!value.contains(offset)) {
let content = this.getMatchingSnippet(value, document);
let content = this.getTextForMatchingNode(value, document);
collector.add({ kind: this.getSuggestionKind(value.type), label: content, insertText: content, documentation: '' });
}
if (value.type === 'boolean') {
@@ -347,6 +347,16 @@ export class JSONCompletion {
detail: nls.localize('json.suggest.default', 'Default value'),
});
}
if (Array.isArray(schema.defaultSnippets)) {
schema.defaultSnippets.forEach(s => {
collector.add({
kind: CompletionItemKind.Snippet,
label: this.getLabelForSnippetValue(s.body),
insertText: this.getTextForSnippetValue(s.body)
});
});
}
if (Array.isArray(schema.allOf)) {
schema.allOf.forEach((s) => this.addDefaultSuggestion(s, collector));
}
@@ -360,7 +370,15 @@ export class JSONCompletion {
private getLabelForValue(value: any): string {
let label = JSON.stringify(value);
label = label.replace('{{', '').replace('}}', '');
if (label.length > 57) {
return label.substr(0, 57).trim() + '...';
}
return label;
}
private getLabelForSnippetValue(value: any): string {
let label = JSON.stringify(value);
label = label.replace(/\{\{|\}\}/g, '');
if (label.length > 57) {
return label.substr(0, 57).trim() + '...';
}
@@ -368,11 +386,17 @@ export class JSONCompletion {
}
private getTextForValue(value: any): string {
var text = JSON.stringify(value, null, '\t');
text = text.replace(/[\\\{\}]/g, '\\$&');
return text;
}
private getTextForSnippetValue(value: any): string {
return JSON.stringify(value, null, '\t');
}
private getSnippetForValue(value: any): string {
let snippet = JSON.stringify(value, null, '\t');
private getTextForEnumValue(value: any): string {
let snippet = this.getTextForValue(value);
switch (typeof value) {
case 'object':
if (value === null) {
@@ -405,7 +429,7 @@ export class JSONCompletion {
}
private getMatchingSnippet(node: Parser.ASTNode, document: ITextDocument): string {
private getTextForMatchingNode(node: Parser.ASTNode, document: ITextDocument): string {
switch (node.type) {
case 'array':
return '[]';
@@ -417,9 +441,9 @@ export class JSONCompletion {
}
}
private getSnippetForProperty(key: string, propertySchema: JsonSchema.IJSONSchema, addValue: boolean, isLast: boolean): string {
private getTextForProperty(key: string, propertySchema: JsonSchema.IJSONSchema, addValue: boolean, isLast: boolean): string {
let result = '"' + key + '"';
let result = this.getTextForValue(key);
if (!addValue) {
return result;
}
@@ -428,9 +452,9 @@ export class JSONCompletion {
if (propertySchema) {
let defaultVal = propertySchema.default;
if (typeof defaultVal !== 'undefined') {
result = result + this.getSnippetForValue(defaultVal);
result = result + this.getTextForEnumValue(defaultVal);
} else if (propertySchema.enum && propertySchema.enum.length > 0) {
result = result + this.getSnippetForValue(propertySchema.enum[0]);
result = result + this.getTextForEnumValue(propertySchema.enum[0]);
} else {
var type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type;
switch (type) {
@@ -465,8 +489,8 @@ export class JSONCompletion {
return result;
}
private getSnippetForSimilarProperty(key: string, templateValue: Parser.ASTNode): string {
return '"' + key + '"';
private getTextForSimilarProperty(key: string, templateValue: Parser.ASTNode): string {
return this.getTextForValue(key);
}
private getCurrentWord(document: ITextDocument, offset: number) {

View File

@@ -103,7 +103,7 @@ export class ASTNode {
if ((<string[]>schema.type).indexOf(this.type) === -1) {
validationResult.warnings.push({
location: { start: this.start, end: this.end },
message: nls.localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}', schema.type.join(', '))
message: nls.localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}', (<string[]>schema.type).join(', '))
});
}
}
@@ -277,14 +277,14 @@ export class ArrayASTNode extends ASTNode {
super.validate(schema, validationResult, matchingSchemas, offset);
if (Array.isArray(schema.items)) {
let subSchemas: JsonSchema.IJSONSchema[] = schema.items;
let subSchemas = <JsonSchema.IJSONSchema[]> schema.items;
subSchemas.forEach((subSchema, index) => {
let itemValidationResult = new ValidationResult();
let item = this.items[index];
if (item) {
item.validate(subSchema, itemValidationResult, matchingSchemas, offset);
validationResult.mergePropertyMatch(itemValidationResult);
} else if (this.items.length >= schema.items.length) {
} else if (this.items.length >= subSchemas.length) {
validationResult.propertiesValueMatches++;
}
});
@@ -294,8 +294,8 @@ export class ArrayASTNode extends ASTNode {
location: { start: this.start, end: this.end },
message: nls.localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer', subSchemas.length)
});
} else if (this.items.length >= schema.items.length) {
validationResult.propertiesValueMatches += (this.items.length - schema.items.length);
} else if (this.items.length >= subSchemas.length) {
validationResult.propertiesValueMatches += (this.items.length - subSchemas.length);
}
}
else if (schema.items) {

View File

@@ -24,7 +24,7 @@ suite('JSON Completion', () => {
var matches = completions.filter(function(completion: CompletionItem) {
return completion.label === label && (!documentation || completion.documentation === documentation);
});
assert.equal(matches.length, 1, label + " should only existing once");
assert.equal(matches.length, 1, label + " should only existing once: Actual: " + completions.map(c => c.label).join(', '));
if (document && resultText) {
assert.equal(applyEdits(document, [ matches[0].textEdit ]), resultText);
}
@@ -51,8 +51,6 @@ suite('JSON Completion', () => {
})
};
test('Complete keys no schema', function(testDone) {
Promise.all([
testSuggestionsFor('[ { "name": "John", "age": 44 }, { /**/ }', '/**/', null, result => {
@@ -478,4 +476,44 @@ suite('JSON Completion', () => {
}),
]).then(() => testDone(), (error) => testDone(error));
});
});
test('Escaping no schema', function(testDone) {
Promise.all([
testSuggestionsFor('[ { "\\\\{{}}": "John" }, { "/**/" }', '/**/', null, result => {
assertSuggestion(result, '\\{{}}');
}),
testSuggestionsFor('[ { "\\\\{{}}": "John" }, { /**/ }', '/**/', null, (result, document) => {
assertSuggestion(result, '\\{{}}', null, document, '[ { "\\\\{{}}": "John" }, { "\\\\\\\\\\{\\{\\}\\}"/**/ }');
}),
testSuggestionsFor('[ { "name": "\\{" }, { "name": /**/ }', '/**/', null, result => {
assertSuggestion(result, '"\\{"');
})
]).then(() => testDone(), (error) => testDone(error));
});
test('Escaping with schema', function(testDone) {
var schema: JsonSchema.IJSONSchema = {
type: 'object',
properties: {
'{\\}': {
default: "{\\}",
defaultSnippets: [ { body: "{{var}}"} ],
enum: ['John{\\}']
}
}
};
Promise.all([
testSuggestionsFor('{ /**/ }', '/**/', schema, (result, document) => {
assertSuggestion(result, '{\\}', null, document, '{ "\\{\\\\\\\\\\}": "{{\\{\\\\\\\\\\}}}"/**/ }');
}),
testSuggestionsFor('{ "{\\\\}": /**/ }', '/**/', schema, (result, document) => {
assertSuggestion(result, '"{\\\\}"', null, document, '{ "{\\\\}": "\\{\\\\\\\\\\}"/**/ }');
assertSuggestion(result, '"John{\\\\}"', null, document, '{ "{\\\\}": "John\\{\\\\\\\\\\}"/**/ }');
assertSuggestion(result, '"var"', null, document, '{ "{\\\\}": "{{var}}"/**/ }');
})
]).then(() => testDone(), (error) => testDone(error));
});
});