Skip to content

Commit e214a0a

Browse files
committed
fix: anyOf compare alternatives
1 parent 7203630 commit e214a0a

File tree

3 files changed

+205
-29
lines changed

3 files changed

+205
-29
lines changed

src/languageservice/parser/jsonParser07.ts

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,12 @@ export function findNodeAtOffset(node: ASTNode, offset: number, includeRightBoun
530530
return undefined;
531531
}
532532

533+
interface IValidationMatch {
534+
schema: JSONSchema;
535+
validationResult: ValidationResult;
536+
matchingSchemas: ISchemaCollector;
537+
}
538+
533539
export class JSONDocument {
534540
public isKubernetes: boolean;
535541
public disableAdditionalProperties: boolean;
@@ -863,7 +869,7 @@ function validate(
863869
const val = getNodeValue(node);
864870
let enumValueMatch = false;
865871
for (const e of schema.enum) {
866-
if (equals(val, e) || (callFromAutoComplete && isString(val) && isString(e) && val && e.startsWith(val))) {
872+
if (equals(val, e) || isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, e)) {
867873
enumValueMatch = true;
868874
break;
869875
}
@@ -894,10 +900,7 @@ function validate(
894900

895901
if (isDefined(schema.const)) {
896902
const val = getNodeValue(node);
897-
if (
898-
!equals(val, schema.const) &&
899-
!(callFromAutoComplete && isString(val) && isString(schema.const) && schema.const.startsWith(val))
900-
) {
903+
if (!equals(val, schema.const) && !isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, schema.const)) {
901904
validationResult.problems.push({
902905
location: { offset: node.offset, length: node.length },
903906
severity: DiagnosticSeverity.Warning,
@@ -1498,23 +1501,11 @@ function validate(
14981501
node: ASTNode,
14991502
maxOneMatch,
15001503
subValidationResult: ValidationResult,
1501-
bestMatch: {
1502-
schema: JSONSchema;
1503-
validationResult: ValidationResult;
1504-
matchingSchemas: ISchemaCollector;
1505-
},
1504+
bestMatch: IValidationMatch,
15061505
subSchema,
15071506
subMatchingSchemas
1508-
): {
1509-
schema: JSONSchema;
1510-
validationResult: ValidationResult;
1511-
matchingSchemas: ISchemaCollector;
1512-
} {
1513-
if (
1514-
!maxOneMatch &&
1515-
!subValidationResult.hasProblems() &&
1516-
(!bestMatch.validationResult.hasProblems() || callFromAutoComplete)
1517-
) {
1507+
): IValidationMatch {
1508+
if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) {
15181509
// no errors, both are equally good matches
15191510
bestMatch.matchingSchemas.merge(subMatchingSchemas);
15201511
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
@@ -1535,19 +1526,30 @@ function validate(
15351526
validationResult: subValidationResult,
15361527
matchingSchemas: subMatchingSchemas,
15371528
};
1538-
} else if (compareResult === 0) {
1529+
} else if (
1530+
compareResult === 0 ||
1531+
((node.value === null || node.type === 'null') && node.length === 0) // node with no value can match any schema potentially
1532+
) {
15391533
// there's already a best matching but we are as good
1540-
bestMatch.matchingSchemas.merge(subMatchingSchemas);
1541-
bestMatch.validationResult.mergeEnumValues(subValidationResult);
1542-
bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [
1543-
ProblemType.missingRequiredPropWarning,
1544-
ProblemType.typeMismatchWarning,
1545-
ProblemType.constWarning,
1546-
]);
1534+
mergeValidationMatches(bestMatch, subMatchingSchemas, subValidationResult);
15471535
}
15481536
}
15491537
return bestMatch;
15501538
}
1539+
1540+
function mergeValidationMatches(
1541+
bestMatch: IValidationMatch,
1542+
subMatchingSchemas: ISchemaCollector,
1543+
subValidationResult: ValidationResult
1544+
): void {
1545+
bestMatch.matchingSchemas.merge(subMatchingSchemas);
1546+
bestMatch.validationResult.mergeEnumValues(subValidationResult);
1547+
bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [
1548+
ProblemType.missingRequiredPropWarning,
1549+
ProblemType.typeMismatchWarning,
1550+
ProblemType.constWarning,
1551+
]);
1552+
}
15511553
}
15521554

15531555
function getSchemaSource(schema: JSONSchema, originalSchema: JSONSchema): string | undefined {
@@ -1585,3 +1587,26 @@ function getSchemaUri(schema: JSONSchema, originalSchema: JSONSchema): string[]
15851587
function getWarningMessage(problemType: ProblemType, args: string[]): string {
15861588
return localize(problemType, ProblemTypeMessages[problemType], args.join(' | '));
15871589
}
1590+
1591+
/**
1592+
* if callFromAutoComplete than compare value from yaml and value from schema (s.const | s.enum[i])
1593+
* allows partial match for autocompletion
1594+
*/
1595+
function isAutoCompleteEqualMaybe(
1596+
callFromAutoComplete: boolean,
1597+
node: ASTNode,
1598+
nodeValue: unknown,
1599+
schemaValue: unknown
1600+
): boolean {
1601+
if (!callFromAutoComplete) {
1602+
return false;
1603+
}
1604+
1605+
// if autocompletion property doesn't have value, then it could be a match
1606+
const isWithoutValue = nodeValue === null && node.length === 0; // allows `prop: ` but ignore `prop: null`
1607+
if (isWithoutValue) {
1608+
return true;
1609+
}
1610+
1611+
return isString(nodeValue) && isString(schemaValue) && schemaValue.startsWith(nodeValue);
1612+
}

test/autoCompletionFix.test.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,157 @@ objB:
759759
expect(completion.items[0].label).to.be.equal('prop');
760760
expect(completion.items[0].insertText).to.be.equal('prop: ${1|const value,null|}');
761761
});
762+
it('should take all sub-schemas when value has not been set (cursor in the middle of the empty space)', async () => {
763+
const schema: JSONSchema = {
764+
anyOf: [
765+
{
766+
properties: {
767+
prop: { type: 'null' },
768+
},
769+
},
770+
{
771+
properties: {
772+
prop: { const: 'const value' },
773+
},
774+
},
775+
{
776+
properties: {
777+
prop: { const: 5 },
778+
},
779+
},
780+
],
781+
};
782+
schemaProvider.addSchema(SCHEMA_ID, schema);
783+
const content = 'prop: | | ';
784+
const completion = await parseCaret(content);
785+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['const value', '5', 'null']);
786+
});
787+
it('should take only null sub-schema when value is "null"', async () => {
788+
const schema: JSONSchema = {
789+
anyOf: [
790+
{
791+
properties: {
792+
prop: { type: 'null' },
793+
},
794+
},
795+
{
796+
properties: {
797+
prop: { const: 'const value' },
798+
},
799+
},
800+
],
801+
};
802+
schemaProvider.addSchema(SCHEMA_ID, schema);
803+
const content = 'prop: null';
804+
const completion = await parseSetup(content, 0, content.length);
805+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['null']);
806+
});
807+
it('should take only one sub-schema because first sub-schema does not match', async () => {
808+
const schema: JSONSchema = {
809+
anyOf: [
810+
{
811+
properties: {
812+
prop: { const: 'const value' },
813+
},
814+
},
815+
{
816+
properties: {
817+
prop: { const: 'const value2' },
818+
},
819+
},
820+
],
821+
};
822+
schemaProvider.addSchema(SCHEMA_ID, schema);
823+
const content = 'prop: const value2';
824+
const completion = await parseSetup(content, 0, content.length);
825+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['const value2']);
826+
});
827+
it('should match only second sub-schema because the first one does not match', async () => {
828+
const schema: JSONSchema = {
829+
anyOf: [
830+
{
831+
properties: {
832+
prop: {
833+
const: 'typeA',
834+
},
835+
propA: {},
836+
},
837+
},
838+
{
839+
properties: {
840+
prop: {
841+
const: 'typeB',
842+
},
843+
propB: {},
844+
},
845+
},
846+
],
847+
};
848+
schemaProvider.addSchema(SCHEMA_ID, schema);
849+
const content = 'prop: typeB\n|\n|';
850+
const completion = await parseCaret(content);
851+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['propB']);
852+
});
853+
it('should suggest from all sub-schemas even if nodes properties match better other schema', async () => {
854+
// this is a case when we have a better match in the second schema but we should still suggest from the first one
855+
// it works because `prop: ` will evaluate to `enumValueMatch = true` for both schemas
856+
const schema: JSONSchema = {
857+
anyOf: [
858+
{
859+
properties: {
860+
prop: {
861+
const: 'typeA',
862+
},
863+
},
864+
},
865+
{
866+
properties: {
867+
prop: {
868+
const: 'typeB',
869+
},
870+
propB: {},
871+
},
872+
},
873+
],
874+
};
875+
schemaProvider.addSchema(SCHEMA_ID, schema);
876+
const content = 'prop: |\n|\npropB: B';
877+
const completion = await parseCaret(content);
878+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['typeA', 'typeB'], 'with null value');
879+
880+
const content2 = 'prop: typ|\n|\npropB: B';
881+
const completion2 = await parseCaret(content2);
882+
expect(completion2.items.map((i) => i.label)).to.be.deep.eq(['typeA', 'typeB'], 'with prefix value');
883+
});
884+
885+
it('should suggest both sub-schemas for anyof array', async () => {
886+
const schema: JSONSchema = {
887+
properties: {
888+
entities: {
889+
type: 'array',
890+
items: {
891+
anyOf: [
892+
{
893+
enum: ['enum1'],
894+
},
895+
{
896+
type: 'object',
897+
title: 'entity object',
898+
properties: {
899+
entityProp: { type: 'string' },
900+
},
901+
required: ['entityProp'],
902+
},
903+
],
904+
},
905+
},
906+
},
907+
};
908+
schemaProvider.addSchema(SCHEMA_ID, schema);
909+
const content = 'entities:\n - |\n|';
910+
const completion = await parseCaret(content);
911+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['enum1', 'entityProp', 'entity object']);
912+
});
762913
});
763914

764915
describe('extra space after cursor', () => {

test/schemaValidation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,7 @@ obj:
12891289
4,
12901290
18,
12911291
DiagnosticSeverity.Error,
1292-
'yaml-schema: Package',
1292+
'yaml-schema: Composer Package',
12931293
'https://raw.githubusercontent.com/composer/composer/master/res/composer-schema.json'
12941294
)
12951295
);

0 commit comments

Comments
 (0)