/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ (function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "jsonc-parser", "vscode-uri", "../utils/strings", "../parser/jsonParser", "vscode-nls", "../utils/glob"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JSONSchemaService = exports.ResolvedSchema = exports.UnresolvedSchema = void 0; var Json = require("jsonc-parser"); var vscode_uri_1 = require("vscode-uri"); var Strings = require("../utils/strings"); var Parser = require("../parser/jsonParser"); var nls = require("vscode-nls"); var glob_1 = require("../utils/glob"); var localize = nls.loadMessageBundle(); var BANG = '!'; var PATH_SEP = '/'; var FilePatternAssociation = /** @class */ (function () { function FilePatternAssociation(pattern, uris) { this.globWrappers = []; try { for (var _i = 0, pattern_1 = pattern; _i < pattern_1.length; _i++) { var patternString = pattern_1[_i]; var include = patternString[0] !== BANG; if (!include) { patternString = patternString.substring(1); } if (patternString.length > 0) { if (patternString[0] === PATH_SEP) { patternString = patternString.substring(1); } this.globWrappers.push({ regexp: glob_1.createRegex('**/' + patternString, { extended: true, globstar: true }), include: include, }); } } ; this.uris = uris; } catch (e) { this.globWrappers.length = 0; this.uris = []; } } FilePatternAssociation.prototype.matchesPattern = function (fileName) { var match = false; for (var _i = 0, _a = this.globWrappers; _i < _a.length; _i++) { var _b = _a[_i], regexp = _b.regexp, include = _b.include; if (regexp.test(fileName)) { match = include; } } return match; }; FilePatternAssociation.prototype.getURIs = function () { return this.uris; }; return FilePatternAssociation; }()); var SchemaHandle = /** @class */ (function () { function SchemaHandle(service, url, unresolvedSchemaContent) { this.service = service; this.url = url; this.dependencies = {}; if (unresolvedSchemaContent) { this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent)); } } SchemaHandle.prototype.getUnresolvedSchema = function () { if (!this.unresolvedSchema) { this.unresolvedSchema = this.service.loadSchema(this.url); } return this.unresolvedSchema; }; SchemaHandle.prototype.getResolvedSchema = function () { var _this = this; if (!this.resolvedSchema) { this.resolvedSchema = this.getUnresolvedSchema().then(function (unresolved) { return _this.service.resolveSchemaContent(unresolved, _this.url, _this.dependencies); }); } return this.resolvedSchema; }; SchemaHandle.prototype.clearSchema = function () { this.resolvedSchema = undefined; this.unresolvedSchema = undefined; this.dependencies = {}; }; return SchemaHandle; }()); var UnresolvedSchema = /** @class */ (function () { function UnresolvedSchema(schema, errors) { if (errors === void 0) { errors = []; } this.schema = schema; this.errors = errors; } return UnresolvedSchema; }()); exports.UnresolvedSchema = UnresolvedSchema; var ResolvedSchema = /** @class */ (function () { function ResolvedSchema(schema, errors) { if (errors === void 0) { errors = []; } this.schema = schema; this.errors = errors; } ResolvedSchema.prototype.getSection = function (path) { var schemaRef = this.getSectionRecursive(path, this.schema); if (schemaRef) { return Parser.asSchema(schemaRef); } return undefined; }; ResolvedSchema.prototype.getSectionRecursive = function (path, schema) { if (!schema || typeof schema === 'boolean' || path.length === 0) { return schema; } var next = path.shift(); if (schema.properties && typeof schema.properties[next]) { return this.getSectionRecursive(path, schema.properties[next]); } else if (schema.patternProperties) { for (var _i = 0, _a = Object.keys(schema.patternProperties); _i < _a.length; _i++) { var pattern = _a[_i]; var regex = Strings.extendedRegExp(pattern); if (regex.test(next)) { return this.getSectionRecursive(path, schema.patternProperties[pattern]); } } } else if (typeof schema.additionalProperties === 'object') { return this.getSectionRecursive(path, schema.additionalProperties); } else if (next.match('[0-9]+')) { if (Array.isArray(schema.items)) { var index = parseInt(next, 10); if (!isNaN(index) && schema.items[index]) { return this.getSectionRecursive(path, schema.items[index]); } } else if (schema.items) { return this.getSectionRecursive(path, schema.items); } } return undefined; }; return ResolvedSchema; }()); exports.ResolvedSchema = ResolvedSchema; var JSONSchemaService = /** @class */ (function () { function JSONSchemaService(requestService, contextService, promiseConstructor) { this.contextService = contextService; this.requestService = requestService; this.promiseConstructor = promiseConstructor || Promise; this.callOnDispose = []; this.contributionSchemas = {}; this.contributionAssociations = []; this.schemasById = {}; this.filePatternAssociations = []; this.registeredSchemasIds = {}; } JSONSchemaService.prototype.getRegisteredSchemaIds = function (filter) { return Object.keys(this.registeredSchemasIds).filter(function (id) { var scheme = vscode_uri_1.URI.parse(id).scheme; return scheme !== 'schemaservice' && (!filter || filter(scheme)); }); }; Object.defineProperty(JSONSchemaService.prototype, "promise", { get: function () { return this.promiseConstructor; }, enumerable: false, configurable: true }); JSONSchemaService.prototype.dispose = function () { while (this.callOnDispose.length > 0) { this.callOnDispose.pop()(); } }; JSONSchemaService.prototype.onResourceChange = function (uri) { var _this = this; var hasChanges = false; uri = normalizeId(uri); var toWalk = [uri]; var all = Object.keys(this.schemasById).map(function (key) { return _this.schemasById[key]; }); while (toWalk.length) { var curr = toWalk.pop(); for (var i = 0; i < all.length; i++) { var handle = all[i]; if (handle && (handle.url === curr || handle.dependencies[curr])) { if (handle.url !== curr) { toWalk.push(handle.url); } handle.clearSchema(); all[i] = undefined; hasChanges = true; } } } return hasChanges; }; JSONSchemaService.prototype.setSchemaContributions = function (schemaContributions) { if (schemaContributions.schemas) { var schemas = schemaContributions.schemas; for (var id in schemas) { var normalizedId = normalizeId(id); this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]); } } if (Array.isArray(schemaContributions.schemaAssociations)) { var schemaAssociations = schemaContributions.schemaAssociations; for (var _i = 0, schemaAssociations_1 = schemaAssociations; _i < schemaAssociations_1.length; _i++) { var schemaAssociation = schemaAssociations_1[_i]; var uris = schemaAssociation.uris.map(normalizeId); var association = this.addFilePatternAssociation(schemaAssociation.pattern, uris); this.contributionAssociations.push(association); } } }; JSONSchemaService.prototype.addSchemaHandle = function (id, unresolvedSchemaContent) { var schemaHandle = new SchemaHandle(this, id, unresolvedSchemaContent); this.schemasById[id] = schemaHandle; return schemaHandle; }; JSONSchemaService.prototype.getOrAddSchemaHandle = function (id, unresolvedSchemaContent) { return this.schemasById[id] || this.addSchemaHandle(id, unresolvedSchemaContent); }; JSONSchemaService.prototype.addFilePatternAssociation = function (pattern, uris) { var fpa = new FilePatternAssociation(pattern, uris); this.filePatternAssociations.push(fpa); return fpa; }; JSONSchemaService.prototype.registerExternalSchema = function (uri, filePatterns, unresolvedSchemaContent) { var id = normalizeId(uri); this.registeredSchemasIds[id] = true; this.cachedSchemaForResource = undefined; if (filePatterns) { this.addFilePatternAssociation(filePatterns, [uri]); } return unresolvedSchemaContent ? this.addSchemaHandle(id, unresolvedSchemaContent) : this.getOrAddSchemaHandle(id); }; JSONSchemaService.prototype.clearExternalSchemas = function () { this.schemasById = {}; this.filePatternAssociations = []; this.registeredSchemasIds = {}; this.cachedSchemaForResource = undefined; for (var id in this.contributionSchemas) { this.schemasById[id] = this.contributionSchemas[id]; this.registeredSchemasIds[id] = true; } for (var _i = 0, _a = this.contributionAssociations; _i < _a.length; _i++) { var contributionAssociation = _a[_i]; this.filePatternAssociations.push(contributionAssociation); } }; JSONSchemaService.prototype.getResolvedSchema = function (schemaId) { var id = normalizeId(schemaId); var schemaHandle = this.schemasById[id]; if (schemaHandle) { return schemaHandle.getResolvedSchema(); } return this.promise.resolve(undefined); }; JSONSchemaService.prototype.loadSchema = function (url) { if (!this.requestService) { var errorMessage = localize('json.schema.norequestservice', 'Unable to load schema from \'{0}\'. No schema request service available', toDisplayString(url)); return this.promise.resolve(new UnresolvedSchema({}, [errorMessage])); } return this.requestService(url).then(function (content) { if (!content) { var errorMessage = localize('json.schema.nocontent', 'Unable to load schema from \'{0}\': No content.', toDisplayString(url)); return new UnresolvedSchema({}, [errorMessage]); } var schemaContent = {}; var jsonErrors = []; schemaContent = Json.parse(content, jsonErrors); var errors = jsonErrors.length ? [localize('json.schema.invalidFormat', 'Unable to parse content from \'{0}\': Parse error at offset {1}.', toDisplayString(url), jsonErrors[0].offset)] : []; return new UnresolvedSchema(schemaContent, errors); }, function (error) { var errorMessage = error.toString(); var errorSplit = error.toString().split('Error: '); if (errorSplit.length > 1) { // more concise error message, URL and context are attached by caller anyways errorMessage = errorSplit[1]; } if (Strings.endsWith(errorMessage, '.')) { errorMessage = errorMessage.substr(0, errorMessage.length - 1); } return new UnresolvedSchema({}, [localize('json.schema.nocontent', 'Unable to load schema from \'{0}\': {1}.', toDisplayString(url), errorMessage)]); }); }; JSONSchemaService.prototype.resolveSchemaContent = function (schemaToResolve, schemaURL, dependencies) { var _this = this; var resolveErrors = schemaToResolve.errors.slice(0); var schema = schemaToResolve.schema; if (schema.$schema) { var id = normalizeId(schema.$schema); if (id === 'http://json-schema.org/draft-03/schema') { return this.promise.resolve(new ResolvedSchema({}, [localize('json.schema.draft03.notsupported', "Draft-03 schemas are not supported.")])); } else if (id === 'https://json-schema.org/draft/2019-09/schema') { resolveErrors.push(localize('json.schema.draft201909.notsupported', "Draft 2019-09 schemas are not yet fully supported.")); } } var contextService = this.contextService; var findSection = function (schema, path) { if (!path) { return schema; } var current = schema; if (path[0] === '/') { path = path.substr(1); } path.split('/').some(function (part) { part = part.replace(/~1/g, '/').replace(/~0/g, '~'); current = current[part]; return !current; }); return current; }; var merge = function (target, sourceRoot, sourceURI, refSegment) { var path = refSegment ? decodeURIComponent(refSegment) : undefined; var section = findSection(sourceRoot, path); if (section) { for (var 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)); } }; var resolveExternalLink = function (node, uri, refSegment, parentSchemaURL, parentSchemaDependencies) { if (contextService && !/^[A-Za-z][A-Za-z0-9+\-.+]*:\/\/.*/.test(uri)) { uri = contextService.resolveRelativePath(uri, parentSchemaURL); } uri = normalizeId(uri); var referencedHandle = _this.getOrAddSchemaHandle(uri); return referencedHandle.getUnresolvedSchema().then(function (unresolvedSchema) { parentSchemaDependencies[uri] = true; if (unresolvedSchema.errors.length) { var loc = refSegment ? uri + '#' + refSegment : uri; resolveErrors.push(localize('json.schema.problemloadingref', 'Problems loading reference \'{0}\': {1}', loc, unresolvedSchema.errors[0])); } merge(node, unresolvedSchema.schema, uri, refSegment); return resolveRefs(node, unresolvedSchema.schema, uri, referencedHandle.dependencies); }); }; var resolveRefs = function (node, parentSchema, parentSchemaURL, parentSchemaDependencies) { if (!node || typeof node !== 'object') { return Promise.resolve(null); } var toWalk = [node]; var seen = []; var openPromises = []; var collectEntries = function () { var entries = []; for (var _i = 0; _i < arguments.length; _i++) { entries[_i] = arguments[_i]; } for (var _a = 0, entries_1 = entries; _a < entries_1.length; _a++) { var entry = entries_1[_a]; if (typeof entry === 'object') { toWalk.push(entry); } } }; var collectMapEntries = function () { var maps = []; for (var _i = 0; _i < arguments.length; _i++) { maps[_i] = arguments[_i]; } for (var _a = 0, maps_1 = maps; _a < maps_1.length; _a++) { var map = maps_1[_a]; if (typeof map === 'object') { for (var k in map) { var key = k; var entry = map[key]; if (typeof entry === 'object') { toWalk.push(entry); } } } } }; var collectArrayEntries = function () { var arrays = []; for (var _i = 0; _i < arguments.length; _i++) { arrays[_i] = arguments[_i]; } for (var _a = 0, arrays_1 = arrays; _a < arrays_1.length; _a++) { var array = arrays_1[_a]; if (Array.isArray(array)) { for (var _b = 0, array_1 = array; _b < array_1.length; _b++) { var entry = array_1[_b]; if (typeof entry === 'object') { toWalk.push(entry); } } } } }; var handleRef = function (next) { var seenRefs = []; while (next.$ref) { var ref = next.$ref; var 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.additionalItems, 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); }; while (toWalk.length) { var next = toWalk.pop(); if (seen.indexOf(next) >= 0) { continue; } seen.push(next); handleRef(next); } return _this.promise.all(openPromises); }; return resolveRefs(schema, schema, schemaURL, dependencies).then(function (_) { return new ResolvedSchema(schema, resolveErrors); }); }; JSONSchemaService.prototype.getSchemaForResource = function (resource, document) { // first use $schema if present if (document && document.root && document.root.type === 'object') { var schemaProperties = document.root.properties.filter(function (p) { return (p.keyNode.value === '$schema') && p.valueNode && p.valueNode.type === 'string'; }); if (schemaProperties.length > 0) { var valueNode = schemaProperties[0].valueNode; if (valueNode && valueNode.type === 'string') { var schemeId = Parser.getNodeValue(valueNode); if (schemeId && Strings.startsWith(schemeId, '.') && this.contextService) { schemeId = this.contextService.resolveRelativePath(schemeId, resource); } if (schemeId) { var id = normalizeId(schemeId); return this.getOrAddSchemaHandle(id).getResolvedSchema(); } } } } if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) { return this.cachedSchemaForResource.resolvedSchema; } var seen = Object.create(null); var schemas = []; var normalizedResource = normalizeResourceForMatching(resource); for (var _i = 0, _a = this.filePatternAssociations; _i < _a.length; _i++) { var entry = _a[_i]; if (entry.matchesPattern(normalizedResource)) { for (var _b = 0, _c = entry.getURIs(); _b < _c.length; _b++) { var schemaId = _c[_b]; if (!seen[schemaId]) { schemas.push(schemaId); seen[schemaId] = true; } } } } var resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined); this.cachedSchemaForResource = { resource: resource, resolvedSchema: resolvedSchema }; return resolvedSchema; }; JSONSchemaService.prototype.createCombinedSchema = function (resource, schemaIds) { if (schemaIds.length === 1) { return this.getOrAddSchemaHandle(schemaIds[0]); } else { var combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(resource); var combinedSchema = { allOf: schemaIds.map(function (schemaId) { return ({ $ref: schemaId }); }) }; return this.addSchemaHandle(combinedSchemaId, combinedSchema); } }; JSONSchemaService.prototype.getMatchingSchemas = function (document, jsonDocument, schema) { if (schema) { var id = schema.id || ('schemaservice://untitled/matchingSchemas/' + idCounter++); return this.resolveSchemaContent(new UnresolvedSchema(schema), id, {}).then(function (resolvedSchema) { return jsonDocument.getMatchingSchemas(resolvedSchema.schema).filter(function (s) { return !s.inverted; }); }); } return this.getSchemaForResource(document.uri, jsonDocument).then(function (schema) { if (schema) { return jsonDocument.getMatchingSchemas(schema.schema).filter(function (s) { return !s.inverted; }); } return []; }); }; return JSONSchemaService; }()); exports.JSONSchemaService = JSONSchemaService; var idCounter = 0; function normalizeId(id) { // remove trailing '#', normalize drive capitalization try { return vscode_uri_1.URI.parse(id).toString(); } catch (e) { return id; } } function normalizeResourceForMatching(resource) { // remove queries and fragments, normalize drive capitalization try { return vscode_uri_1.URI.parse(resource).with({ fragment: null, query: null }).toString(); } catch (e) { return resource; } } function toDisplayString(url) { try { var uri = vscode_uri_1.URI.parse(url); if (uri.scheme === 'file') { return uri.fsPath; } } catch (e) { // ignore } return url; } });