@@ -84,6 +84,7 @@ export const ProblemTypeMessages: Record<ProblemType, string> = {
8484 [ ProblemType . typeMismatchWarning ] : 'Incorrect type. Expected "{0}".' ,
8585 [ ProblemType . constWarning ] : 'Value must be {0}.' ,
8686} ;
87+
8788export interface IProblem {
8889 location : IRange ;
8990 severity : DiagnosticSeverity ;
@@ -161,6 +162,7 @@ export abstract class ASTNodeImpl {
161162export class NullASTNodeImpl extends ASTNodeImpl implements NullASTNode {
162163 public type : 'null' = 'null' as const ;
163164 public value = null ;
165+
164166 constructor ( parent : ASTNode , internalNode : Node , offset : number , length ?: number ) {
165167 super ( parent , internalNode , offset , length ) ;
166168 }
@@ -281,27 +283,36 @@ export enum EnumMatch {
281283
282284export interface ISchemaCollector {
283285 schemas : IApplicableSchema [ ] ;
286+
284287 add ( schema : IApplicableSchema ) : void ;
288+
285289 merge ( other : ISchemaCollector ) : void ;
290+
286291 include ( node : ASTNode ) : boolean ;
292+
287293 newSub ( ) : ISchemaCollector ;
288294}
289295
290296class SchemaCollector implements ISchemaCollector {
291297 schemas : IApplicableSchema [ ] = [ ] ;
298+
292299 constructor (
293300 private focusOffset = - 1 ,
294301 private exclude : ASTNode = null
295302 ) { }
303+
296304 add ( schema : IApplicableSchema ) : void {
297305 this . schemas . push ( schema ) ;
298306 }
307+
299308 merge ( other : ISchemaCollector ) : void {
300309 this . schemas . push ( ...other . schemas ) ;
301310 }
311+
302312 include ( node : ASTNode ) : boolean {
303313 return ( this . focusOffset === - 1 || contains ( node , this . focusOffset ) ) && node !== this . exclude ;
304314 }
315+
305316 newSub ( ) : ISchemaCollector {
306317 return new SchemaCollector ( - 1 , this . exclude ) ;
307318 }
@@ -311,22 +322,27 @@ class NoOpSchemaCollector implements ISchemaCollector {
311322 private constructor ( ) {
312323 // ignore
313324 }
325+
314326 // eslint-disable-next-line @typescript-eslint/no-explicit-any
315327 get schemas ( ) : any [ ] {
316328 return [ ] ;
317329 }
330+
318331 // eslint-disable-next-line @typescript-eslint/no-unused-vars
319332 add ( schema : IApplicableSchema ) : void {
320333 // ignore
321334 }
335+
322336 // eslint-disable-next-line @typescript-eslint/no-unused-vars
323337 merge ( other : ISchemaCollector ) : void {
324338 // ignore
325339 }
340+
326341 // eslint-disable-next-line @typescript-eslint/no-unused-vars
327342 include ( node : ASTNode ) : boolean {
328343 return true ;
329344 }
345+
330346 newSub ( ) : ISchemaCollector {
331347 return this ;
332348 }
@@ -620,13 +636,15 @@ export class JSONDocument {
620636 * @param focusOffset offsetValue
621637 * @param exclude excluded Node
622638 * @param didCallFromAutoComplete true if method called from AutoComplete
639+ * @param gracefulMatches true if graceful matching should be done, meaning that if at least one property is validated in a sub schema, it's kept as a candidate
623640 * @returns array of applicable schemas
624641 */
625642 public getMatchingSchemas (
626643 schema : JSONSchema ,
627644 focusOffset = - 1 ,
628645 exclude : ASTNode = null ,
629- didCallFromAutoComplete ?: boolean
646+ didCallFromAutoComplete ?: boolean ,
647+ gracefulMatches ?: boolean
630648 ) : IApplicableSchema [ ] {
631649 const matchingSchemas = new SchemaCollector ( focusOffset , exclude ) ;
632650 if ( this . root && schema ) {
@@ -635,17 +653,21 @@ export class JSONDocument {
635653 disableAdditionalProperties : this . disableAdditionalProperties ,
636654 uri : this . uri ,
637655 callFromAutoComplete : didCallFromAutoComplete ,
656+ gracefulMatches : gracefulMatches ,
638657 } ) ;
639658 }
640659 return matchingSchemas . schemas ;
641660 }
642661}
662+
643663interface Options {
644664 isKubernetes : boolean ;
645665 disableAdditionalProperties : boolean ;
646666 uri : string ;
647667 callFromAutoComplete ?: boolean ;
668+ gracefulMatches ?: boolean ;
648669}
670+
649671function validate (
650672 node : ASTNode ,
651673 schema : JSONSchema ,
@@ -655,7 +677,7 @@ function validate(
655677 options : Options
656678 // eslint-disable-next-line @typescript-eslint/no-explicit-any
657679) : any {
658- const { isKubernetes, callFromAutoComplete } = options ;
680+ const { isKubernetes, callFromAutoComplete, gracefulMatches } = options ;
659681 if ( ! node ) {
660682 return ;
661683 }
@@ -952,6 +974,7 @@ function validate(
952974 } ) ;
953975 }
954976 }
977+
955978 function getExclusiveLimit ( limit : number | undefined , exclusive : boolean | number | undefined ) : number | undefined {
956979 if ( isNumber ( exclusive ) ) {
957980 return exclusive ;
@@ -961,12 +984,14 @@ function validate(
961984 }
962985 return undefined ;
963986 }
987+
964988 function getLimit ( limit : number | undefined , exclusive : boolean | number | undefined ) : number | undefined {
965989 if ( ! isBoolean ( exclusive ) || ! exclusive ) {
966990 return limit ;
967991 }
968992 return undefined ;
969993 }
994+
970995 const exclusiveMinimum = getExclusiveLimit ( schema . minimum , schema . exclusiveMinimum ) ;
971996 if ( isNumber ( exclusiveMinimum ) && val <= exclusiveMinimum ) {
972997 validationResult . problems . push ( {
@@ -1102,6 +1127,7 @@ function validate(
11021127 }
11031128 }
11041129 }
1130+
11051131 function _validateArrayNode (
11061132 node : ArrayASTNode ,
11071133 schema : JSONSchema ,
@@ -1267,7 +1293,12 @@ function validate(
12671293 for ( const propertyName of schema . required ) {
12681294 if ( seenKeys [ propertyName ] === undefined ) {
12691295 const keyNode = node . parent && node . parent . type === 'property' && node . parent . keyNode ;
1270- const location = keyNode ? { offset : keyNode . offset , length : keyNode . length } : { offset : node . offset , length : 1 } ;
1296+ const location = keyNode
1297+ ? { offset : keyNode . offset , length : keyNode . length }
1298+ : {
1299+ offset : node . offset ,
1300+ length : 1 ,
1301+ } ;
12711302 validationResult . problems . push ( {
12721303 location : location ,
12731304 severity : DiagnosticSeverity . Warning ,
@@ -1520,10 +1551,14 @@ function validate(
15201551 return bestMatch ;
15211552 }
15221553
1554+ function gracefulMatchFilter ( maxOneMatch : boolean , propertiesValueMatches : number ) : boolean {
1555+ return gracefulMatches && ! maxOneMatch && callFromAutoComplete && propertiesValueMatches > 0 ;
1556+ }
1557+
15231558 //genericComparison tries to find the best matching schema using a generic comparison
15241559 function genericComparison (
15251560 node : ASTNode ,
1526- maxOneMatch ,
1561+ maxOneMatch : boolean ,
15271562 subValidationResult : ValidationResult ,
15281563 bestMatch : IValidationMatch ,
15291564 subSchema ,
@@ -1552,7 +1587,8 @@ function validate(
15521587 } ;
15531588 } else if (
15541589 compareResult === 0 ||
1555- ( ( node . value === null || node . type === 'null' ) && node . length === 0 ) // node with no value can match any schema potentially
1590+ ( ( node . value === null || node . type === 'null' ) && node . length === 0 ) || // node with no value can match any schema potentially
1591+ gracefulMatchFilter ( maxOneMatch , subValidationResult . propertiesValueMatches )
15561592 ) {
15571593 // there's already a best matching but we are as good
15581594 mergeValidationMatches ( bestMatch , subMatchingSchemas , subValidationResult ) ;
0 commit comments