Skip to content

Commit 4e05825

Browse files
authored
Merge branch 'main' into feat/property-snippets
2 parents e2d85eb + 2a2e614 commit 4e05825

5 files changed

Lines changed: 298 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
### 1.18.0
2+
- Feat: Do not suggest propertyNames if doNotSuggest is true [#1045](https://github.com/redhat-developer/yaml-language-server/pull/1045)
3+
- Feat: Exclude not suggest properties from possible properties error [#1051](https://github.com/redhat-developer/yaml-language-server/pull/1051)
4+
- Feat: Fix enum values to be unique [#1028](https://github.com/redhat-developer/yaml-language-server/pull/1028)
5+
- Fix: URL-encoded characters in $ref paths break schema resolution [#1082](https://github.com/redhat-developer/vscode-yaml/issues/1082)
6+
- Fix: Tests fail with Node.JS 23.7.x [#1018](https://github.com/redhat-developer/yaml-language-server/issues/1018)
7+
- Fix: Autocompletion problem when value is null inside anyOf object [#684](https://github.com/redhat-developer/yaml-language-server/issues/684)
8+
- Fix: Enum strings YES / NO are converted to boolean incorrectly [#1036](https://github.com/redhat-developer/yaml-language-server/issues/1036)
9+
- Fix: Autocompletion with escape sequence chars [#1040](https://github.com/redhat-developer/yaml-language-server/pull/1040)
10+
- Fix: multipleOf does not work for floats [#985](https://github.com/redhat-developer/yaml-language-server/issues/985)
11+
12+
Thanks to [Petr Spacek](https://github.com/p-spacek), [Kosta](https://github.com/Kosta-Github) and [Willem Jan](https://github.com/Willem-J-an) for your contributions.
13+
114
### 1.17.0
215
- Feat: Supported docker arm64 image [#1027](https://github.com/redhat-developer/yaml-language-server/pull/1027)
316
- Fix: Reverted ajv-draft04 as it not support HTTPS [#1026](https://github.com/redhat-developer/yaml-language-server/pull/1026)

src/languageservice/parser/jsonParser07.ts

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,12 @@ export function findNodeAtOffset(node: ASTNode, offset: number, includeRightBoun
537537
return undefined;
538538
}
539539

540+
interface IValidationMatch {
541+
schema: JSONSchema;
542+
validationResult: ValidationResult;
543+
matchingSchemas: ISchemaCollector;
544+
}
545+
540546
export class JSONDocument {
541547
public isKubernetes: boolean;
542548
public disableAdditionalProperties: boolean;
@@ -870,7 +876,7 @@ function validate(
870876
const val = getNodeValue(node);
871877
let enumValueMatch = false;
872878
for (const e of schema.enum) {
873-
if (val === e || (callFromAutoComplete && isString(val) && isString(e) && val && e.startsWith(val))) {
879+
if (val === e || isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, e)) {
874880
enumValueMatch = true;
875881
break;
876882
}
@@ -902,10 +908,7 @@ function validate(
902908

903909
if (isDefined(schema.const)) {
904910
const val = getNodeValue(node);
905-
if (
906-
!equals(val, schema.const) &&
907-
!(callFromAutoComplete && isString(val) && isString(schema.const) && schema.const.startsWith(val))
908-
) {
911+
if (!equals(val, schema.const) && !isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, schema.const)) {
909912
validationResult.problems.push({
910913
location: { offset: node.offset, length: node.length },
911914
severity: DiagnosticSeverity.Warning,
@@ -1374,7 +1377,21 @@ function validate(
13741377
(schema.type === 'object' && schema.additionalProperties === undefined && options.disableAdditionalProperties === true)
13751378
) {
13761379
if (unprocessedProperties.length > 0) {
1377-
const possibleProperties = schema.properties && Object.keys(schema.properties).filter((prop) => !seenKeys[prop]);
1380+
const possibleProperties =
1381+
schema.properties &&
1382+
Object.entries(schema.properties)
1383+
.filter(([key, property]) => {
1384+
// don't include existing properties
1385+
if (seenKeys[key]) {
1386+
return false;
1387+
}
1388+
// don't include properties that are not suggested in completion
1389+
if (property && typeof property === 'object' && (property.doNotSuggest || property.deprecationMessage)) {
1390+
return false;
1391+
}
1392+
return true;
1393+
})
1394+
.map(([key]) => key);
13781395

13791396
for (const propertyName of unprocessedProperties) {
13801397
const child = seenKeys[propertyName];
@@ -1508,23 +1525,11 @@ function validate(
15081525
node: ASTNode,
15091526
maxOneMatch,
15101527
subValidationResult: ValidationResult,
1511-
bestMatch: {
1512-
schema: JSONSchema;
1513-
validationResult: ValidationResult;
1514-
matchingSchemas: ISchemaCollector;
1515-
},
1528+
bestMatch: IValidationMatch,
15161529
subSchema,
15171530
subMatchingSchemas
1518-
): {
1519-
schema: JSONSchema;
1520-
validationResult: ValidationResult;
1521-
matchingSchemas: ISchemaCollector;
1522-
} {
1523-
if (
1524-
!maxOneMatch &&
1525-
!subValidationResult.hasProblems() &&
1526-
(!bestMatch.validationResult.hasProblems() || callFromAutoComplete)
1527-
) {
1531+
): IValidationMatch {
1532+
if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) {
15281533
// no errors, both are equally good matches
15291534
bestMatch.matchingSchemas.merge(subMatchingSchemas);
15301535
bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches;
@@ -1545,19 +1550,30 @@ function validate(
15451550
validationResult: subValidationResult,
15461551
matchingSchemas: subMatchingSchemas,
15471552
};
1548-
} else if (compareResult === 0) {
1553+
} else if (
1554+
compareResult === 0 ||
1555+
((node.value === null || node.type === 'null') && node.length === 0) // node with no value can match any schema potentially
1556+
) {
15491557
// there's already a best matching but we are as good
1550-
bestMatch.matchingSchemas.merge(subMatchingSchemas);
1551-
bestMatch.validationResult.mergeEnumValues(subValidationResult);
1552-
bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [
1553-
ProblemType.missingRequiredPropWarning,
1554-
ProblemType.typeMismatchWarning,
1555-
ProblemType.constWarning,
1556-
]);
1558+
mergeValidationMatches(bestMatch, subMatchingSchemas, subValidationResult);
15571559
}
15581560
}
15591561
return bestMatch;
15601562
}
1563+
1564+
function mergeValidationMatches(
1565+
bestMatch: IValidationMatch,
1566+
subMatchingSchemas: ISchemaCollector,
1567+
subValidationResult: ValidationResult
1568+
): void {
1569+
bestMatch.matchingSchemas.merge(subMatchingSchemas);
1570+
bestMatch.validationResult.mergeEnumValues(subValidationResult);
1571+
bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [
1572+
ProblemType.missingRequiredPropWarning,
1573+
ProblemType.typeMismatchWarning,
1574+
ProblemType.constWarning,
1575+
]);
1576+
}
15611577
}
15621578

15631579
function getSchemaSource(schema: JSONSchema, originalSchema: JSONSchema): string | undefined {
@@ -1595,3 +1611,26 @@ function getSchemaUri(schema: JSONSchema, originalSchema: JSONSchema): string[]
15951611
function getWarningMessage(problemType: ProblemType, args: string[]): string {
15961612
return localize(problemType, ProblemTypeMessages[problemType], args.join(' | '));
15971613
}
1614+
1615+
/**
1616+
* if callFromAutoComplete than compare value from yaml and value from schema (s.const | s.enum[i])
1617+
* allows partial match for autocompletion
1618+
*/
1619+
function isAutoCompleteEqualMaybe(
1620+
callFromAutoComplete: boolean,
1621+
node: ASTNode,
1622+
nodeValue: unknown,
1623+
schemaValue: unknown
1624+
): boolean {
1625+
if (!callFromAutoComplete) {
1626+
return false;
1627+
}
1628+
1629+
// if autocompletion property doesn't have value, then it could be a match
1630+
const isWithoutValue = nodeValue === null && node.length === 0; // allows `prop: ` but ignore `prop: null`
1631+
if (isWithoutValue) {
1632+
return true;
1633+
}
1634+
1635+
return isString(nodeValue) && isString(schemaValue) && schemaValue.startsWith(nodeValue);
1636+
}

src/languageservice/services/yamlCompletion.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -862,14 +862,16 @@ export class YamlCompletion {
862862

863863
if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === 'object') {
864864
const propertyNameSchema = asSchema(schema.schema.propertyNames);
865-
const label = propertyNameSchema.title || 'property';
866-
collector.add({
867-
kind: CompletionItemKind.Property,
868-
label,
869-
insertText: '$' + `{1:${label}}: `,
870-
insertTextFormat: InsertTextFormat.Snippet,
871-
documentation: this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || '',
872-
});
865+
if (!propertyNameSchema.deprecationMessage && !propertyNameSchema.doNotSuggest) {
866+
const label = propertyNameSchema.title || 'property';
867+
collector.add({
868+
kind: CompletionItemKind.Property,
869+
label,
870+
insertText: '$' + `{1:${label}}: `,
871+
insertTextFormat: InsertTextFormat.Snippet,
872+
documentation: this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || '',
873+
});
874+
}
873875
}
874876
}
875877

test/autoCompletionFix.test.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,157 @@ objB:
792792
expect(completion.items[0].label).to.be.equal('prop');
793793
expect(completion.items[0].insertText).to.be.equal('prop: ${1|const value,null|}');
794794
});
795+
it('should take all sub-schemas when value has not been set (cursor in the middle of the empty space)', async () => {
796+
const schema: JSONSchema = {
797+
anyOf: [
798+
{
799+
properties: {
800+
prop: { type: 'null' },
801+
},
802+
},
803+
{
804+
properties: {
805+
prop: { const: 'const value' },
806+
},
807+
},
808+
{
809+
properties: {
810+
prop: { const: 5 },
811+
},
812+
},
813+
],
814+
};
815+
schemaProvider.addSchema(SCHEMA_ID, schema);
816+
const content = 'prop: | | ';
817+
const completion = await parseCaret(content);
818+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['const value', '5', 'null']);
819+
});
820+
it('should take only null sub-schema when value is "null"', async () => {
821+
const schema: JSONSchema = {
822+
anyOf: [
823+
{
824+
properties: {
825+
prop: { type: 'null' },
826+
},
827+
},
828+
{
829+
properties: {
830+
prop: { const: 'const value' },
831+
},
832+
},
833+
],
834+
};
835+
schemaProvider.addSchema(SCHEMA_ID, schema);
836+
const content = 'prop: null';
837+
const completion = await parseSetup(content, 0, content.length);
838+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['null']);
839+
});
840+
it('should take only one sub-schema because first sub-schema does not match', async () => {
841+
const schema: JSONSchema = {
842+
anyOf: [
843+
{
844+
properties: {
845+
prop: { const: 'const value' },
846+
},
847+
},
848+
{
849+
properties: {
850+
prop: { const: 'const value2' },
851+
},
852+
},
853+
],
854+
};
855+
schemaProvider.addSchema(SCHEMA_ID, schema);
856+
const content = 'prop: const value2';
857+
const completion = await parseSetup(content, 0, content.length);
858+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['const value2']);
859+
});
860+
it('should match only second sub-schema because the first one does not match', async () => {
861+
const schema: JSONSchema = {
862+
anyOf: [
863+
{
864+
properties: {
865+
prop: {
866+
const: 'typeA',
867+
},
868+
propA: {},
869+
},
870+
},
871+
{
872+
properties: {
873+
prop: {
874+
const: 'typeB',
875+
},
876+
propB: {},
877+
},
878+
},
879+
],
880+
};
881+
schemaProvider.addSchema(SCHEMA_ID, schema);
882+
const content = 'prop: typeB\n|\n|';
883+
const completion = await parseCaret(content);
884+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['propB']);
885+
});
886+
it('should suggest from all sub-schemas even if nodes properties match better other schema', async () => {
887+
// this is a case when we have a better match in the second schema but we should still suggest from the first one
888+
// it works because `prop: ` will evaluate to `enumValueMatch = true` for both schemas
889+
const schema: JSONSchema = {
890+
anyOf: [
891+
{
892+
properties: {
893+
prop: {
894+
const: 'typeA',
895+
},
896+
},
897+
},
898+
{
899+
properties: {
900+
prop: {
901+
const: 'typeB',
902+
},
903+
propB: {},
904+
},
905+
},
906+
],
907+
};
908+
schemaProvider.addSchema(SCHEMA_ID, schema);
909+
const content = 'prop: |\n|\npropB: B';
910+
const completion = await parseCaret(content);
911+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['typeA', 'typeB'], 'with null value');
912+
913+
const content2 = 'prop: typ|\n|\npropB: B';
914+
const completion2 = await parseCaret(content2);
915+
expect(completion2.items.map((i) => i.label)).to.be.deep.eq(['typeA', 'typeB'], 'with prefix value');
916+
});
917+
918+
it('should suggest both sub-schemas for anyof array', async () => {
919+
const schema: JSONSchema = {
920+
properties: {
921+
entities: {
922+
type: 'array',
923+
items: {
924+
anyOf: [
925+
{
926+
enum: ['enum1'],
927+
},
928+
{
929+
type: 'object',
930+
title: 'entity object',
931+
properties: {
932+
entityProp: { type: 'string' },
933+
},
934+
required: ['entityProp'],
935+
},
936+
],
937+
},
938+
},
939+
},
940+
};
941+
schemaProvider.addSchema(SCHEMA_ID, schema);
942+
const content = 'entities:\n - |\n|';
943+
const completion = await parseCaret(content);
944+
expect(completion.items.map((i) => i.label)).to.be.deep.eq(['enum1', 'entityProp', 'entity object']);
945+
});
795946
});
796947

797948
describe('extra space after cursor', () => {
@@ -1362,6 +1513,22 @@ test1:
13621513
expect(completion.items[0].insertText).to.be.equal('${1:property}: ');
13631514
expect(completion.items[0].documentation).to.be.equal('Property Description');
13641515
});
1516+
it('should not suggest propertyNames with doNotSuggest', async () => {
1517+
const schema: JSONSchema = {
1518+
type: 'object',
1519+
additionalProperties: true,
1520+
propertyNames: {
1521+
title: 'property',
1522+
doNotSuggest: true,
1523+
},
1524+
};
1525+
schemaProvider.addSchema(SCHEMA_ID, schema);
1526+
const content = '';
1527+
const completion = await parseSetup(content, 0, content.length);
1528+
1529+
expect(completion.items.length).equal(0);
1530+
});
1531+
13651532
it('should suggest enum based on type', async () => {
13661533
const schema: JSONSchema = {
13671534
type: 'object',

0 commit comments

Comments
 (0)