Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add basic support for declaring schema with inline $schema #970

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/languageservice/services/dollarUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat, Inc. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { SingleYAMLDocument } from '../parser/yamlParser07';
import { JSONDocument } from '../parser/jsonParser07';

/**
* Retrieve schema if declared by `$schema`.
* Public for testing purpose, not part of the API.
* @param doc
*/
export function getDollarSchema(doc: SingleYAMLDocument | JSONDocument): string | undefined {
if ((doc instanceof SingleYAMLDocument || doc instanceof JSONDocument) && doc.root?.type === 'object') {
let dollarSchema: string | undefined = undefined;
for (const property of doc.root.properties) {
if (property.keyNode?.value === '$schema' && typeof property.valueNode?.value === 'string') {
dollarSchema = property.valueNode?.value;
break;
}
}
if (typeof dollarSchema === 'string') {
return dollarSchema;
}
if (dollarSchema) {
console.log('The $schema attribute is not a string, and will be ignored');
}
}
return undefined;
}
58 changes: 38 additions & 20 deletions src/languageservice/services/yamlSchemaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { SchemaVersions } from '../yamlTypes';

import Ajv, { DefinedError } from 'ajv';
import { getSchemaTitle } from '../utils/schemaUtils';
import { getDollarSchema } from './dollarUtils';

const localize = nls.loadMessageBundle();

Expand Down Expand Up @@ -343,33 +344,46 @@ export class YAMLSchemaService extends JSONSchemaService {
}

public getSchemaForResource(resource: string, doc: JSONDocument): Promise<ResolvedSchema> {
const normalizeSchemaRef = (schemaRef: string): string | undefined => {
if (!schemaRef.startsWith('file:') && !schemaRef.startsWith('http')) {
// If path contains a fragment and it is left intact, "#" will be
// considered part of the filename and converted to "%23" by
// path.resolve() -> take it out and add back after path.resolve
let appendix = '';
if (schemaRef.indexOf('#') > 0) {
const segments = schemaRef.split('#', 2);
schemaRef = segments[0];
appendix = segments[1];
}
if (!path.isAbsolute(schemaRef)) {
const resUri = URI.parse(resource);
schemaRef = URI.file(path.resolve(path.parse(resUri.fsPath).dir, schemaRef)).toString();
} else {
schemaRef = URI.file(schemaRef).toString();
}
if (appendix.length > 0) {
schemaRef += '#' + appendix;
}
}
return schemaRef;
};

const resolveModelineSchema = (): string | undefined => {
let schemaFromModeline = getSchemaFromModeline(doc);
if (schemaFromModeline !== undefined) {
if (!schemaFromModeline.startsWith('file:') && !schemaFromModeline.startsWith('http')) {
// If path contains a fragment and it is left intact, "#" will be
// considered part of the filename and converted to "%23" by
// path.resolve() -> take it out and add back after path.resolve
let appendix = '';
if (schemaFromModeline.indexOf('#') > 0) {
const segments = schemaFromModeline.split('#', 2);
schemaFromModeline = segments[0];
appendix = segments[1];
}
if (!path.isAbsolute(schemaFromModeline)) {
const resUri = URI.parse(resource);
schemaFromModeline = URI.file(path.resolve(path.parse(resUri.fsPath).dir, schemaFromModeline)).toString();
} else {
schemaFromModeline = URI.file(schemaFromModeline).toString();
}
if (appendix.length > 0) {
schemaFromModeline += '#' + appendix;
}
}
schemaFromModeline = normalizeSchemaRef(schemaFromModeline);
return schemaFromModeline;
}
};

const resolveDollarSchema = (): string | undefined => {
let dollarSchema = getDollarSchema(doc);
if (dollarSchema !== undefined) {
dollarSchema = normalizeSchemaRef(dollarSchema);
return dollarSchema;
}
};

const resolveSchemaForResource = (schemas: string[]): Promise<ResolvedSchema> => {
const schemaHandle = super.createCombinedSchema(resource, schemas);
return schemaHandle.getResolvedSchema().then((schema) => {
Expand Down Expand Up @@ -416,6 +430,10 @@ export class YAMLSchemaService extends JSONSchemaService {
if (modelineSchema) {
return resolveSchemaForResource([modelineSchema]);
}
const dollarSchema = resolveDollarSchema();
if (dollarSchema) {
return resolveSchemaForResource([dollarSchema]);
}
if (this.customSchemaProvider) {
return this.customSchemaProvider(resource)
.then((schemaUri) => {
Expand Down
8 changes: 8 additions & 0 deletions test/fixtures/sample-dollar-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"properties": {
"dollar-schema": {
"type":"string"
}
}
}
34 changes: 33 additions & 1 deletion test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,8 @@ describe('JSON Schema', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const schemaModelineSample = path.join(__dirname, './fixtures/sample-modeline.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const schemaDollarSample = path.join(__dirname, './fixtures/sample-dollar-schema.json');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const schemaDefaultSnippetSample = require(path.join(__dirname, './fixtures/defaultSnippets-const-if-else.json'));
const languageSettingsSetup = new ServiceSetup().withCompletion();

Expand All @@ -615,12 +617,42 @@ describe('JSON Schema', () => {
});
languageService.configure(languageSettingsSetup.languageSettings);
languageService.registerCustomSchemaProvider((uri: string) => Promise.resolve(uri));
const testTextDocument = setupTextDocument(`# yaml-language-server: $schema=${schemaModelineSample}\n\n`);
const testTextDocument = setupTextDocument(
`# yaml-language-server: $schema=${schemaModelineSample}\n$schema: ${schemaDollarSample}\n\n`
);
const result = await languageService.doComplete(testTextDocument, Position.create(1, 0), false);
assert.strictEqual(result.items.length, 1);
assert.strictEqual(result.items[0].label, 'modeline');
});

it('Explicit $schema takes precedence over all other lower priority schemas', async () => {
languageSettingsSetup
.withSchemaFileMatch({
fileMatch: ['test.yaml'],
uri: TEST_URI,
priority: SchemaPriority.SchemaStore,
schema: schemaStoreSample,
})
.withSchemaFileMatch({
fileMatch: ['test.yaml'],
uri: TEST_URI,
priority: SchemaPriority.SchemaAssociation,
schema: schemaAssociationSample,
})
.withSchemaFileMatch({
fileMatch: ['test.yaml'],
uri: TEST_URI,
priority: SchemaPriority.Settings,
schema: schemaSettingsSample,
});
languageService.configure(languageSettingsSetup.languageSettings);
languageService.registerCustomSchemaProvider((uri: string) => Promise.resolve(uri));
const testTextDocument = setupTextDocument(`$schema: ${schemaDollarSample}\n\n`);
const result = await languageService.doComplete(testTextDocument, Position.create(1, 0), false);
assert.strictEqual(result.items.length, 1);
assert.strictEqual(result.items[0].label, 'dollar-schema');
});

it('Manually setting schema takes precendence over all other lower priority schemas', async () => {
languageSettingsSetup
.withSchemaFileMatch({
Expand Down
Loading