From 6dc9a60c9afaa92eb99f6801f390a6edbb2d3dd3 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Fri, 1 Nov 2019 20:46:29 -0400 Subject: [PATCH] Fixed up schema sequence --- src/languageservice/jsonSchema04.ts | 2 + src/languageservice/jsonSchema07.ts | 2 + src/languageservice/parser/yamlParser07.ts | 1 + .../services/yamlCompletion.ts | 1 + src/languageservice/services/yamlHover.ts | 2 + .../services/yamlSchemaService.ts | 157 +++++++++++++++- .../services/yamlValidation.ts | 3 + .../customMultipleSchemaSequences.json | 4 +- test/mulipleDocuments.test.ts | 174 ++++++++---------- 9 files changed, 240 insertions(+), 106 deletions(-) diff --git a/src/languageservice/jsonSchema04.ts b/src/languageservice/jsonSchema04.ts index a083ae2b..afeb53af 100644 --- a/src/languageservice/jsonSchema04.ts +++ b/src/languageservice/jsonSchema04.ts @@ -48,6 +48,8 @@ export interface JSONSchema { patternErrorMessage?: string; // VSCode extension deprecationMessage?: string; // VSCode extension enumDescriptions?: string[]; // VSCode extension + + schemaSequence?: JSONSchema[]; // extension for multiple schemas related to multiple documents in single yaml file } export interface JSONSchemaMap { diff --git a/src/languageservice/jsonSchema07.ts b/src/languageservice/jsonSchema07.ts index 145a607a..5ddac961 100644 --- a/src/languageservice/jsonSchema07.ts +++ b/src/languageservice/jsonSchema07.ts @@ -74,6 +74,8 @@ export interface JSONSchema { markdownDescription?: string; // VSCode extension doNotSuggest?: boolean; // VSCode extension allowComments?: boolean; // VSCode extension + + schemaSequence?: JSONSchema[]; // extension for multiple schemas related to multiple documents in single yaml file } export interface JSONSchemaMap { diff --git a/src/languageservice/parser/yamlParser07.ts b/src/languageservice/parser/yamlParser07.ts index cdc03253..6e4179aa 100644 --- a/src/languageservice/parser/yamlParser07.ts +++ b/src/languageservice/parser/yamlParser07.ts @@ -25,6 +25,7 @@ export class SingleYAMLDocument extends JSONDocument { public errors; public warnings; public isKubernetes: boolean; + public currentDocIndex: number; constructor(lines: number[]) { super(null, []); diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 79e1ef4d..da5b00ab 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -133,6 +133,7 @@ export class YAMLCompletion { this.getCustomTagValueCompletions(collector); } + currentDoc.currentDocIndex = currentDocIndex; return this.schemaService.getSchemaForResource(document.uri, currentDoc).then(schema => { if (!schema) { diff --git a/src/languageservice/services/yamlHover.ts b/src/languageservice/services/yamlHover.ts index e9321a2f..dc74a000 100644 --- a/src/languageservice/services/yamlHover.ts +++ b/src/languageservice/services/yamlHover.ts @@ -43,6 +43,8 @@ export class YAMLHover { return this.promise.resolve(void 0); } + const currentDocIndex = doc.documents.indexOf(currentDoc); + currentDoc.currentDocIndex = currentDocIndex; return this.jsonHover.doHover(document, position, currentDoc); } } diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index a2bb2360..6f1b035d 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -5,17 +5,21 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { JSONSchema } from '../jsonSchema07'; +import { JSONSchema, JSONSchemaMap, JSONSchemaRef } from '../jsonSchema07'; import { SchemaRequestService, WorkspaceContextService, PromiseConstructor, Thenable } from '../yamlLanguageService'; import { UnresolvedSchema, ResolvedSchema, JSONSchemaService, SchemaDependencies, FilePatternAssociation, ISchemaContributions } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + export declare type CustomSchemaProvider = (uri: string) => Thenable; export class YAMLSchemaService extends JSONSchemaService { private customSchemaProvider: CustomSchemaProvider | undefined; private filePatternAssociations: FilePatternAssociation[]; + private contextService: WorkspaceContextService; constructor(requestService: SchemaRequestService, contextService?: WorkspaceContextService, promiseConstructor?: PromiseConstructor) { super(requestService, contextService, promiseConstructor); @@ -26,6 +30,135 @@ export class YAMLSchemaService extends JSONSchemaService { this.customSchemaProvider = customSchemaProvider; } + //tslint:disable + public resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaURL: string, dependencies: SchemaDependencies): Thenable { + + let resolveErrors: string[] = schemaToResolve.errors.slice(0); + let schema = schemaToResolve.schema; + let contextService = this.contextService; + + let findSection = (schema: JSONSchema, path: string): any => { + if (!path) { + return schema; + } + let current: any = schema; + if (path[0] === '/') { + path = path.substr(1); + } + path.split('/').some((part) => { + current = current[part]; + return !current; + }); + return current; + }; + + let merge = (target: JSONSchema, sourceRoot: JSONSchema, sourceURI: string, path: string): void => { + let section = findSection(sourceRoot, path); + if (section) { + for (let key in section) { + if (section.hasOwnProperty(key) && !target.hasOwnProperty(key)) { + target[key] = section[key]; + } + } + } else { + resolveErrors.push(localize('json.schema.invalidref', '$ref \'{0}\' in \'{1}\' can not be resolved.', path, sourceURI)); + } + }; + + let resolveExternalLink = (node: JSONSchema, uri: string, linkPath: string, parentSchemaURL: string, parentSchemaDependencies: SchemaDependencies): Thenable => { + if (contextService && !/^\w+:\/\/.*/.test(uri)) { + uri = contextService.resolveRelativePath(uri, parentSchemaURL); + } + uri = this.normalizeId(uri); + const referencedHandle = this.getOrAddSchemaHandle(uri); + return referencedHandle.getUnresolvedSchema().then(unresolvedSchema => { + parentSchemaDependencies[uri] = true; + if (unresolvedSchema.errors.length) { + let loc = linkPath ? uri + '#' + linkPath : uri; + resolveErrors.push(localize('json.schema.problemloadingref', 'Problems loading reference \'{0}\': {1}', loc, unresolvedSchema.errors[0])); + } + merge(node, unresolvedSchema.schema, uri, linkPath); + return resolveRefs(node, unresolvedSchema.schema, uri, referencedHandle.dependencies); + }); + }; + + let resolveRefs = (node: JSONSchema, parentSchema: JSONSchema, parentSchemaURL: string, parentSchemaDependencies: SchemaDependencies): Thenable => { + if (!node || typeof node !== 'object') { + return Promise.resolve(null); + } + + let toWalk: JSONSchema[] = [node]; + let seen: JSONSchema[] = []; + + let openPromises: Thenable[] = []; + + let collectEntries = (...entries: JSONSchemaRef[]) => { + for (let entry of entries) { + if (typeof entry === 'object') { + toWalk.push(entry); + } + } + }; + let collectMapEntries = (...maps: JSONSchemaMap[]) => { + for (let map of maps) { + if (typeof map === 'object') { + for (let key in map) { + let entry = map[key]; + if (typeof entry === 'object') { + toWalk.push(entry); + } + } + } + } + }; + let collectArrayEntries = (...arrays: JSONSchemaRef[][]) => { + for (let array of arrays) { + if (Array.isArray(array)) { + for (let entry of array) { + if (typeof entry === 'object') { + toWalk.push(entry); + } + } + } + } + }; + let handleRef = (next: JSONSchema) => { + let seenRefs = []; + while (next.$ref) { + const ref = next.$ref; + let segments = ref.split('#', 2); + delete next.$ref; + if (segments[0].length > 0) { + openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies)); + return; + } else { + if (seenRefs.indexOf(ref) === -1) { + merge(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again, use seenRefs to avoid circle + seenRefs.push(ref); + } + } + } + + collectEntries(next.items, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else); + collectMapEntries(next.definitions, next.properties, next.patternProperties, next.dependencies); + collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.items, next.schemaSequence); + }; + + while (toWalk.length) { + let next = toWalk.pop(); + if (seen.indexOf(next) >= 0) { + continue; + } + seen.push(next); + handleRef(next); + } + return Promise.all(openPromises); + }; + + return resolveRefs(schema, schema, schemaURL, dependencies).then(_ => new ResolvedSchema(schema, resolveErrors)); + } + //tslint:enable + public getSchemaForResource(resource: string, doc = undefined): Thenable { const resolveSchema = () => { @@ -43,7 +176,12 @@ export class YAMLSchemaService extends JSONSchemaService { } if (schemas.length > 0) { - return super.createCombinedSchema(resource, schemas).getResolvedSchema(); + return super.createCombinedSchema(resource, schemas).getResolvedSchema().then(schema => { + if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex]) { + return new ResolvedSchema(schema.schema.schemaSequence[doc.currentDocIndex]); + } + return schema; + }); } return Promise.resolve(null); @@ -56,7 +194,12 @@ export class YAMLSchemaService extends JSONSchemaService { } return this.loadSchema(schemaUri) - .then(unsolvedSchema => this.resolveSchemaContent(unsolvedSchema, schemaUri, [])); + .then(unsolvedSchema => this.resolveSchemaContent(unsolvedSchema, schemaUri, []).then(schema => { + if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex]) { + return new ResolvedSchema(schema.schema.schemaSequence[doc.currentDocIndex]); + } + return schema; + })); }) .then(schema => schema, err => resolveSchema()); } else { @@ -69,8 +212,12 @@ export class YAMLSchemaService extends JSONSchemaService { * to provide a wrapper around the javascript methods we are calling since they have no type */ - resolveSchemaContent(schemaToResolve: UnresolvedSchema, schemaURL: string, dependencies: SchemaDependencies): Thenable { - return super.resolveSchemaContent(schemaToResolve, schemaURL, dependencies); + normalizeId(id: string) { + return super.normalizeId(id); + } + + getOrAddSchemaHandle(id: string, unresolvedSchemaContent?: JSONSchema) { + return super.getOrAddSchemaHandle(id, unresolvedSchemaContent); } // tslint:disable-next-line: no-any diff --git a/src/languageservice/services/yamlValidation.ts b/src/languageservice/services/yamlValidation.ts index ef445ac1..8be07e5a 100644 --- a/src/languageservice/services/yamlValidation.ts +++ b/src/languageservice/services/yamlValidation.ts @@ -41,8 +41,10 @@ export class YAMLValidation { } const yamlDocument: YAMLDocument = parseYAML(textDocument.getText(), this.customTags); const validationResult: Diagnostic[] = []; + let index = 0; for (const currentYAMLDoc of yamlDocument.documents) { currentYAMLDoc.isKubernetes = isKubernetes; + currentYAMLDoc.currentDocIndex = index; const validation = await this.jsonValidation.doValidation(textDocument, currentYAMLDoc); const syd = currentYAMLDoc as unknown as SingleYAMLDocument; @@ -51,6 +53,7 @@ export class YAMLValidation { } validationResult.push(...validation); + index++; } const foundSignatures = new Set(); diff --git a/test/fixtures/customMultipleSchemaSequences.json b/test/fixtures/customMultipleSchemaSequences.json index c00fc83e..d37a2146 100644 --- a/test/fixtures/customMultipleSchemaSequences.json +++ b/test/fixtures/customMultipleSchemaSequences.json @@ -1,5 +1,5 @@ { - "oneOf": [ + "schemaSequence": [ { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Person object", @@ -14,7 +14,7 @@ "description": "The age of this person" } }, - "required":["name","age"] + "required": ["name","age"] }, { "$ref": "http://json.schemastore.org/bowerrc" diff --git a/test/mulipleDocuments.test.ts b/test/mulipleDocuments.test.ts index c83659f8..b6a3a7b2 100644 --- a/test/mulipleDocuments.test.ts +++ b/test/mulipleDocuments.test.ts @@ -2,121 +2,97 @@ * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getLanguageService, LanguageSettings } from '../src/languageservice/yamlLanguageService'; import path = require('path'); -import { schemaRequestService, workspaceContext, createJSONLanguageService } from './utils/testHelper'; +import { setupTextDocument, configureLanguageService, toFsPath } from './utils/testHelper'; import assert = require('assert'); +import { ServiceSetup } from './utils/serviceSetup'; -const languageService = getLanguageService(schemaRequestService, workspaceContext, [], null); - -function toFsPath(str): string { - if (typeof str !== 'string') { - throw new TypeError(`Expected a string, got ${typeof str}`); - } - - let pathName; - pathName = path.resolve(str); - pathName = pathName.replace(/\\/g, '/'); - // Windows drive letter must be prefixed with a slash - if (pathName[0] !== '/') { - pathName = `/${pathName}`; - } - return encodeURI(`file://${pathName}`).replace(/[?#]/g, encodeURIComponent); -} - +/** + * Setup the schema we are going to use with the language settings + */ const uri = toFsPath(path.join(__dirname, './fixtures/customMultipleSchemaSequences.json')); -const languageSettings: LanguageSettings = { - schemas: [], - validate: true, - customTags: [], - hover: true -}; const fileMatch = ['*.yml', '*.yaml']; -languageSettings.schemas.push({ uri, fileMatch: fileMatch }); -languageSettings.customTags.push('!Test'); -languageSettings.customTags.push('!Ref sequence'); -languageService.configure(languageSettings); +const languageSettingsSetup = new ServiceSetup() + .withHover() + .withValidate() + .withSchemaFileMatch({ uri, fileMatch: fileMatch }) + .withCustomTags(['!Test', '!Ref sequence']); + // Defines a Mocha test suite to group tests of similar kind together suite('Multiple Documents Validation Tests', () => { // Tests for validator describe('Multiple Documents Validation', function () { -// function setup(content: string){ -// return TextDocument.create("file://~/Desktop/vscode-k8s/test.yaml", "yaml", 0, content); -// } -// function validatorSetup(content: string){ -// const testTextDocument = setup(content); -// const yDoc = parseYAML(testTextDocument.getText(), languageSettings.customTags); -// return languageService.doValidation(testTextDocument, yDoc); -// } + function validatorSetup(content: string) { + const testTextDocument = setupTextDocument(content); + const languageService = configureLanguageService(languageSettingsSetup.languageSettings); + return languageService.doValidation(testTextDocument, false); + } -// function hoverSetup(content: string, position){ -// let testTextDocument = setup(content); -// let jsonDocument = parseYAML2(testTextDocument.getText()); -// const jsonLanguageService = createJSONLanguageService(); -// jsonLanguageService.configure({ -// schemas: [{ -// fileMatch, -// uri -// }] -// }); -// return languageService.doHover(jsonLanguageService, testTextDocument, testTextDocument.positionAt(position), jsonDocument); -// } + function hoverSetup(content: string, position) { + const testTextDocument = setupTextDocument(content); + const languageService = configureLanguageService(languageSettingsSetup.languageSettings); + return languageService.doHover( + testTextDocument, + testTextDocument.positionAt(position) + ); + } -// it('Should validate multiple documents', (done) => { -// const content = ` -// name: jack -// age: 22 -// --- -// analytics: true -// `; -// const validator = validatorSetup(content); -// validator.then((result) => { -// assert.equal(result.length, 0); -// }).then(done, done); -// }); + it('Should validate multiple documents', done => { + const content = ` +name: jack +age: 22 +--- +cwd: test + `; + const validator = validatorSetup(content); + validator.then(result => { + assert.equal(result.length, 0); + }).then(done, done); + }); -// it('Should find errors in both documents', (done) => { -// let content = `name1: jack -// age: asd -// --- -// cwd: False`; -// let validator = validatorSetup(content); -// validator.then(function(result){ -// assert.equal(result.length, 3); -// }).then(done, done); -// }); + it('Should find errors in both documents', done => { + const content = `name1: jack +age: asd +--- +cwd: False`; + const validator = validatorSetup(content); + validator.then(function (result) { + assert.equal(result.length, 3); + }).then(done, done); + }); -// it('Should find errors in first document', (done) => { -// let content = `name: jack -// age: age -// --- -// analytics: true`; -// let validator = validatorSetup(content); -// validator.then(function(result){ -// assert.equal(result.length, 1); -// }).then(done, done); -// }); + it('Should find errors in first document', done => { + const content = `name: jack +age: age +--- +cwd: test`; + const validator = validatorSetup(content); + validator.then(function (result) { + assert.equal(result.length, 1); + }).then(done, done); + }); -// it('Should find errors in second document', (done) => { -// let content = `name: jack -// age: 22 -// --- -// cwd: False`; -// let validator = validatorSetup(content); -// validator.then(function(result){ -// assert.equal(result.length, 1); -// }).then(done, done); -// }); + it('Should find errors in second document', done => { + const content = `name: jack +age: 22 +--- +cwd: False +`; + const validator = validatorSetup(content); + validator.then(function (result) { + assert.equal(result.length, 1); + }).then(done, done); + }); -// it('Should hover in first document', (done) => { -// let content = `name: jack\nage: 22\n---\ncwd: False`; -// let hover = hoverSetup(content, 1 + content.indexOf('age')); -// hover.then(function(result){ -// assert.notEqual(result.contents.length, 0); -// assert.equal(result.contents[0], 'The age of this person'); -// }).then(done, done); -// }); + it('Should hover in first document', done => { + const content = 'name: jack\nage: 22\n---\ncwd: False'; + const hover = hoverSetup(content, 1 + content.indexOf('age')); + hover.then(function (result) { + assert.notEqual((result.contents as []).length, 0); + assert.equal(result.contents[0], 'The age of this person'); + }).then(done, done); + }); }); });