Skip to content

Commit 769a8f7

Browse files
authored
Use AJV to validate referenced schemas (#1164)
* validate referenced schemas with AJV Signed-off-by: Morgan Chang <shin19991207@gmail.com> * extend jsonSchema with 2019-09/2020-12 keywords + add schema meta-validation tests Signed-off-by: Morgan Chang <shin19991207@gmail.com> * fix tests for Windows Signed-off-by: Morgan Chang <shin19991207@gmail.com> --------- Signed-off-by: Morgan Chang <shin19991207@gmail.com>
1 parent a38caf0 commit 769a8f7

File tree

11 files changed

+170
-26
lines changed

11 files changed

+170
-26
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{
77
"label": "watch typescript",
88
"type": "shell",
9-
"command": "yarn run watch",
9+
"command": "npm run watch",
1010
"presentation": {
1111
"reveal": "never"
1212
},

l10n/bundle.l10n.de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "Probleme beim Laden der Referenz '{0}': {1}",
55
"json.schema.nocontent": "Schema konnte nicht von '{0}' geladen werden: Kein Inhalt.",
66
"json.schema.invalidFormat": "Inhalt von '{0}' konnte nicht analysiert werden: Analysefehler in Zeile:{1}, Spalte:{2}",
7+
"json.schema.invalidSchema": "Schema '{0}' ist ungültig: {1}",
78
"colorHexFormatWarning": "Ungültiges Farbformat. Verwenden Sie #RGB, #RGBA, #RRGGBB oder #RRGGBBAA.",
89
"dateTimeFormatWarning": "Zeichenfolge ist kein RFC3339-Datum-Zeit-Wert.",
910
"dateFormatWarning": "Zeichenfolge ist kein RFC3339-Datum.",

l10n/bundle.l10n.fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "Problèmes de chargement de la référence '{0}' : {1}",
55
"json.schema.noContent": "Impossible de charger le schéma à partir de {0}: aucun contenu.",
66
"json.schema.invalidFormat": "Impossible d’analyser le contenu de {0}: erreur d’analyse à la ligne:{1}, colonne:{2}",
7+
"json.schema.invalidSchema": "Le schéma '{0}' n’est pas valide: {1}",
78
"colorHexFormatWarning": "Format de couleur non valide. Utilisez #RGB, #RGBA, #RRGGBB ou #RRGGBBAA.",
89
"dateTimeFormatWarning": "La chaîne n'est pas une date-heure RFC3339.",
910
"dateFormatWarning": "La chaîne n'est pas une date RFC3339.",

l10n/bundle.l10n.ja.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "参照 '{0}' の読み込み中に問題が発生しました: {1}",
55
"json.schema.nocontent": "'{0}' からスキーマを読み込めませんでした: コンテンツがありません。",
66
"json.schema.invalidFormat": "'{0}' の内容を解析できませんでした: 行 {1}、列 {2} で解析エラーが発生しました",
7+
"json.schema.invalidSchema": "スキーマ '{0}' は無効です: {1}",
78
"colorHexFormatWarning": "無効なカラー形式です。#RGB、#RGBA、#RRGGBB、または #RRGGBBAA を使用してください。",
89
"dateTimeFormatWarning": "文字列は RFC3339 の日付と時刻形式ではありません。",
910
"dateFormatWarning": "文字列は RFC3339 の日付形式ではありません。",

l10n/bundle.l10n.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "Problems loading reference '{0}': {1}",
55
"json.schema.noContent": "Unable to load schema from '{0}': No content.",
66
"json.schema.invalidFormat": "Unable to parse content from '{0}': Parse error at line: {1} column: {2}",
7+
"json.schema.invalidSchema": "Schema '{0}' is not valid: {1}",
78
"colorHexFormatWarning": "Invalid color format. Use #RGB, #RGBA, #RRGGBB or #RRGGBBAA.",
89
"dateTimeFormatWarning": "String is not a RFC3339 date-time.",
910
"dateFormatWarning": "String is not a RFC3339 date.",

l10n/bundle.l10n.ko.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "'{0}' 참조를 불러오는 데 문제가 발생했습니다: {1}",
55
"json.schema.nocontent": "'{0}'에서 스키마를 불러올 수 없습니다: 내용이 없습니다.",
66
"json.schema.invalidFormat": "'{0}'의 내용을 구문 분석할 수 없습니다: {1}행 {2}열에서 구문 오류가 발생했습니다",
7+
"json.schema.invalidSchema": "스키마 '{0}'이(가) 유효하지 않습니다: {1}",
78
"colorHexFormatWarning": "잘못된 색상 형식입니다. #RGB, #RGBA, #RRGGBB 또는 #RRGGBBAA를 사용하세요.",
89
"dateTimeFormatWarning": "문자열이 RFC3339 날짜-시간 형식이 아닙니다.",
910
"dateFormatWarning": "문자열이 RFC3339 날짜 형식이 아닙니다.",

l10n/bundle.l10n.zh-cn.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "加载引用 '{0}' 时出现问题:{1}",
55
"json.schema.nocontent": "无法从“{0}”加载架构:没有内容。",
66
"json.schema.invalidFormat": "无法解析来自“{0}”的内容:在第 {1} 行第 {2} 列发生解析错误",
7+
"json.schema.invalidSchema": "架构 '{0}' 无效: {1}",
78
"colorHexFormatWarning": "无效的颜色格式。请使用 #RGB、#RGBA、#RRGGBB 或 #RRGGBBAA。",
89
"dateTimeFormatWarning": "字符串不是 RFC3339 日期时间格式。",
910
"dateFormatWarning": "字符串不是 RFC3339 日期格式。",

l10n/bundle.l10n.zh-tw.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"json.schema.problemloadingref": "載入參考 '{0}' 時出現問題:{1}",
55
"json.schema.nocontent": "無法從「{0}」載入結構描述:沒有內容。",
66
"json.schema.invalidFormat": "無法解析來自「{0}」的內容:在第 {1} 行第 {2} 欄發生解析錯誤",
7+
"json.schema.invalidSchema": "結構描述 '{0}' 無效:{1}",
78
"colorHexFormatWarning": "無效的顏色格式。請使用 #RGB、#RGBA、#RRGGBB 或 #RRGGBBAA。",
89
"dateTimeFormatWarning": "字串不是 RFC3339 日期時間格式。",
910
"dateFormatWarning": "字串不是 RFC3339 日期格式。",

src/languageservice/jsonSchema.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ export interface JSONSchema {
6565
then?: JSONSchemaRef;
6666
else?: JSONSchemaRef;
6767

68+
// schema draft 2019-09
69+
$anchor?: string;
70+
$defs?: { [name: string]: JSONSchema };
71+
$recursiveAnchor?: boolean;
72+
$recursiveRef?: string;
73+
$vocabulary?: Record<string, boolean>;
74+
dependentSchemas?: JSONSchemaMap;
75+
unevaluatedItems?: boolean | JSONSchemaRef;
76+
unevaluatedProperties?: boolean | JSONSchemaRef;
77+
dependentRequired?: Record<string, string[]>;
78+
minContains?: number;
79+
maxContains?: number;
80+
81+
// schema draft 2020-12
82+
prefixItems?: JSONSchemaRef[];
83+
$dynamicRef?: string;
84+
$dynamicAnchor?: string;
85+
6886
// VSCode extensions
6987

7088
defaultSnippets?: {

src/languageservice/services/yamlSchemaService.ts

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,29 @@ import { SchemaVersions } from '../yamlTypes';
2727

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

34-
const ajv = new Ajv();
35-
const ajv4 = new Ajv4();
35+
const ajv4 = new Ajv4({ allErrors: true });
36+
const ajv7 = new Ajv({ allErrors: true });
37+
const ajv2019 = new Ajv2019({ allErrors: true });
38+
const ajv2020 = new Ajv2020({ allErrors: true });
3639

37-
// load JSON Schema 07 def to validate loaded schemas
40+
// eslint-disable-next-line @typescript-eslint/no-var-requires
41+
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');
3842
// eslint-disable-next-line @typescript-eslint/no-var-requires
3943
const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json');
40-
const schema07Validator = ajv.compile(jsonSchema07);
41-
4244
// eslint-disable-next-line @typescript-eslint/no-var-requires
43-
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');
45+
const jsonSchema2019 = require('ajv/dist/refs/json-schema-2019-09/schema.json');
46+
// eslint-disable-next-line @typescript-eslint/no-var-requires
47+
const jsonSchema2020 = require('ajv/dist/refs/json-schema-2020-12/schema.json');
48+
4449
const schema04Validator = ajv4.compile(jsonSchema04);
45-
const SCHEMA_04_URI_WITH_HTTPS = ajv4.defaultMeta().replace('http://', 'https://');
50+
const schema07Validator = ajv7.compile(jsonSchema07);
51+
const schema2019Validator = ajv2019.compile(jsonSchema2019);
52+
const schema2020Validator = ajv2020.compile(jsonSchema2020);
4653

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

@@ -166,19 +173,24 @@ export class YAMLSchemaService extends JSONSchemaService {
166173
dependencies: SchemaDependencies
167174
): Promise<ResolvedSchema> {
168175
const resolveErrors: string[] = schemaToResolve.errors.slice(0);
169-
let schema: JSONSchema = schemaToResolve.schema;
170-
const contextService = this.contextService;
176+
const loc = toDisplayString(schemaURL);
177+
178+
const raw: unknown = schemaToResolve.schema;
179+
if (raw === null || Array.isArray(raw) || (typeof raw !== 'object' && typeof raw !== 'boolean')) {
180+
const got = raw === null ? 'null' : Array.isArray(raw) ? 'array' : typeof raw;
181+
resolveErrors.push(l10n.t('json.schema.invalidSchema', loc, `expected a JSON Schema object or boolean, got ${got}`));
182+
return new ResolvedSchema({}, resolveErrors);
183+
}
171184

172-
const validator =
173-
this.normalizeId(schema.$schema) === ajv4.defaultMeta() || this.normalizeId(schema.$schema) === SCHEMA_04_URI_WITH_HTTPS
174-
? schema04Validator
175-
: schema07Validator;
176-
if (!validator(schema)) {
185+
const contextService = this.contextService;
186+
let schema = raw as JSONSchema;
187+
const validator = pickMetaValidator(schema.$schema);
188+
if (validator && !validator(schema)) {
177189
const errs: string[] = [];
178190
for (const err of validator.errors as DefinedError[]) {
179191
errs.push(`${err.instancePath} : ${err.message}`);
180192
}
181-
resolveErrors.push(`Schema '${getSchemaTitle(schemaToResolve.schema, schemaURL)}' is not valid:\n${errs.join('\n')}`);
193+
resolveErrors.push(l10n.t('json.schema.invalidSchema', loc, `\n${errs.join('\n')}`));
182194
}
183195

184196
const findSection = (schema: JSONSchema, path: string): JSONSchema => {
@@ -764,3 +776,38 @@ function getLineAndColumnFromOffset(text: string, offset: number): { line: numbe
764776
const column = lines[lines.length - 1].length + 1; // 1-based column number
765777
return { line, column };
766778
}
779+
780+
function normalizeSchemaUri(uri: string | AnySchemaObject): string {
781+
if (!uri) return '';
782+
783+
let s: string;
784+
if (typeof uri === 'string') {
785+
s = uri;
786+
} else {
787+
s = uri.$id || uri.id || '';
788+
}
789+
s = s.trim();
790+
791+
// strips fragment (# or #/something)
792+
const hash = s.indexOf('#');
793+
794+
s = hash === -1 ? s : s.slice(0, hash);
795+
796+
// normalize http to https (don't normalize custom dialects)
797+
s = s.replace(/^http:\/\/json-schema\.org\//i, 'https://json-schema.org/');
798+
799+
// normalize to no trailing slash
800+
s = s.replace(/\/+$/g, '');
801+
return s;
802+
}
803+
804+
function pickMetaValidator(schema: string): ValidateFunction | undefined {
805+
const s = normalizeSchemaUri(schema);
806+
if (s === normalizeSchemaUri(ajv4.defaultMeta())) return schema04Validator;
807+
if (s === normalizeSchemaUri(ajv7.defaultMeta())) return schema07Validator;
808+
if (s === normalizeSchemaUri(ajv2019.defaultMeta())) return schema2019Validator;
809+
if (s === normalizeSchemaUri(ajv2020.defaultMeta())) return schema2020Validator;
810+
811+
// don't meta-validate unknown schema URI
812+
return undefined;
813+
}

0 commit comments

Comments
 (0)