@@ -29,6 +29,12 @@ import { parse } from 'yaml';
29
29
import * as Json from 'jsonc-parser' ;
30
30
import { getSchemaTitle } from '../utils/schemaUtils' ;
31
31
32
+ import * as Draft04 from '@hyperjump/json-schema/draft-04' ;
33
+ import * as Draft07 from '@hyperjump/json-schema/draft-07' ;
34
+ import * as Draft201909 from '@hyperjump/json-schema/draft-2019-09' ;
35
+ import * as Draft202012 from '@hyperjump/json-schema/draft-2020-12' ;
36
+
37
+ type SupportedSchemaVersions = '2020-12' | '2019-09' | 'draft-07' | 'draft-04' ;
32
38
export declare type CustomSchemaProvider = ( uri : string ) => Promise < string | string [ ] > ;
33
39
34
40
export enum MODIFICATION_ACTIONS {
@@ -154,12 +160,34 @@ export class YAMLSchemaService extends JSONSchemaService {
154
160
let schema : JSONSchema = schemaToResolve . schema ;
155
161
const contextService = this . contextService ;
156
162
157
- // Basic schema validation - check if schema is a valid object
158
163
if ( typeof schema !== 'object' || schema === null || Array . isArray ( schema ) ) {
159
164
const invalidSchemaType = Array . isArray ( schema ) ? 'array' : typeof schema ;
160
165
resolveErrors . push (
161
166
`Schema '${ getSchemaTitle ( schemaToResolve . schema , schemaURL ) } ' is not valid:\nWrong schema: "${ invalidSchemaType } ", it MUST be an Object or Boolean`
162
167
) ;
168
+ } else {
169
+ try {
170
+ const schemaVersion = this . detectSchemaVersion ( schema ) ;
171
+ const validator = this . getValidatorForVersion ( schemaVersion ) ;
172
+ const metaSchemaUrl = this . getSchemaMetaSchema ( schemaVersion ) ;
173
+
174
+ // Validate the schema against its meta-schema using the URL directly
175
+ const result = await validator . validate ( metaSchemaUrl , schema , 'BASIC' ) ;
176
+ if ( ! result . valid && result . errors ) {
177
+ const errs : string [ ] = [ ] ;
178
+ for ( const error of result . errors ) {
179
+ if ( error . instanceLocation && error . keyword ) {
180
+ errs . push ( `${ error . instanceLocation } : ${ this . extractKeywordName ( error . keyword ) } constraint violation` ) ;
181
+ }
182
+ }
183
+ if ( errs . length > 0 ) {
184
+ resolveErrors . push ( `Schema '${ getSchemaTitle ( schemaToResolve . schema , schemaURL ) } ' is not valid:\n${ errs . join ( '\n' ) } ` ) ;
185
+ }
186
+ }
187
+ } catch ( error ) {
188
+ // If meta-schema validation fails, log but don't block schema loading
189
+ console . error ( `Failed to validate schema meta-schema: ${ error . message } ` ) ;
190
+ }
163
191
}
164
192
165
193
const findSection = ( schema : JSONSchema , path : string ) : JSONSchema => {
@@ -268,16 +296,16 @@ export class YAMLSchemaService extends JSONSchemaService {
268
296
const seenRefs = new Set ( ) ;
269
297
while ( next . $ref ) {
270
298
const ref = decodeURIComponent ( next . $ref ) ;
271
- const segments = ref . split ( '#' , 2 ) ;
272
299
//return back removed $ref. We lost info about referenced type without it.
300
+ const segments = ref . split ( '#' , 2 ) ;
273
301
next . _$ref = next . $ref ;
274
302
delete next . $ref ;
275
303
if ( segments [ 0 ] . length > 0 ) {
276
304
openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentSchemaURL , parentSchemaDependencies ) ) ;
277
305
return ;
278
306
} else {
279
307
if ( ! seenRefs . has ( ref ) ) {
280
- merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ; // can set next.$ref again, use seenRefs to avoid circle
308
+ merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ;
281
309
seenRefs . add ( ref ) ;
282
310
}
283
311
}
@@ -475,7 +503,6 @@ export class YAMLSchemaService extends JSONSchemaService {
475
503
if ( prio > highestPrio ) {
476
504
highestPrio = prio ;
477
505
}
478
-
479
506
// Build up a mapping of priority to schemas so that we can easily get the highest priority schemas easier
480
507
let currPriorityArray = priorityMapping . get ( prio ) ;
481
508
if ( currPriorityArray ) {
@@ -620,10 +647,10 @@ export class YAMLSchemaService extends JSONSchemaService {
620
647
621
648
loadSchema ( schemaUri : string ) : Promise < UnresolvedSchema > {
622
649
const requestService = this . requestService ;
650
+ // If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
651
+ // If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
652
+ // contain a number instead of being undefined, so we need to check for that too.
623
653
return super . loadSchema ( schemaUri ) . then ( async ( unresolvedJsonSchema : UnresolvedSchema ) => {
624
- // If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
625
- // If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
626
- // contain a number instead of being undefined, so we need to check for that too.
627
654
if (
628
655
unresolvedJsonSchema . errors &&
629
656
( unresolvedJsonSchema . schema === undefined || typeof unresolvedJsonSchema . schema === 'number' )
@@ -656,9 +683,9 @@ export class YAMLSchemaService extends JSONSchemaService {
656
683
// eslint-disable-next-line @typescript-eslint/no-explicit-any
657
684
( error : any ) => {
658
685
let errorMessage = error . toString ( ) ;
686
+ // more concise error message, URL and context are attached by caller anyways
659
687
const errorSplit = error . toString ( ) . split ( 'Error: ' ) ;
660
688
if ( errorSplit . length > 1 ) {
661
- // more concise error message, URL and context are attached by caller anyways
662
689
errorMessage = errorSplit [ 1 ] ;
663
690
}
664
691
return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ;
@@ -725,6 +752,79 @@ export class YAMLSchemaService extends JSONSchemaService {
725
752
onResourceChange ( uri : string ) : boolean {
726
753
return super . onResourceChange ( uri ) ;
727
754
}
755
+
756
+ /**
757
+ * Detect the JSON Schema version from the $schema property
758
+ */
759
+ private detectSchemaVersion ( schema : JSONSchema ) : SupportedSchemaVersions {
760
+ const schemaProperty = schema . $schema ;
761
+ if ( typeof schemaProperty === 'string' ) {
762
+ if ( schemaProperty . includes ( '2020-12' ) ) {
763
+ return '2020-12' ;
764
+ } else if ( schemaProperty . includes ( '2019-09' ) ) {
765
+ return '2019-09' ;
766
+ } else if ( schemaProperty . includes ( 'draft-07' ) || schemaProperty . includes ( 'draft/7' ) ) {
767
+ return 'draft-07' ;
768
+ } else if ( schemaProperty . includes ( 'draft-04' ) || schemaProperty . includes ( 'draft/4' ) ) {
769
+ return 'draft-04' ;
770
+ }
771
+ }
772
+ return 'draft-07' ;
773
+ }
774
+
775
+ /**
776
+ * Get the appropriate validator module for a schema version
777
+ */
778
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
779
+ private getValidatorForVersion ( version : SupportedSchemaVersions ) : any {
780
+ switch ( version ) {
781
+ case '2020-12' :
782
+ return Draft202012 ;
783
+ case '2019-09' :
784
+ return Draft201909 ;
785
+ case 'draft-07' :
786
+ return Draft07 ;
787
+ case 'draft-04' :
788
+ default :
789
+ return Draft04 ;
790
+ }
791
+ }
792
+
793
+ /**
794
+ * Get the correct schema meta URI for a given version
795
+ */
796
+ private getSchemaMetaSchema ( version : SupportedSchemaVersions ) : string {
797
+ switch ( version ) {
798
+ case '2020-12' :
799
+ return 'https://json-schema.org/draft/2020-12/schema' ;
800
+ case '2019-09' :
801
+ return 'https://json-schema.org/draft/2019-09/schema' ;
802
+ case 'draft-07' :
803
+ return 'http://json-schema.org/draft-07/schema' ;
804
+ case 'draft-04' :
805
+ return 'http://json-schema.org/draft-04/schema' ;
806
+ default :
807
+ return 'http://json-schema.org/draft-07/schema' ;
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Extract a human-readable keyword name from a keyword URI
813
+ */
814
+ private extractKeywordName ( keywordUri : string ) : string {
815
+ if ( typeof keywordUri !== 'string' ) {
816
+ return 'validation' ;
817
+ }
818
+
819
+ const parts = keywordUri . split ( '/' ) ;
820
+ const lastPart = parts [ parts . length - 1 ] ;
821
+
822
+ if ( lastPart === 'validate' ) {
823
+ return 'schema validation' ;
824
+ }
825
+
826
+ return lastPart || 'validation' ;
827
+ }
728
828
}
729
829
730
830
function toDisplayString ( url : string ) : string {
0 commit comments