UNPKG

vscode-json-languageservice

Version:
632 lines (631 loc) 29.5 kB
/*--------------------------------------------------------------------------------------------- * 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", "../jsonLanguageTypes", "@vscode/l10n", "../utils/glob", "../utils/objects", "vscode-languageserver-types"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JSONSchemaService = exports.ResolvedSchema = exports.UnresolvedSchema = void 0; const Json = require("jsonc-parser"); const vscode_uri_1 = require("vscode-uri"); const Strings = require("../utils/strings"); const jsonParser_1 = require("../parser/jsonParser"); const jsonLanguageTypes_1 = require("../jsonLanguageTypes"); const l10n = require("@vscode/l10n"); const glob_1 = require("../utils/glob"); const objects_1 = require("../utils/objects"); const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const BANG = '!'; const PATH_SEP = '/'; class FilePatternAssociation { constructor(pattern, folderUri, uris) { this.folderUri = folderUri; this.uris = uris; this.globWrappers = []; try { for (let patternString of pattern) { const 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: (0, glob_1.createRegex)('**/' + patternString, { extended: true, globstar: true }), include: include, }); } } ; if (folderUri) { folderUri = normalizeResourceForMatching(folderUri); if (!folderUri.endsWith('/')) { folderUri = folderUri + '/'; } this.folderUri = folderUri; } } catch (e) { this.globWrappers.length = 0; this.uris = []; } } matchesPattern(fileName) { if (this.folderUri && !fileName.startsWith(this.folderUri)) { return false; } let match = false; for (const { regexp, include } of this.globWrappers) { if (regexp.test(fileName)) { match = include; } } return match; } getURIs() { return this.uris; } } class SchemaHandle { constructor(service, uri, unresolvedSchemaContent) { this.service = service; this.uri = uri; this.dependencies = new Set(); this.anchors = undefined; if (unresolvedSchemaContent) { this.unresolvedSchema = this.service.promise.resolve(new UnresolvedSchema(unresolvedSchemaContent)); } } getUnresolvedSchema() { if (!this.unresolvedSchema) { this.unresolvedSchema = this.service.loadSchema(this.uri); } return this.unresolvedSchema; } getResolvedSchema() { if (!this.resolvedSchema) { this.resolvedSchema = this.getUnresolvedSchema().then(unresolved => { return this.service.resolveSchemaContent(unresolved, this); }); } return this.resolvedSchema; } clearSchema() { const hasChanges = !!this.unresolvedSchema; this.resolvedSchema = undefined; this.unresolvedSchema = undefined; this.dependencies.clear(); this.anchors = undefined; return hasChanges; } } class UnresolvedSchema { constructor(schema, errors = []) { this.schema = schema; this.errors = errors; } } exports.UnresolvedSchema = UnresolvedSchema; function toDiagnostic(message, code, relatedURL) { const relatedInformation = relatedURL ? [{ location: { uri: relatedURL, range: vscode_languageserver_types_1.Range.create(0, 0, 0, 0) }, message }] : undefined; return { message, code, relatedInformation }; } class ResolvedSchema { constructor(schema, errors = [], warnings = [], schemaDraft) { this.schema = schema; this.errors = errors; this.warnings = warnings; this.schemaDraft = schemaDraft; } getSection(path) { const schemaRef = this.getSectionRecursive(path, this.schema); if (schemaRef) { return (0, jsonParser_1.asSchema)(schemaRef); } return undefined; } getSectionRecursive(path, schema) { if (!schema || typeof schema === 'boolean' || path.length === 0) { return schema; } const next = path.shift(); if (schema.properties && typeof schema.properties[next]) { return this.getSectionRecursive(path, schema.properties[next]); } else if (schema.patternProperties) { for (const pattern of Object.keys(schema.patternProperties)) { const 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)) { const 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; } } exports.ResolvedSchema = ResolvedSchema; class JSONSchemaService { constructor(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 = {}; } getRegisteredSchemaIds(filter) { return Object.keys(this.registeredSchemasIds).filter(id => { const scheme = vscode_uri_1.URI.parse(id).scheme; return scheme !== 'schemaservice' && (!filter || filter(scheme)); }); } get promise() { return this.promiseConstructor; } dispose() { while (this.callOnDispose.length > 0) { this.callOnDispose.pop()(); } } onResourceChange(uri) { // always clear this local cache when a resource changes this.cachedSchemaForResource = undefined; let hasChanges = false; uri = (0, jsonParser_1.normalizeId)(uri); const toWalk = [uri]; const all = Object.keys(this.schemasById).map(key => this.schemasById[key]); while (toWalk.length) { const curr = toWalk.pop(); for (let i = 0; i < all.length; i++) { const handle = all[i]; if (handle && (handle.uri === curr || handle.dependencies.has(curr))) { if (handle.uri !== curr) { toWalk.push(handle.uri); } if (handle.clearSchema()) { hasChanges = true; } all[i] = undefined; } } } return hasChanges; } setSchemaContributions(schemaContributions) { if (schemaContributions.schemas) { const schemas = schemaContributions.schemas; for (const id in schemas) { const normalizedId = (0, jsonParser_1.normalizeId)(id); this.contributionSchemas[normalizedId] = this.addSchemaHandle(normalizedId, schemas[id]); } } if (Array.isArray(schemaContributions.schemaAssociations)) { const schemaAssociations = schemaContributions.schemaAssociations; for (let schemaAssociation of schemaAssociations) { const uris = schemaAssociation.uris.map(jsonParser_1.normalizeId); const association = this.addFilePatternAssociation(schemaAssociation.pattern, schemaAssociation.folderUri, uris); this.contributionAssociations.push(association); } } } addSchemaHandle(id, unresolvedSchemaContent) { const schemaHandle = new SchemaHandle(this, id, unresolvedSchemaContent); this.schemasById[id] = schemaHandle; return schemaHandle; } getOrAddSchemaHandle(id, unresolvedSchemaContent) { return this.schemasById[id] || this.addSchemaHandle(id, unresolvedSchemaContent); } addFilePatternAssociation(pattern, folderUri, uris) { const fpa = new FilePatternAssociation(pattern, folderUri, uris); this.filePatternAssociations.push(fpa); return fpa; } registerExternalSchema(config) { const id = (0, jsonParser_1.normalizeId)(config.uri); this.registeredSchemasIds[id] = true; this.cachedSchemaForResource = undefined; if (config.fileMatch && config.fileMatch.length) { this.addFilePatternAssociation(config.fileMatch, config.folderUri, [id]); } return config.schema ? this.addSchemaHandle(id, config.schema) : this.getOrAddSchemaHandle(id); } clearExternalSchemas() { this.schemasById = {}; this.filePatternAssociations = []; this.registeredSchemasIds = {}; this.cachedSchemaForResource = undefined; for (const id in this.contributionSchemas) { this.schemasById[id] = this.contributionSchemas[id]; this.registeredSchemasIds[id] = true; } for (const contributionAssociation of this.contributionAssociations) { this.filePatternAssociations.push(contributionAssociation); } } getResolvedSchema(schemaId) { const id = (0, jsonParser_1.normalizeId)(schemaId); const schemaHandle = this.schemasById[id]; if (schemaHandle) { return schemaHandle.getResolvedSchema(); } return this.promise.resolve(undefined); } loadSchema(url) { if (!this.requestService) { const errorMessage = l10n.t('Unable to load schema from \'{0}\'. No schema request service available', toDisplayString(url)); return this.promise.resolve(new UnresolvedSchema({}, [toDiagnostic(errorMessage, jsonLanguageTypes_1.ErrorCode.SchemaResolveError, url)])); } return this.requestService(url).then(content => { if (!content) { const errorMessage = l10n.t('Unable to load schema from \'{0}\': No content.', toDisplayString(url)); return new UnresolvedSchema({}, [toDiagnostic(errorMessage, jsonLanguageTypes_1.ErrorCode.SchemaResolveError, url)]); } const errors = []; if (content.charCodeAt(0) === 65279) { errors.push(toDiagnostic(l10n.t('Problem reading content from \'{0}\': UTF-8 with BOM detected, only UTF 8 is allowed.', toDisplayString(url)), jsonLanguageTypes_1.ErrorCode.SchemaResolveError, url)); content = content.trimStart(); } let schemaContent = {}; const jsonErrors = []; schemaContent = Json.parse(content, jsonErrors); if (jsonErrors.length) { errors.push(toDiagnostic(l10n.t('Unable to parse content from \'{0}\': Parse error at offset {1}.', toDisplayString(url), jsonErrors[0].offset), jsonLanguageTypes_1.ErrorCode.SchemaResolveError, url)); } return new UnresolvedSchema(schemaContent, errors); }, (error) => { let { message, code } = error; if (typeof message !== 'string') { let errorMessage = error.toString(); const 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); } message = errorMessage; } let errorCode = jsonLanguageTypes_1.ErrorCode.SchemaResolveError; if (typeof code === 'number' && code < 0x10000) { errorCode += code; } const errorMessage = l10n.t('Unable to load schema from \'{0}\': {1}.', toDisplayString(url), message); return new UnresolvedSchema({}, [toDiagnostic(errorMessage, errorCode, url)]); }); } resolveSchemaContent(schemaToResolve, handle) { const resolveErrors = schemaToResolve.errors.slice(0); const schema = schemaToResolve.schema; const schemaDraft = schema.$schema ? (0, jsonParser_1.getSchemaDraftFromId)(schema.$schema) : undefined; if (schemaDraft === jsonLanguageTypes_1.SchemaDraft.v3) { return this.promise.resolve(new ResolvedSchema({}, [toDiagnostic(l10n.t("Draft-03 schemas are not supported."), jsonLanguageTypes_1.ErrorCode.SchemaUnsupportedFeature)], [], schemaDraft)); } let usesUnsupportedFeatures = new Set(); const contextService = this.contextService; const findSectionByJSONPointer = (schema, path) => { path = decodeURIComponent(path); let current = schema; if (path[0] === '/') { path = path.substring(1); } path.split('/').some((part) => { part = part.replace(/~1/g, '/').replace(/~0/g, '~'); current = current[part]; return !current; }); return current; }; const findSchemaById = (schema, handle, id) => { if (!handle.anchors) { handle.anchors = collectAnchors(schema); } return handle.anchors.get(id); }; const merge = (target, section) => { for (const key in section) { if (section.hasOwnProperty(key) && key !== 'id' && key !== '$id') { target[key] = section[key]; } } }; const mergeRef = (target, sourceRoot, sourceHandle, refSegment) => { let section; if (refSegment === undefined || refSegment.length === 0) { section = sourceRoot; } else if (refSegment.charAt(0) === '/') { // A $ref to a JSON Pointer (i.e #/definitions/foo) section = findSectionByJSONPointer(sourceRoot, refSegment); } else { // A $ref to a sub-schema with an $id (i.e #hello) section = findSchemaById(sourceRoot, sourceHandle, refSegment); } if (section) { merge(target, section); } else { const message = l10n.t('$ref \'{0}\' in \'{1}\' can not be resolved.', refSegment || '', sourceHandle.uri); resolveErrors.push(toDiagnostic(message, jsonLanguageTypes_1.ErrorCode.SchemaResolveError)); } }; const resolveExternalLink = (node, uri, refSegment, parentHandle) => { if (contextService && !/^[A-Za-z][A-Za-z0-9+\-.+]*:\/.*/.test(uri)) { uri = contextService.resolveRelativePath(uri, parentHandle.uri); } uri = (0, jsonParser_1.normalizeId)(uri); const referencedHandle = this.getOrAddSchemaHandle(uri); return referencedHandle.getUnresolvedSchema().then(unresolvedSchema => { parentHandle.dependencies.add(uri); if (unresolvedSchema.errors.length) { const error = unresolvedSchema.errors[0]; const loc = refSegment ? uri + '#' + refSegment : uri; const errorMessage = refSegment ? l10n.t('Problems loading reference \'{0}\': {1}', refSegment, error.message) : error.message; resolveErrors.push(toDiagnostic(errorMessage, error.code, uri)); } mergeRef(node, unresolvedSchema.schema, referencedHandle, refSegment); return resolveRefs(node, unresolvedSchema.schema, referencedHandle); }); }; const resolveRefs = (node, parentSchema, parentHandle) => { const openPromises = []; this.traverseNodes(node, next => { const seenRefs = new Set(); while (next.$ref) { const ref = next.$ref; const segments = ref.split('#', 2); delete next.$ref; if (segments[0].length > 0) { // This is a reference to an external schema openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentHandle)); return; } else { // This is a reference inside the current schema if (!seenRefs.has(ref)) { const id = segments[1]; mergeRef(next, parentSchema, parentHandle, id); seenRefs.add(ref); } } } if (next.$recursiveRef) { usesUnsupportedFeatures.add('$recursiveRef'); } if (next.$dynamicRef) { usesUnsupportedFeatures.add('$dynamicRef'); } }); return this.promise.all(openPromises); }; const collectAnchors = (root) => { const result = new Map(); this.traverseNodes(root, next => { const id = next.$id || next.id; const anchor = (0, objects_1.isString)(id) && id.charAt(0) === '#' ? id.substring(1) : next.$anchor; if (anchor) { if (result.has(anchor)) { resolveErrors.push(toDiagnostic(l10n.t('Duplicate anchor declaration: \'{0}\'', anchor), jsonLanguageTypes_1.ErrorCode.SchemaResolveError)); } else { result.set(anchor, next); } } if (next.$recursiveAnchor) { usesUnsupportedFeatures.add('$recursiveAnchor'); } if (next.$dynamicAnchor) { usesUnsupportedFeatures.add('$dynamicAnchor'); } }); return result; }; return resolveRefs(schema, schema, handle).then(_ => { let resolveWarnings = []; if (usesUnsupportedFeatures.size) { resolveWarnings.push(toDiagnostic(l10n.t('The schema uses meta-schema features ({0}) that are not yet supported by the validator.', Array.from(usesUnsupportedFeatures.keys()).join(', ')), jsonLanguageTypes_1.ErrorCode.SchemaUnsupportedFeature)); } return new ResolvedSchema(schema, resolveErrors, resolveWarnings, schemaDraft); }); } traverseNodes(root, handle) { if (!root || typeof root !== 'object') { return Promise.resolve(null); } const seen = new Set(); const collectEntries = (...entries) => { for (const entry of entries) { if ((0, objects_1.isObject)(entry)) { toWalk.push(entry); } } }; const collectMapEntries = (...maps) => { for (const map of maps) { if ((0, objects_1.isObject)(map)) { for (const k in map) { const key = k; const entry = map[key]; if ((0, objects_1.isObject)(entry)) { toWalk.push(entry); } } } } }; const collectArrayEntries = (...arrays) => { for (const array of arrays) { if (Array.isArray(array)) { for (const entry of array) { if ((0, objects_1.isObject)(entry)) { toWalk.push(entry); } } } } }; const collectEntryOrArrayEntries = (items) => { if (Array.isArray(items)) { for (const entry of items) { if ((0, objects_1.isObject)(entry)) { toWalk.push(entry); } } } else if ((0, objects_1.isObject)(items)) { toWalk.push(items); } }; const toWalk = [root]; let next = toWalk.pop(); while (next) { if (!seen.has(next)) { seen.add(next); handle(next); collectEntries(next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else, next.unevaluatedItems, next.unevaluatedProperties); collectMapEntries(next.definitions, next.$defs, next.properties, next.patternProperties, next.dependencies, next.dependentSchemas); collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems); collectEntryOrArrayEntries(next.items); } next = toWalk.pop(); } } ; getSchemaFromProperty(resource, document) { if (document.root?.type === 'object') { for (const p of document.root.properties) { if (p.keyNode.value === '$schema' && p.valueNode?.type === 'string') { let schemaId = p.valueNode.value; if (this.contextService && !/^\w[\w\d+.-]*:/.test(schemaId)) { // has scheme schemaId = this.contextService.resolveRelativePath(schemaId, resource); } return schemaId; } } } return undefined; } getAssociatedSchemas(resource) { const seen = Object.create(null); const schemas = []; const normalizedResource = normalizeResourceForMatching(resource); for (const entry of this.filePatternAssociations) { if (entry.matchesPattern(normalizedResource)) { for (const schemaId of entry.getURIs()) { if (!seen[schemaId]) { schemas.push(schemaId); seen[schemaId] = true; } } } } return schemas; } getSchemaURIsForResource(resource, document) { let schemeId = document && this.getSchemaFromProperty(resource, document); if (schemeId) { return [schemeId]; } return this.getAssociatedSchemas(resource); } getSchemaForResource(resource, document) { if (document) { // first use $schema if present let schemeId = this.getSchemaFromProperty(resource, document); if (schemeId) { const id = (0, jsonParser_1.normalizeId)(schemeId); return this.getOrAddSchemaHandle(id).getResolvedSchema(); } } if (this.cachedSchemaForResource && this.cachedSchemaForResource.resource === resource) { return this.cachedSchemaForResource.resolvedSchema; } const schemas = this.getAssociatedSchemas(resource); const resolvedSchema = schemas.length > 0 ? this.createCombinedSchema(resource, schemas).getResolvedSchema() : this.promise.resolve(undefined); this.cachedSchemaForResource = { resource, resolvedSchema }; return resolvedSchema; } createCombinedSchema(resource, schemaIds) { if (schemaIds.length === 1) { return this.getOrAddSchemaHandle(schemaIds[0]); } else { const combinedSchemaId = 'schemaservice://combinedSchema/' + encodeURIComponent(resource); const combinedSchema = { allOf: schemaIds.map(schemaId => ({ $ref: schemaId })) }; return this.addSchemaHandle(combinedSchemaId, combinedSchema); } } getMatchingSchemas(document, jsonDocument, schema) { if (schema) { const id = schema.id || ('schemaservice://untitled/matchingSchemas/' + idCounter++); const handle = this.addSchemaHandle(id, schema); return handle.getResolvedSchema().then(resolvedSchema => { return jsonDocument.getMatchingSchemas(resolvedSchema.schema).filter(s => !s.inverted); }); } return this.getSchemaForResource(document.uri, jsonDocument).then(schema => { if (schema) { return jsonDocument.getMatchingSchemas(schema.schema).filter(s => !s.inverted); } return []; }); } } exports.JSONSchemaService = JSONSchemaService; let idCounter = 0; function normalizeResourceForMatching(resource) { // remove queries and fragments, normalize drive capitalization try { return vscode_uri_1.URI.parse(resource).with({ fragment: null, query: null }).toString(true); } catch (e) { return resource; } } function toDisplayString(url) { try { const uri = vscode_uri_1.URI.parse(url); if (uri.scheme === 'file') { return uri.fsPath; } } catch (e) { // ignore } return url; } });