@@ -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 {
@@ -90,7 +96,6 @@ interface SchemaStoreSchema {
90
96
versions ?: SchemaVersions ;
91
97
}
92
98
export class YAMLSchemaService extends JSONSchemaService {
93
- // To allow to use schemasById from super.
94
99
// eslint-disable-next-line @typescript-eslint/no-explicit-any
95
100
[ x : string ] : any ;
96
101
@@ -154,12 +159,34 @@ export class YAMLSchemaService extends JSONSchemaService {
154
159
let schema : JSONSchema = schemaToResolve . schema ;
155
160
const contextService = this . contextService ;
156
161
157
- // Basic schema validation - check if schema is a valid object
158
162
if ( typeof schema !== 'object' || schema === null || Array . isArray ( schema ) ) {
159
163
const invalidSchemaType = Array . isArray ( schema ) ? 'array' : typeof schema ;
160
164
resolveErrors . push (
161
165
`Schema '${ getSchemaTitle ( schemaToResolve . schema , schemaURL ) } ' is not valid:\nWrong schema: "${ invalidSchemaType } ", it MUST be an Object or Boolean`
162
166
) ;
167
+ } else {
168
+ try {
169
+ const schemaVersion = this . detectSchemaVersion ( schema ) ;
170
+ const validator = this . getValidatorForVersion ( schemaVersion ) ;
171
+ const metaSchemaUrl = this . getSchemaMetaSchema ( schemaVersion ) ;
172
+
173
+ // Validate the schema against its meta-schema using the URL directly
174
+ const result = await validator . validate ( metaSchemaUrl , schema , 'BASIC' ) ;
175
+ if ( ! result . valid && result . errors ) {
176
+ const errs : string [ ] = [ ] ;
177
+ for ( const error of result . errors ) {
178
+ if ( error . instanceLocation && error . keyword ) {
179
+ errs . push ( `${ error . instanceLocation } : ${ this . extractKeywordName ( error . keyword ) } constraint violation` ) ;
180
+ }
181
+ }
182
+ if ( errs . length > 0 ) {
183
+ resolveErrors . push ( `Schema '${ getSchemaTitle ( schemaToResolve . schema , schemaURL ) } ' is not valid:\n${ errs . join ( '\n' ) } ` ) ;
184
+ }
185
+ }
186
+ } catch ( error ) {
187
+ // If meta-schema validation fails, log but don't block schema loading
188
+ console . error ( `Failed to validate schema meta-schema: ${ error . message } ` ) ;
189
+ }
163
190
}
164
191
165
192
const findSection = ( schema : JSONSchema , path : string ) : JSONSchema => {
@@ -269,15 +296,14 @@ export class YAMLSchemaService extends JSONSchemaService {
269
296
while ( next . $ref ) {
270
297
const ref = decodeURIComponent ( next . $ref ) ;
271
298
const segments = ref . split ( '#' , 2 ) ;
272
- //return back removed $ref. We lost info about referenced type without it.
273
299
next . _$ref = next . $ref ;
274
300
delete next . $ref ;
275
301
if ( segments [ 0 ] . length > 0 ) {
276
302
openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentSchemaURL , parentSchemaDependencies ) ) ;
277
303
return ;
278
304
} else {
279
305
if ( ! seenRefs . has ( ref ) ) {
280
- merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ; // can set next.$ref again, use seenRefs to avoid circle
306
+ merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ;
281
307
seenRefs . add ( ref ) ;
282
308
}
283
309
}
@@ -335,9 +361,6 @@ export class YAMLSchemaService extends JSONSchemaService {
335
361
let schemaFromModeline = getSchemaFromModeline ( doc ) ;
336
362
if ( schemaFromModeline !== undefined ) {
337
363
if ( ! schemaFromModeline . startsWith ( 'file:' ) && ! schemaFromModeline . startsWith ( 'http' ) ) {
338
- // If path contains a fragment and it is left intact, "#" will be
339
- // considered part of the filename and converted to "%23" by
340
- // path.resolve() -> take it out and add back after path.resolve
341
364
let appendix = '' ;
342
365
if ( schemaFromModeline . indexOf ( '#' ) > 0 ) {
343
366
const segments = schemaFromModeline . split ( '#' , 2 ) ;
@@ -393,7 +416,6 @@ export class YAMLSchemaService extends JSONSchemaService {
393
416
}
394
417
395
418
if ( schemas . length > 0 ) {
396
- // Join all schemas with the highest priority.
397
419
const highestPrioSchemas = this . highestPrioritySchemas ( schemas ) ;
398
420
return resolveSchemaForResource ( highestPrioSchemas ) ;
399
421
}
@@ -469,14 +491,12 @@ export class YAMLSchemaService extends JSONSchemaService {
469
491
let highestPrio = 0 ;
470
492
const priorityMapping = new Map < SchemaPriority , string [ ] > ( ) ;
471
493
schemas . forEach ( ( schema ) => {
472
- // If the schema does not have a priority then give it a default one of [0]
473
494
const priority = this . schemaPriorityMapping . get ( schema ) || [ 0 ] ;
474
495
priority . forEach ( ( prio ) => {
475
496
if ( prio > highestPrio ) {
476
497
highestPrio = prio ;
477
498
}
478
499
479
- // Build up a mapping of priority to schemas so that we can easily get the highest priority schemas easier
480
500
let currPriorityArray = priorityMapping . get ( prio ) ;
481
501
if ( currPriorityArray ) {
482
502
currPriorityArray = ( currPriorityArray as string [ ] ) . concat ( schema ) ;
@@ -601,7 +621,6 @@ export class YAMLSchemaService extends JSONSchemaService {
601
621
*/
602
622
603
623
normalizeId ( id : string ) : string {
604
- // The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here
605
624
try {
606
625
return URI . parse ( id ) . toString ( ) ;
607
626
} catch ( e ) {
@@ -621,9 +640,6 @@ export class YAMLSchemaService extends JSONSchemaService {
621
640
loadSchema ( schemaUri : string ) : Promise < UnresolvedSchema > {
622
641
const requestService = this . requestService ;
623
642
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
643
if (
628
644
unresolvedJsonSchema . errors &&
629
645
( unresolvedJsonSchema . schema === undefined || typeof unresolvedJsonSchema . schema === 'number' )
@@ -658,7 +674,6 @@ export class YAMLSchemaService extends JSONSchemaService {
658
674
let errorMessage = error . toString ( ) ;
659
675
const errorSplit = error . toString ( ) . split ( 'Error: ' ) ;
660
676
if ( errorSplit . length > 1 ) {
661
- // more concise error message, URL and context are attached by caller anyways
662
677
errorMessage = errorSplit [ 1 ] ;
663
678
}
664
679
return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ;
@@ -725,6 +740,79 @@ export class YAMLSchemaService extends JSONSchemaService {
725
740
onResourceChange ( uri : string ) : boolean {
726
741
return super . onResourceChange ( uri ) ;
727
742
}
743
+
744
+ /**
745
+ * Detect the JSON Schema version from the $schema property
746
+ */
747
+ private detectSchemaVersion ( schema : JSONSchema ) : SupportedSchemaVersions {
748
+ const schemaProperty = schema . $schema ;
749
+ if ( typeof schemaProperty === 'string' ) {
750
+ if ( schemaProperty . includes ( '2020-12' ) ) {
751
+ return '2020-12' ;
752
+ } else if ( schemaProperty . includes ( '2019-09' ) ) {
753
+ return '2019-09' ;
754
+ } else if ( schemaProperty . includes ( 'draft-07' ) || schemaProperty . includes ( 'draft/7' ) ) {
755
+ return 'draft-07' ;
756
+ } else if ( schemaProperty . includes ( 'draft-04' ) || schemaProperty . includes ( 'draft/4' ) ) {
757
+ return 'draft-04' ;
758
+ }
759
+ }
760
+ return 'draft-07' ;
761
+ }
762
+
763
+ /**
764
+ * Get the appropriate validator module for a schema version
765
+ */
766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
767
+ private getValidatorForVersion ( version : SupportedSchemaVersions ) : any {
768
+ switch ( version ) {
769
+ case '2020-12' :
770
+ return Draft202012 ;
771
+ case '2019-09' :
772
+ return Draft201909 ;
773
+ case 'draft-07' :
774
+ return Draft07 ;
775
+ case 'draft-04' :
776
+ default :
777
+ return Draft04 ;
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Get the correct schema meta URI for a given version
783
+ */
784
+ private getSchemaMetaSchema ( version : SupportedSchemaVersions ) : string {
785
+ switch ( version ) {
786
+ case '2020-12' :
787
+ return 'https://json-schema.org/draft/2020-12/schema' ;
788
+ case '2019-09' :
789
+ return 'https://json-schema.org/draft/2019-09/schema' ;
790
+ case 'draft-07' :
791
+ return 'http://json-schema.org/draft-07/schema' ;
792
+ case 'draft-04' :
793
+ return 'http://json-schema.org/draft-04/schema' ;
794
+ default :
795
+ return 'http://json-schema.org/draft-07/schema' ;
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Extract a human-readable keyword name from a keyword URI
801
+ */
802
+ private extractKeywordName ( keywordUri : string ) : string {
803
+ if ( typeof keywordUri !== 'string' ) {
804
+ return 'validation' ;
805
+ }
806
+
807
+ const parts = keywordUri . split ( '/' ) ;
808
+ const lastPart = parts [ parts . length - 1 ] ;
809
+
810
+ if ( lastPart === 'validate' ) {
811
+ return 'schema validation' ;
812
+ }
813
+
814
+ return lastPart || 'validation' ;
815
+ }
728
816
}
729
817
730
818
function toDisplayString ( url : string ) : string {
@@ -741,7 +829,7 @@ function toDisplayString(url: string): string {
741
829
742
830
function getLineAndColumnFromOffset ( text : string , offset : number ) : { line : number ; column : number } {
743
831
const lines = text . slice ( 0 , offset ) . split ( / \r ? \n / ) ;
744
- const line = lines . length ; // 1-based line number
745
- const column = lines [ lines . length - 1 ] . length + 1 ; // 1-based column number
832
+ const line = lines . length ;
833
+ const column = lines [ lines . length - 1 ] . length + 1 ;
746
834
return { line, column } ;
747
835
}
0 commit comments