Skip to content

Commit 990e441

Browse files
Allow graceful matching by parameter and repair null values matching
1 parent de78c0e commit 990e441

File tree

5 files changed

+140
-20
lines changed

5 files changed

+140
-20
lines changed

src/languageserver/handlers/languageHandlers.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class LanguageHandlers {
156156
* Called when auto-complete is triggered in an editor
157157
* Returns a list of valid completion items
158158
*/
159-
completionHandler(textDocumentPosition: TextDocumentPositionParams): Promise<CompletionList> {
159+
completionHandler(textDocumentPosition: TextDocumentPositionParams, gracefulMatches = false): Promise<CompletionList> {
160160
const textDocument = this.yamlSettings.documents.get(textDocumentPosition.textDocument.uri);
161161

162162
const result: CompletionList = {
@@ -170,7 +170,9 @@ export class LanguageHandlers {
170170
return this.languageService.doComplete(
171171
textDocument,
172172
textDocumentPosition.position,
173-
isKubernetesAssociatedDocument(textDocument, this.yamlSettings.specificValidatorPaths)
173+
isKubernetesAssociatedDocument(textDocument, this.yamlSettings.specificValidatorPaths),
174+
true,
175+
gracefulMatches
174176
);
175177
}
176178

src/languageservice/parser/jsonParser07.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
8788
export interface IProblem {
8889
location: IRange;
8990
severity: DiagnosticSeverity;
@@ -161,6 +162,7 @@ export abstract class ASTNodeImpl {
161162
export 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

282284
export 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

290296
class 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+
643663
interface Options {
644664
isKubernetes: boolean;
645665
disableAdditionalProperties: boolean;
646666
uri: string;
647667
callFromAutoComplete?: boolean;
668+
gracefulMatches?: boolean;
648669
}
670+
649671
function 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);

src/languageservice/services/yamlCompletion.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,13 @@ export class YamlCompletion {
101101
this.parentSkeletonSelectedFirst = languageSettings.parentSkeletonSelectedFirst;
102102
}
103103

104-
async doComplete(document: TextDocument, position: Position, isKubernetes = false, doComplete = true): Promise<CompletionList> {
104+
async doComplete(
105+
document: TextDocument,
106+
position: Position,
107+
isKubernetes = false,
108+
doComplete = true,
109+
gracefulMatches = false
110+
): Promise<CompletionList> {
105111
const result = CompletionList.create([], false);
106112
if (!this.completionEnabled) {
107113
return result;
@@ -536,7 +542,8 @@ export class YamlCompletion {
536542
collector,
537543
textBuffer,
538544
overwriteRange,
539-
doComplete
545+
doComplete,
546+
gracefulMatches
540547
);
541548

542549
if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') {
@@ -551,7 +558,7 @@ export class YamlCompletion {
551558

552559
// proposals for values
553560
const types: { [type: string]: boolean } = {};
554-
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete);
561+
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete, gracefulMatches);
555562
} catch (err) {
556563
this.telemetry?.sendError('yaml.completion.error', err);
557564
}
@@ -691,9 +698,10 @@ export class YamlCompletion {
691698
collector: CompletionsCollector,
692699
textBuffer: TextBuffer,
693700
overwriteRange: Range,
694-
doComplete: boolean
701+
doComplete: boolean,
702+
gracefulMatches: boolean
695703
): void {
696-
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete);
704+
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete, gracefulMatches);
697705
const existingKey = textBuffer.getText(overwriteRange);
698706
const lineContent = textBuffer.getLineContent(overwriteRange.start.line);
699707
const hasOnlyWhitespace = lineContent.trim().length === 0;
@@ -893,7 +901,8 @@ export class YamlCompletion {
893901
document: TextDocument,
894902
collector: CompletionsCollector,
895903
types: { [type: string]: boolean },
896-
doComplete: boolean
904+
doComplete: boolean,
905+
gracefulMatches: boolean
897906
): void {
898907
let parentKey: string = null;
899908

@@ -917,7 +926,7 @@ export class YamlCompletion {
917926

918927
if (node && (parentKey !== null || isSeq(node))) {
919928
const separatorAfter = '';
920-
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete);
929+
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete, gracefulMatches);
921930
for (const s of matchingSchemas) {
922931
if (s.node.internalNode === node && !s.inverted && s.schema) {
923932
if (s.schema.items) {

src/languageservice/yamlLanguageService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,13 @@ export interface CustomFormatterOptions {
160160
export interface LanguageService {
161161
configure: (settings: LanguageSettings) => void;
162162
registerCustomSchemaProvider: (schemaProvider: CustomSchemaProvider) => void;
163-
doComplete: (document: TextDocument, position: Position, isKubernetes: boolean) => Promise<CompletionList>;
163+
doComplete: (
164+
document: TextDocument,
165+
position: Position,
166+
isKubernetes: boolean,
167+
doComplete?: boolean,
168+
gracefulMatches?: boolean
169+
) => Promise<CompletionList>;
164170
doValidation: (document: TextDocument, isKubernetes: boolean) => Promise<Diagnostic[]>;
165171
doHover: (document: TextDocument, position: Position) => Promise<Hover | null>;
166172
findDocumentSymbols: (document: TextDocument, context?: DocumentSymbolsContext) => SymbolInformation[];

0 commit comments

Comments
 (0)