@@ -129,7 +129,7 @@ class SchemaHandle implements ISchemaHandle {
129
129
130
130
public readonly uri : string ;
131
131
public readonly dependencies : SchemaDependencies ;
132
-
132
+ public readonly anchors : Map < string , JSONSchema > ;
133
133
private resolvedSchema : Thenable < ResolvedSchema > | undefined ;
134
134
private unresolvedSchema : Thenable < UnresolvedSchema > | undefined ;
135
135
private readonly service : JSONSchemaService ;
@@ -138,6 +138,7 @@ class SchemaHandle implements ISchemaHandle {
138
138
this . service = service ;
139
139
this . uri = uri ;
140
140
this . dependencies = new Set ( ) ;
141
+ this . anchors = new Map ( ) ;
141
142
if ( unresolvedSchemaContent ) {
142
143
this . unresolvedSchema = this . service . promise . resolve ( new UnresolvedSchema ( unresolvedSchemaContent ) ) ;
143
144
}
@@ -153,7 +154,7 @@ class SchemaHandle implements ISchemaHandle {
153
154
public getResolvedSchema ( ) : Thenable < ResolvedSchema > {
154
155
if ( ! this . resolvedSchema ) {
155
156
this . resolvedSchema = this . getUnresolvedSchema ( ) . then ( unresolved => {
156
- return this . service . resolveSchemaContent ( unresolved , this . uri , this . dependencies ) ;
157
+ return this . service . resolveSchemaContent ( unresolved , this ) ;
157
158
} ) ;
158
159
}
159
160
return this . resolvedSchema ;
@@ -164,6 +165,7 @@ class SchemaHandle implements ISchemaHandle {
164
165
this . resolvedSchema = undefined ;
165
166
this . unresolvedSchema = undefined ;
166
167
this . dependencies . clear ( ) ;
168
+ this . anchors . clear ( ) ;
167
169
return hasChanges ;
168
170
}
169
171
}
@@ -404,7 +406,7 @@ export class JSONSchemaService implements IJSONSchemaService {
404
406
) ;
405
407
}
406
408
407
- public resolveSchemaContent ( schemaToResolve : UnresolvedSchema , schemaURL : string , dependencies : SchemaDependencies ) : Thenable < ResolvedSchema > {
409
+ public resolveSchemaContent ( schemaToResolve : UnresolvedSchema , handle : SchemaHandle ) : Thenable < ResolvedSchema > {
408
410
409
411
const resolveErrors : string [ ] = schemaToResolve . errors . slice ( 0 ) ;
410
412
const schema = schemaToResolve . schema ;
@@ -438,38 +440,91 @@ export class JSONSchemaService implements IJSONSchemaService {
438
440
return current ;
439
441
} ;
440
442
441
- const merge = ( target : JSONSchema , sourceRoot : JSONSchema , sourceURI : string , refSegment : string | undefined ) : void => {
443
+ const merge = ( target : JSONSchema , section : any ) : void => {
444
+ for ( const key in section ) {
445
+ if ( section . hasOwnProperty ( key ) && ! target . hasOwnProperty ( key ) ) {
446
+ ( < any > target ) [ key ] = section [ key ] ;
447
+ }
448
+ }
449
+ } ;
450
+
451
+ const mergeByJsonPointer = ( target : JSONSchema , sourceRoot : JSONSchema , sourceURI : string , refSegment : string | undefined ) : void => {
442
452
const path = refSegment ? decodeURIComponent ( refSegment ) : undefined ;
443
453
const section = findSection ( sourceRoot , path ) ;
444
454
if ( section ) {
445
- for ( const key in section ) {
446
- if ( section . hasOwnProperty ( key ) && ! target . hasOwnProperty ( key ) ) {
447
- ( < any > target ) [ key ] = section [ key ] ;
448
- }
449
- }
455
+ merge ( target , section ) ;
450
456
} else {
451
457
resolveErrors . push ( localize ( 'json.schema.invalidref' , '$ref \'{0}\' in \'{1}\' can not be resolved.' , path , sourceURI ) ) ;
452
458
}
453
459
} ;
454
460
455
- const resolveExternalLink = ( node : JSONSchema , uri : string , refSegment : string | undefined , parentSchemaURL : string , parentSchemaDependencies : SchemaDependencies ) : Thenable < any > => {
461
+ const isSubSchemaRef = ( refSegment ?: string ) : boolean => {
462
+ // Check if the first character is not '/' to determine whether it's a sub schema reference or a JSON Pointer
463
+ return ! ! refSegment && refSegment . charAt ( 0 ) !== '/' ;
464
+ } ;
465
+
466
+ const reconstructRefURI = ( uri : string , fragment ?: string , separator : string = '#' ) : string => {
467
+ return normalizeId ( `${ uri } ${ separator } ${ fragment } ` ) ;
468
+ } ;
469
+
470
+ // To find which $refs point to which $ids we keep two maps:
471
+ // pendingSubSchemas '$id' we expect to encounter (if they exist)
472
+ // handle.anchors for the ones we have encountered
473
+ const pendingSubSchemas : Map < string , JSONSchema [ ] > = new Map ( ) ;
474
+
475
+ const tryMergeSubSchema = ( target : JSONSchema , id : string , handle : SchemaHandle ) : boolean => {
476
+ // Get the full URI for the current schema to avoid matching schema1#hello and schema2#hello to the same
477
+ // reference by accident
478
+ const fullId = reconstructRefURI ( handle . uri , id ) ;
479
+ const resolved = handle . anchors . get ( fullId ) ;
480
+ if ( resolved ) {
481
+ merge ( target , resolved ) ;
482
+ return true ; // return success
483
+ }
484
+
485
+ // This subschema has not been resolved yet
486
+ // Remember the target to merge later once resolved
487
+ let pending = pendingSubSchemas . get ( fullId ) ;
488
+ if ( ! pending ) {
489
+ pending = [ ] ;
490
+ pendingSubSchemas . set ( fullId , pending ) ;
491
+ }
492
+ pending . push ( target ) ;
493
+ return false ; // return failure - merge didn't occur
494
+ } ;
495
+
496
+ const resolveExternalLink = ( node : JSONSchema , uri : string , refSegment : string | undefined , parentHandle : SchemaHandle ) : Thenable < any > => {
456
497
if ( contextService && ! / ^ [ A - Z a - z ] [ A - Z a - z 0 - 9 + \- . + ] * : \/ \/ .* / . test ( uri ) ) {
457
- uri = contextService . resolveRelativePath ( uri , parentSchemaURL ) ;
498
+ uri = contextService . resolveRelativePath ( uri , parentHandle . uri ) ;
458
499
}
459
500
uri = normalizeId ( uri ) ;
460
501
const referencedHandle = this . getOrAddSchemaHandle ( uri ) ;
461
502
return referencedHandle . getUnresolvedSchema ( ) . then ( unresolvedSchema => {
462
- parentSchemaDependencies . add ( uri ) ;
503
+ parentHandle . dependencies . add ( uri ) ;
463
504
if ( unresolvedSchema . errors . length ) {
464
505
const loc = refSegment ? uri + '#' + refSegment : uri ;
465
506
resolveErrors . push ( localize ( 'json.schema.problemloadingref' , 'Problems loading reference \'{0}\': {1}' , loc , unresolvedSchema . errors [ 0 ] ) ) ;
466
507
}
467
- merge ( node , unresolvedSchema . schema , uri , refSegment ) ;
468
- return resolveRefs ( node , unresolvedSchema . schema , uri , referencedHandle . dependencies ) ;
508
+
509
+ // A placeholder promise that might execute later a ref resolution for the newly resolved schema
510
+ let externalLinkPromise : Thenable < any > = Promise . resolve ( true ) ;
511
+ if ( refSegment === undefined || ! isSubSchemaRef ( refSegment ) ) {
512
+ // This is not a sub schema, merge the regular way
513
+ mergeByJsonPointer ( node , unresolvedSchema . schema , uri , refSegment ) ;
514
+ } else {
515
+ // This is a reference to a subschema
516
+ if ( ! tryMergeSubSchema ( node , refSegment , referencedHandle ) ) {
517
+ // We weren't able to merge currently so we'll try to resolve this schema first to obtain subschemas
518
+ // that could be missed
519
+ // to improve: it would be enough to find the nodes, no need to resolve the full schema
520
+ externalLinkPromise = resolveRefs ( unresolvedSchema . schema , unresolvedSchema . schema , referencedHandle ) ;
521
+ }
522
+ }
523
+ return externalLinkPromise . then ( ( ) => resolveRefs ( node , unresolvedSchema . schema , referencedHandle ) ) ;
469
524
} ) ;
470
525
} ;
471
526
472
- const resolveRefs = ( node : JSONSchema , parentSchema : JSONSchema , parentSchemaURL : string , parentSchemaDependencies : SchemaDependencies ) : Thenable < any > => {
527
+ const resolveRefs = ( node : JSONSchema , parentSchema : JSONSchema , parentHandle : SchemaHandle ) : Thenable < any > => {
473
528
if ( ! node || typeof node !== 'object' ) {
474
529
return Promise . resolve ( null ) ;
475
530
}
@@ -517,11 +572,18 @@ export class JSONSchemaService implements IJSONSchemaService {
517
572
const segments = ref . split ( '#' , 2 ) ;
518
573
delete next . $ref ;
519
574
if ( segments [ 0 ] . length > 0 ) {
520
- openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentSchemaURL , parentSchemaDependencies ) ) ;
575
+ // This is a reference to an external schema
576
+ openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentHandle ) ) ;
521
577
return ;
522
578
} else {
579
+ // This is a reference inside the current schema
523
580
if ( ! seenRefs . has ( ref ) ) {
524
- merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ; // can set next.$ref again, use seenRefs to avoid circle
581
+ const id = segments [ 1 ] ;
582
+ if ( id !== undefined && isSubSchemaRef ( id ) ) { // A $ref to a sub-schema with an $id (i.e #hello)
583
+ tryMergeSubSchema ( next , id , handle ) ;
584
+ } else { // A $ref to a JSON Pointer (i.e #/definitions/foo)
585
+ mergeByJsonPointer ( next , parentSchema , parentHandle . uri , id ) ; // can set next.$ref again, use seenRefs to avoid circle
586
+ }
525
587
seenRefs . add ( ref ) ;
526
588
}
527
589
}
@@ -532,18 +594,54 @@ export class JSONSchemaService implements IJSONSchemaService {
532
594
collectArrayEntries ( next . anyOf , next . allOf , next . oneOf , < JSONSchema [ ] > next . items ) ;
533
595
} ;
534
596
597
+ const handleId = ( next : JSONSchema ) => {
598
+ // TODO figure out should loops be preventse
599
+ const id = next . $id || next . id ;
600
+ if ( typeof id === 'string' && id . charAt ( 0 ) === '#' ) {
601
+ delete next . $id ;
602
+ delete next . id ;
603
+ // Use a blank separator, as the $id already has the '#'
604
+ const fullId = reconstructRefURI ( parentHandle . uri , id , '' ) ;
605
+
606
+ const resolved = parentHandle . anchors . get ( fullId ) ;
607
+ if ( ! resolved ) {
608
+ // it's resolved now
609
+ parentHandle . anchors . set ( fullId , next ) ;
610
+ } else if ( resolved !== next ) {
611
+ // Duplicate may occur in recursive $refs, but as long as they are the same object
612
+ // it's ok, otherwise report and error
613
+ resolveErrors . push ( localize ( 'json.schema.duplicateid' , 'Duplicate id declaration: \'{0}\'' , id ) ) ;
614
+ }
615
+
616
+ // Resolve all pending requests and cleanup the queue list
617
+ const pending = pendingSubSchemas . get ( fullId ) ;
618
+ if ( pending ) {
619
+ for ( const target of pending ) {
620
+ merge ( target , next ) ;
621
+ }
622
+ pendingSubSchemas . delete ( fullId ) ;
623
+ }
624
+ }
625
+ } ;
626
+
535
627
while ( toWalk . length ) {
536
628
const next = toWalk . pop ( ) ! ;
537
629
if ( seen . has ( next ) ) {
538
630
continue ;
539
631
}
540
632
seen . add ( next ) ;
633
+ handleId ( next ) ;
541
634
handleRef ( next ) ;
542
635
}
543
636
return this . promise . all ( openPromises ) ;
544
637
} ;
545
638
546
- return resolveRefs ( schema , schema , schemaURL , dependencies ) . then ( _ => new ResolvedSchema ( schema , resolveErrors ) ) ;
639
+ return resolveRefs ( schema , schema , handle ) . then ( _ => {
640
+ for ( const unresolvedSubschemaId in pendingSubSchemas ) {
641
+ resolveErrors . push ( localize ( 'json.schema.idnotfound' , 'Subschema with id \'{0}\' was not found' , unresolvedSubschemaId ) ) ;
642
+ }
643
+ return new ResolvedSchema ( schema , resolveErrors ) ;
644
+ } ) ;
547
645
}
548
646
private getSchemaFromProperty ( resource : string , document : Parser . JSONDocument ) : string | undefined {
549
647
if ( document . root ?. type === 'object' ) {
@@ -618,7 +716,8 @@ export class JSONSchemaService implements IJSONSchemaService {
618
716
public getMatchingSchemas ( document : TextDocument , jsonDocument : Parser . JSONDocument , schema ?: JSONSchema ) : Thenable < MatchingSchema [ ] > {
619
717
if ( schema ) {
620
718
const id = schema . id || ( 'schemaservice://untitled/matchingSchemas/' + idCounter ++ ) ;
621
- return this . resolveSchemaContent ( new UnresolvedSchema ( schema ) , id , new Set ( ) ) . then ( resolvedSchema => {
719
+ const handle = this . addSchemaHandle ( id , schema ) ;
720
+ return handle . getResolvedSchema ( ) . then ( resolvedSchema => {
622
721
return jsonDocument . getMatchingSchemas ( resolvedSchema . schema ) . filter ( s => ! s . inverted ) ;
623
722
} ) ;
624
723
}
0 commit comments