Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{
"label": "watch typescript",
"type": "shell",
"command": "yarn run watch",
"command": "npm run watch",
"presentation": {
"reveal": "never"
},
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "Probleme beim Laden der Referenz '{0}': {1}",
"json.schema.nocontent": "Schema konnte nicht von '{0}' geladen werden: Kein Inhalt.",
"json.schema.invalidFormat": "Inhalt von '{0}' konnte nicht analysiert werden: Analysefehler in Zeile:{1}, Spalte:{2}",
"json.schema.invalidSchema": "Schema '{0}' ist ungültig: {1}",
"colorHexFormatWarning": "Ungültiges Farbformat. Verwenden Sie #RGB, #RGBA, #RRGGBB oder #RRGGBBAA.",
"dateTimeFormatWarning": "Zeichenfolge ist kein RFC3339-Datum-Zeit-Wert.",
"dateFormatWarning": "Zeichenfolge ist kein RFC3339-Datum.",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "Problèmes de chargement de la référence '{0}' : {1}",
"json.schema.noContent": "Impossible de charger le schéma à partir de {0}: aucun contenu.",
"json.schema.invalidFormat": "Impossible d’analyser le contenu de {0}: erreur d’analyse à la ligne:{1}, colonne:{2}",
"json.schema.invalidSchema": "Le schéma '{0}' n’est pas valide: {1}",
"colorHexFormatWarning": "Format de couleur non valide. Utilisez #RGB, #RGBA, #RRGGBB ou #RRGGBBAA.",
"dateTimeFormatWarning": "La chaîne n'est pas une date-heure RFC3339.",
"dateFormatWarning": "La chaîne n'est pas une date RFC3339.",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "参照 '{0}' の読み込み中に問題が発生しました: {1}",
"json.schema.nocontent": "'{0}' からスキーマを読み込めませんでした: コンテンツがありません。",
"json.schema.invalidFormat": "'{0}' の内容を解析できませんでした: 行 {1}、列 {2} で解析エラーが発生しました",
"json.schema.invalidSchema": "スキーマ '{0}' は無効です: {1}",
"colorHexFormatWarning": "無効なカラー形式です。#RGB、#RGBA、#RRGGBB、または #RRGGBBAA を使用してください。",
"dateTimeFormatWarning": "文字列は RFC3339 の日付と時刻形式ではありません。",
"dateFormatWarning": "文字列は RFC3339 の日付形式ではありません。",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "Problems loading reference '{0}': {1}",
"json.schema.noContent": "Unable to load schema from '{0}': No content.",
"json.schema.invalidFormat": "Unable to parse content from '{0}': Parse error at line: {1} column: {2}",
"json.schema.invalidSchema": "Schema '{0}' is not valid: {1}",
"colorHexFormatWarning": "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.",
"dateTimeFormatWarning": "String is not a RFC3339 date-time.",
"dateFormatWarning": "String is not a RFC3339 date.",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "'{0}' 참조를 불러오는 데 문제가 발생했습니다: {1}",
"json.schema.nocontent": "'{0}'에서 스키마를 불러올 수 없습니다: 내용이 없습니다.",
"json.schema.invalidFormat": "'{0}'의 내용을 구문 분석할 수 없습니다: {1}행 {2}열에서 구문 오류가 발생했습니다",
"json.schema.invalidSchema": "스키마 '{0}'이(가) 유효하지 않습니다: {1}",
"colorHexFormatWarning": "잘못된 색상 형식입니다. #RGB, #RGBA, #RRGGBB 또는 #RRGGBBAA를 사용하세요.",
"dateTimeFormatWarning": "문자열이 RFC3339 날짜-시간 형식이 아닙니다.",
"dateFormatWarning": "문자열이 RFC3339 날짜 형식이 아닙니다.",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "加载引用 '{0}' 时出现问题:{1}",
"json.schema.nocontent": "无法从“{0}”加载架构:没有内容。",
"json.schema.invalidFormat": "无法解析来自“{0}”的内容:在第 {1} 行第 {2} 列发生解析错误",
"json.schema.invalidSchema": "架构 '{0}' 无效: {1}",
"colorHexFormatWarning": "无效的颜色格式。请使用 #RGB、#RGBA、#RRGGBB 或 #RRGGBBAA。",
"dateTimeFormatWarning": "字符串不是 RFC3339 日期时间格式。",
"dateFormatWarning": "字符串不是 RFC3339 日期格式。",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"json.schema.problemloadingref": "載入參考 '{0}' 時出現問題:{1}",
"json.schema.nocontent": "無法從「{0}」載入結構描述:沒有內容。",
"json.schema.invalidFormat": "無法解析來自「{0}」的內容:在第 {1} 行第 {2} 欄發生解析錯誤",
"json.schema.invalidSchema": "結構描述 '{0}' 無效:{1}",
"colorHexFormatWarning": "無效的顏色格式。請使用 #RGB、#RGBA、#RRGGBB 或 #RRGGBBAA。",
"dateTimeFormatWarning": "字串不是 RFC3339 日期時間格式。",
"dateFormatWarning": "字串不是 RFC3339 日期格式。",
Expand Down
18 changes: 18 additions & 0 deletions src/languageservice/jsonSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ export interface JSONSchema {
then?: JSONSchemaRef;
else?: JSONSchemaRef;

// schema draft 2019-09
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly needed for this change, but I'm okay with adding it now, since we'll need to do it eventually.

$anchor?: string;
$defs?: { [name: string]: JSONSchema };
$recursiveAnchor?: boolean;
$recursiveRef?: string;
$vocabulary?: Record<string, boolean>;
dependentSchemas?: JSONSchemaMap;
unevaluatedItems?: boolean | JSONSchemaRef;
unevaluatedProperties?: boolean | JSONSchemaRef;
dependentRequired?: Record<string, string[]>;
minContains?: number;
maxContains?: number;

// schema draft 2020-12
prefixItems?: JSONSchemaRef[];
$dynamicRef?: string;
$dynamicAnchor?: string;

// VSCode extensions

defaultSnippets?: {
Expand Down
81 changes: 64 additions & 17 deletions src/languageservice/services/yamlSchemaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,29 @@

import { parse } from 'yaml';
import * as Json from 'jsonc-parser';
import Ajv, { DefinedError } from 'ajv';
import Ajv, { DefinedError, type AnySchemaObject, type ValidateFunction } from 'ajv';
import Ajv4 from 'ajv-draft-04';
import { getSchemaTitle } from '../utils/schemaUtils';
import Ajv2019 from 'ajv/dist/2019';
import Ajv2020 from 'ajv/dist/2020';

const ajv = new Ajv();
const ajv4 = new Ajv4();
const ajv4 = new Ajv4({ allErrors: true });
const ajv7 = new Ajv({ allErrors: true });
const ajv2019 = new Ajv2019({ allErrors: true });
const ajv2020 = new Ajv2020({ allErrors: true });

// load JSON Schema 07 def to validate loaded schemas
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');

Check failure

Code scanning / ESLint

Disallow `require` statements except in import statements Error

Require statement not part of import statement.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json');
const schema07Validator = ajv.compile(jsonSchema07);

// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');
const jsonSchema2019 = require('ajv/dist/refs/json-schema-2019-09/schema.json');

Check failure

Code scanning / ESLint

Disallow `require` statements except in import statements Error

Require statement not part of import statement.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema2020 = require('ajv/dist/refs/json-schema-2020-12/schema.json');

Check failure

Code scanning / ESLint

Disallow `require` statements except in import statements Error

Require statement not part of import statement.

const schema04Validator = ajv4.compile(jsonSchema04);
const SCHEMA_04_URI_WITH_HTTPS = ajv4.defaultMeta().replace('http://', 'https://');
const schema07Validator = ajv7.compile(jsonSchema07);
const schema2019Validator = ajv2019.compile(jsonSchema2019);
const schema2020Validator = ajv2020.compile(jsonSchema2020);

export declare type CustomSchemaProvider = (uri: string) => Promise<string | string[]>;

Expand Down Expand Up @@ -166,19 +173,24 @@
dependencies: SchemaDependencies
): Promise<ResolvedSchema> {
const resolveErrors: string[] = schemaToResolve.errors.slice(0);
let schema: JSONSchema = schemaToResolve.schema;
const contextService = this.contextService;
const loc = toDisplayString(schemaURL);

const raw: unknown = schemaToResolve.schema;
if (raw === null || Array.isArray(raw) || (typeof raw !== 'object' && typeof raw !== 'boolean')) {
const got = raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw;
resolveErrors.push(l10n.t('json.schema.invalidSchema', loc, `expected a JSON Schema object or boolean, got ${got}`));
return new ResolvedSchema({}, resolveErrors);
}

const validator =
this.normalizeId(schema.$schema) === ajv4.defaultMeta() || this.normalizeId(schema.$schema) === SCHEMA_04_URI_WITH_HTTPS
? schema04Validator
: schema07Validator;
if (!validator(schema)) {
const contextService = this.contextService;
let schema = raw as JSONSchema;
const validator = pickMetaValidator(schema.$schema);
if (validator && !validator(schema)) {
const errs: string[] = [];
for (const err of validator.errors as DefinedError[]) {
errs.push(`${err.instancePath} : ${err.message}`);
}
resolveErrors.push(`Schema '${getSchemaTitle(schemaToResolve.schema, schemaURL)}' is not valid:\n${errs.join('\n')}`);
resolveErrors.push(l10n.t('json.schema.invalidSchema', loc, `\n${errs.join('\n')}`));
}

const findSection = (schema: JSONSchema, path: string): JSONSchema => {
Expand Down Expand Up @@ -764,3 +776,38 @@
const column = lines[lines.length - 1].length + 1; // 1-based column number
return { line, column };
}

function normalizeSchemaUri(uri: string | AnySchemaObject): string {
if (!uri) return '';

let s: string;
if (typeof uri === 'string') {
s = uri;
} else {
s = uri.$id || uri.id || '';
}
s = s.trim();

// strips fragment (# or #/something)
const hash = s.indexOf('#');

s = hash === -1 ? s : s.slice(0, hash);

// normalize http to https (don't normalize custom dialects)
s = s.replace(/^http:\/\/json-schema\.org\//i, 'https://json-schema.org/');

// normalize to no trailing slash
s = s.replace(/\/+$/g, '');
return s;
}

function pickMetaValidator(schema: string): ValidateFunction | undefined {
const s = normalizeSchemaUri(schema);
if (s === normalizeSchemaUri(ajv4.defaultMeta())) return schema04Validator;
if (s === normalizeSchemaUri(ajv7.defaultMeta())) return schema07Validator;
if (s === normalizeSchemaUri(ajv2019.defaultMeta())) return schema2019Validator;
if (s === normalizeSchemaUri(ajv2020.defaultMeta())) return schema2020Validator;

// don't meta-validate unknown schema URI
return undefined;
}
88 changes: 80 additions & 8 deletions test/schemaValidation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1968,14 +1968,86 @@ obj:
expect(telemetry.messages).to.be.empty;
});

it('should handle not valid schema object', async () => {
const schema = 'Foo';
schemaProvider.addSchema(SCHEMA_ID, schema as JSONSchema);
const content = `foo: bar`;
const result = await parseSetup(content);
expect(result).to.have.length(1);
expect(result[0].message).to.include("Schema 'default_schema_id.yaml' is not valid");
expect(telemetry.messages).to.be.empty;
describe('Schema meta-validation', () => {
it('should handle not valid schema object', async () => {
const schema = 'Foo';
schemaProvider.addSchema(SCHEMA_ID, schema as JSONSchema);
const content = `foo: bar`;
const result = await parseSetup(content);
expect(result).to.have.length(1);
expect(result[0].message).to.include('default_schema_id.yaml');
expect(result[0].message).to.include('is not valid:');
expect(result[0].message).to.include('expected a JSON Schema object or boolean, got string');
expect(telemetry.messages).to.be.empty;
});

const content = '6';

it('draft-04: exclusiveMinimum must be boolean', async () => {
const schema = {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'number',
minimum: 5,
exclusiveMinimum: 5,
} as unknown as JSONSchema;
schemaProvider.addSchema(SCHEMA_ID, schema);
const result = await parseSetup(content);
expect(result).to.have.length(1);
expect(result[0].message).to.include('default_schema_id.yaml');
expect(result[0].message).to.include('is not valid:');
expect(result[0].message).to.include('exclusiveMinimum');
});

it('draft-07: exclusiveMinimum must be number', async () => {
const schema: JSONSchema = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'number',
minimum: 5,
exclusiveMinimum: true,
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const result = await parseSetup(content);
expect(result).to.have.length(1);
expect(result[0].message).to.include('default_schema_id.yaml');
expect(result[0].message).to.include('is not valid:');
expect(result[0].message).to.include('exclusiveMinimum');
});

it('draft-2019-09: should handle invalid type in $defs', async () => {
const schema: JSONSchema = {
$schema: 'https://json-schema.org/draft/2019-09/schema',
$defs: {
foo: {
type: 'object',
properties: {
bar: {
type: ['foo', 'bar'],
},
},
},
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const result = await parseSetup(content);
expect(result).to.have.length(1);
expect(result[0].message).to.include('default_schema_id.yaml');
expect(result[0].message).to.include('is not valid:');
expect(result[0].message).to.include('$defs');
});

it('draft-2020-12: prefixItems must be an array', async () => {
const schema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'number',
prefixItems: 'foo',
} as unknown as JSONSchema;
schemaProvider.addSchema(SCHEMA_ID, schema);
const result = await parseSetup(content);
expect(result).to.have.length(1);
expect(result[0].message).to.include('default_schema_id.yaml');
expect(result[0].message).to.include('is not valid:');
expect(result[0].message).to.include('prefixItems');
});
});

it('should handle bad schema refs', async () => {
Expand Down