import * as Json from 'jsonc-parser' ; import { JSONSchema , JSONSchemaMap , JSONSchemaRef } from '../jsonSchema' ; import { URI } from 'vscode-uri' ; import * as Strings from '../utils/strings' ; import * as Parser from '../parser/jsonParser' ; import { SchemaRequestService , WorkspaceContextService , PromiseConstructor , Thenable , MatchingSchema , TextDocument } from '../jsonLanguageTypes' ; import * as nls from 'vscode-nls' ; import { createRegex } from '../utils/glob' ; const localize = nls . loadMessageBundle ( ) ; export interface IJSONSchemaService { /**
     * Registers a schema file in the current workspace to be applicable to files that match the pattern
     */ registerExternalSchema ( uri , filePatterns ? , unresolvedSchema ? ) : ISchemaHandle ; /**
     * Clears all cached schema files
     */ clearExternalSchemas ( ) : void ; /**
     * Registers contributed schemas
     */ setSchemaContributions ( schemaContributions ) : void ; /**
     * Looks up the appropriate schema for the given URI
     */ getSchemaForResource ( resource , document ? ) : Thenable < ResolvedSchema | undefined > ; /**
     * Returns all registered schema ids
     */ getRegisteredSchemaIds ( filter ? ) : string [ ] ; } export interface SchemaAssociation { pattern : string [ ] ; uris : string [ ] ; } export interface ISchemaContributions { schemas ? : { [ id ] : JSONSchema ; } ; schemaAssociations ? : SchemaAssociation [ ] ; } export interface ISchemaHandle { /**
     * The schema id
     */ uri : string ; /**
     * The schema from the file, with potential $ref references
     */ getUnresolvedSchema ( ) : Thenable < UnresolvedSchema > ; /**
     * The schema from the file, with references resolved
     */ getResolvedSchema ( ) : Thenable < ResolvedSchema > ; } const BANG = '!' ; const PATH_SEP = '/' ; interface IGlobWrapper { regexp : RegExp ; include : boolean ; } class FilePatternAssociation { private readonly uris : string [ ] ; private readonly globWrappers : IGlobWrapper [ ] ; constructor ( pattern , 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 : createRegex ( '**/' + patternString , { extended : true , globstar : true } ) , include : include , } ) ; } } ; this . uris = uris ; } catch ( e ) { this . globWrappers . length = 0 ; this . uris = [ ] ; } } public matchesPattern ( fileName ) { let match = false ; for ( const { regexp , include } of this . globWrappers ) { if ( regexp . test ( fileName ) ) { match = include ; } } return match ; } public getURIs ( ) { return this . uris ; } } type SchemaDependencies = Set < string > ; class SchemaHandle implements ISchemaHandle { public readonly uri : string ; public readonly dependencies : SchemaDependencies ; public anchors : Map < string , JSONSchema > | undefined ; private resolvedSchema : Thenable < ResolvedSchema > | undefined ; private unresolvedSchema : Thenable < UnresolvedSchema > | undefined ; private readonly service : JSONSchemaService ; 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 ) ) ; } } public getUnresolvedSchema ( ) { if ( ! this . unresolvedSchema ) { this . unresolvedSchema = this . service . loadSchema ( this . uri ) ; } return this . unresolvedSchema ; } public getResolvedSchema ( ) { if ( ! this . resolvedSchema ) { this . resolvedSchema = this . getUnresolvedSchema ( ) . then ( unresolved => { return this . service . resolveSchemaContent ( unresolved , this ) ; } ) ; } return this . resolvedSchema ; } public clearSchema ( ) { const hasChanges = ! ! this . unresolvedSchema ; this . resolvedSchema = undefined ; this . unresolvedSchema = undefined ; this . dependencies . clear ( ) ; this . anchors = undefined ; return hasChanges ; } } export class UnresolvedSchema { public schema : JSONSchema ; public errors : string [ ] ; constructor ( schema , errors = [ ] ) { this . schema = schema ; this . errors = errors ; } } export class ResolvedSchema { public schema : JSONSchema ; public errors : string [ ] ; constructor ( schema , errors = [ ] ) { this . schema = schema ; this . errors = errors ; } public getSection ( path ) { const schemaRef = this . getSectionRecursive ( path , this . schema ) ; if ( schemaRef ) { return Parser . asSchema ( schemaRef ) ; } return undefined ; } private 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 ; } } export class JSONSchemaService implements IJSONSchemaService { private contributionSchemas : { [ id ] : SchemaHandle ; } ; private contributionAssociations : FilePatternAssociation [ ] ; private schemasById : { [ id ] : SchemaHandle ; } ; private filePatternAssociations : FilePatternAssociation [ ] ; private registeredSchemasIds : { [ id ] : boolean ; } ; private contextService : WorkspaceContextService | undefined ; private callOnDispose : Function [ ] ; private requestService : SchemaRequestService | undefined ; private promiseConstructor : PromiseConstructor ; private cachedSchemaForResource : { resource : string ; resolvedSchema : Thenable < ResolvedSchema | undefined > ; } | undefined ; 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 = { } ; } public getRegisteredSchemaIds ( filter ? ) { return Object . keys ( this . registeredSchemasIds ) . filter ( id => { const scheme = URI . parse ( id ) . scheme ; return scheme !== 'schemaservice' && ( ! filter || filter ( scheme ) ) ; } ) ; } public get promise ( ) { return this . promiseConstructor ; } public dispose ( ) { while ( this . callOnDispose . length > 0 ) { this . callOnDispose . pop ( ) ! ( ) ; } } public onResourceChange ( uri ) { this . cachedSchemaForResource = undefined ; let hasChanges = false ; uri = 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 ; } public setSchemaContributions ( schemaContributions ) { if ( schemaContributions . schemas ) { const schemas = schemaContributions . schemas ; for ( const id in schemas ) { const normalizedId = 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 ( normalizeId ) ; const association = this . addFilePatternAssociation ( schemaAssociation . pattern , uris ) ; this . contributionAssociations . push ( association ) ; } } } private addSchemaHandle ( id , unresolvedSchemaContent ? ) { const schemaHandle = new SchemaHandle ( this , id , unresolvedSchemaContent ) ; this . schemasById [ id ] = schemaHandle ; return schemaHandle ; } private getOrAddSchemaHandle ( id , unresolvedSchemaContent ? ) { return this . schemasById [ id ] || this . addSchemaHandle ( id , unresolvedSchemaContent ) ; } private addFilePatternAssociation ( pattern , uris ) { const fpa = new FilePatternAssociation ( pattern , uris ) ; this . filePatternAssociations . push ( fpa ) ; return fpa ; } public registerExternalSchema ( uri , filePatterns ? , unresolvedSchemaContent ? ) { const id = normalizeId ( uri ) ; this . registeredSchemasIds [ id ] = true ; this . cachedSchemaForResource = undefined ; if ( filePatterns ) { this . addFilePatternAssociation ( filePatterns , [ id ] ) ; } return unresolvedSchemaContent ? this . addSchemaHandle ( id , unresolvedSchemaContent ) : this . getOrAddSchemaHandle ( id ) ; } public 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 ) ; } } public getResolvedSchema ( schemaId ) { const id = normalizeId ( schemaId ) ; const schemaHandle = this . schemasById [ id ] ; if ( schemaHandle ) { return schemaHandle . getResolvedSchema ( ) ; } return this . promise . resolve ( undefined ) ; } public loadSchema ( url ) { if ( ! this . requestService ) { const errorMessage = localize ( 'json.schema.norequestservice' , 'Unable to load schema from \'{0}\'. No schema request service available' , toDisplayString ( url ) ) ; return this . promise . resolve ( new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ) ; } return this . requestService ( url ) . then ( content => { if ( ! content ) { const errorMessage = localize ( 'json.schema.nocontent' , 'Unable to load schema from \'{0}\': No content.' , toDisplayString ( url ) ) ; return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ; } let schemaContent = { } ; const jsonErrors = [ ] ; schemaContent = Json . parse ( content , jsonErrors ) ; const 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 ) ; } , ( error ) => { let errorMessage = error . toString ( ) as string ; const errorSplit = error . toString ( ) . split ( 'Error: ' ) ; if ( errorSplit . length > 1 ) { errorMessage = errorSplit [ 1 ] ; } if ( Strings . endsWith ( errorMessage , '.' ) ) { errorMessage = errorMessage . substr ( 0 , errorMessage . length - 1 ) ; } return new UnresolvedSchema ( < JSONSchema > { } , [ localize ( 'json.schema.nocontent' , 'Unable to load schema from \'{0}\': {1}.' , toDisplayString ( url ) , errorMessage ) ] ) ; } ) ; } public resolveSchemaContent ( schemaToResolve , handle ) { const resolveErrors = schemaToResolve . errors . slice ( 0 ) ; const schema = schemaToResolve . schema ; if ( schema . $schema ) { const 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." ) ) ; } else if ( id === 'https://json-schema.org/draft/2020-12/schema' ) { resolveErrors . push ( localize ( 'json.schema.draft202012.notsupported' , "Draft 2020-12 schemas are not yet fully supported." ) ) ; } } const contextService = this . contextService ; const findSectionByJSONPointer = ( schema , path ) : any => { 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 ) : void => { for ( const key in section ) { if ( section . hasOwnProperty ( key ) && ! target . hasOwnProperty ( key ) && key !== 'id' && key !== '$id' ) { ( < any > target ) [ key ] = section [ key ] ; } } } ; const mergeRef = ( target , sourceRoot , sourceHandle , refSegment ) : void => { let section ; if ( refSegment === undefined ) { section = sourceRoot ; } else if ( refSegment . charAt ( 0 ) === '/' ) { section = findSectionByJSONPointer ( sourceRoot , refSegment ) ; } else { section = findSchemaById ( sourceRoot , sourceHandle , refSegment ) ; } if ( section ) { merge ( target , section ) ; } else { resolveErrors . push ( localize ( 'json.schema.invalidid' , '$ref \'{0}\' in \'{1}\' can not be resolved.' , refSegment , sourceHandle . uri ) ) ; } } ; const resolveExternalLink = ( node , uri , refSegment , parentHandle ) : Thenable < any > => { if ( contextService && ! /^[A-Za-z][A-Za-z0-9+\-.+]*:\/\/.*/ . test ( uri ) ) { uri = contextService . resolveRelativePath ( uri , parentHandle . uri ) ; } uri = normalizeId ( uri ) ; const referencedHandle = this . getOrAddSchemaHandle ( uri ) ; return referencedHandle . getUnresolvedSchema ( ) . then ( unresolvedSchema => { parentHandle . dependencies . add ( uri ) ; if ( unresolvedSchema . errors . length ) { const loc = refSegment ? uri + '#' + refSegment : uri ; resolveErrors . push ( localize ( 'json.schema.problemloadingref' , 'Problems loading reference \'{0}\': {1}' , loc , unresolvedSchema . errors [ 0 ] ) ) ; } mergeRef ( node , unresolvedSchema . schema , referencedHandle , refSegment ) ; return resolveRefs ( node , unresolvedSchema . schema , referencedHandle ) ; } ) ; } ; const resolveRefs = ( node , parentSchema , parentHandle ) : Thenable < any > => { const openPromises = [ ] ; this . traverseNodes ( node , next => { const seenRefs = new Set < string > ( ) ; while ( next . $ref ) { const ref = next . $ref ; const segments = ref . split ( '#' , 2 ) ; delete next . $ref ; if ( segments [ 0 ] . length > 0 ) { openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentHandle ) ) ; return ; } else { if ( ! seenRefs . has ( ref ) ) { const id = segments [ 1 ] ; mergeRef ( next , parentSchema , parentHandle , id ) ; seenRefs . add ( ref ) ; } } } } ) ; return this . promise . all ( openPromises ) ; } ; const collectAnchors = ( root ) : Map < string , JSONSchema > => { const result = new Map < string , JSONSchema > ( ) ; this . traverseNodes ( root , next => { const id = next . $id || next . id ; if ( typeof id === 'string' && id . charAt ( 0 ) === '#' ) { const anchor = id . substring ( 1 ) ; if ( result . has ( anchor ) ) { resolveErrors . push ( localize ( 'json.schema.duplicateid' , 'Duplicate id declaration: \'{0}\'' , id ) ) ; } else { result . set ( anchor , next ) ; } } } ) ; return result ; } ; return resolveRefs ( schema , schema , handle ) . then ( _ => { return new ResolvedSchema ( schema , resolveErrors ) ; } ) ; } private traverseNodes ( root , handle ) { if ( ! root || typeof root !== 'object' ) { return Promise . resolve ( null ) ; } const seen = new Set < JSONSchema > ( ) ; const collectEntries = ( ... entries ) => { for ( const entry of entries ) { if ( typeof entry === 'object' ) { toWalk . push ( entry ) ; } } } ; const collectMapEntries = ( ... maps ) => { for ( const map of maps ) { if ( typeof map === 'object' ) { for ( const k in map ) { const key = k as keyof JSONSchemaMap ; const entry = map [ key ] ; if ( typeof entry === 'object' ) { toWalk . push ( entry ) ; } } } } } ; const collectArrayEntries = ( ... arrays ) => { for ( const array of arrays ) { if ( Array . isArray ( array ) ) { for ( const entry of array ) { if ( typeof entry === 'object' ) { toWalk . push ( entry ) ; } } } } } ; const toWalk = [ root ] ; let next = toWalk . pop ( ) ; while ( next ) { if ( ! seen . has ( next ) ) { seen . add ( next ) ; handle ( next ) ; collectEntries ( < JSONSchema > next . items , next . additionalItems , < JSONSchema > next . additionalProperties , next . not , next . contains , next . propertyNames , next . if , next . then , next . else ) ; collectMapEntries ( next . definitions , next . properties , next . patternProperties , < JSONSchemaMap > next . dependencies ) ; collectArrayEntries ( next . anyOf , next . allOf , next . oneOf , < JSONSchema [ ] > next . items ) ; } next = toWalk . pop ( ) ; } } ; private 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 ) ) { schemaId = this . contextService . resolveRelativePath ( schemaId , resource ) ; } return schemaId ; } } } return undefined ; } private 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 ; } public getSchemaURIsForResource ( resource , document ? ) { let schemeId = document && this . getSchemaFromProperty ( resource , document ) ; if ( schemeId ) { return [ schemeId ] ; } return this . getAssociatedSchemas ( resource ) ; } public getSchemaForResource ( resource , document ? ) { if ( document ) { let schemeId = this . getSchemaFromProperty ( resource , document ) ; if ( schemeId ) { const id = 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 ; } private 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 ) ; } } public 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 [ ] ; } ) ; } } let idCounter = 0 ; function normalizeId ( id ) { try { return URI . parse ( id ) . toString ( ) ; } catch ( e ) { return id ; } } function normalizeResourceForMatching ( resource ) { try { return URI . parse ( resource ) . with ( { fragment : null , query : null } ) . toString ( ) ; } catch ( e ) { return resource ; } } function toDisplayString ( url ) { try { const uri = URI . parse ( url ) ; if ( uri . scheme === 'file' ) { return uri . fsPath ; } } catch ( e ) { } return url ; }