Skip to content

Commit 9629755

Browse files
Allow graceful matching by parameter and repair null values matching
1 parent 87e80ef commit 9629755

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
@@ -81,6 +81,7 @@ export const ProblemTypeMessages: Record<ProblemType, string> = {
8181
[ProblemType.typeMismatchWarning]: 'Incorrect type. Expected "{0}".',
8282
[ProblemType.constWarning]: 'Value must be {0}.',
8383
};
84+
8485
export interface IProblem {
8586
location: IRange;
8687
severity: DiagnosticSeverity;
@@ -158,6 +159,7 @@ export abstract class ASTNodeImpl {
158159
export class NullASTNodeImpl extends ASTNodeImpl implements NullASTNode {
159160
public type: 'null' = 'null' as const;
160161
public value = null;
162+
161163
constructor(parent: ASTNode, internalNode: Node, offset: number, length?: number) {
162164
super(parent, internalNode, offset, length);
163165
}
@@ -278,27 +280,36 @@ export enum EnumMatch {
278280

279281
export interface ISchemaCollector {
280282
schemas: IApplicableSchema[];
283+
281284
add(schema: IApplicableSchema): void;
285+
282286
merge(other: ISchemaCollector): void;
287+
283288
include(node: ASTNode): boolean;
289+
284290
newSub(): ISchemaCollector;
285291
}
286292

287293
class SchemaCollector implements ISchemaCollector {
288294
schemas: IApplicableSchema[] = [];
295+
289296
constructor(
290297
private focusOffset = -1,
291298
private exclude: ASTNode = null
292299
) {}
300+
293301
add(schema: IApplicableSchema): void {
294302
this.schemas.push(schema);
295303
}
304+
296305
merge(other: ISchemaCollector): void {
297306
this.schemas.push(...other.schemas);
298307
}
308+
299309
include(node: ASTNode): boolean {
300310
return (this.focusOffset === -1 || contains(node, this.focusOffset)) && node !== this.exclude;
301311
}
312+
302313
newSub(): ISchemaCollector {
303314
return new SchemaCollector(-1, this.exclude);
304315
}
@@ -308,22 +319,27 @@ class NoOpSchemaCollector implements ISchemaCollector {
308319
private constructor() {
309320
// ignore
310321
}
322+
311323
// eslint-disable-next-line @typescript-eslint/no-explicit-any
312324
get schemas(): any[] {
313325
return [];
314326
}
327+
315328
// eslint-disable-next-line @typescript-eslint/no-unused-vars
316329
add(schema: IApplicableSchema): void {
317330
// ignore
318331
}
332+
319333
// eslint-disable-next-line @typescript-eslint/no-unused-vars
320334
merge(other: ISchemaCollector): void {
321335
// ignore
322336
}
337+
323338
// eslint-disable-next-line @typescript-eslint/no-unused-vars
324339
include(node: ASTNode): boolean {
325340
return true;
326341
}
342+
327343
newSub(): ISchemaCollector {
328344
return this;
329345
}
@@ -616,13 +632,15 @@ export class JSONDocument {
616632
* @param focusOffset offsetValue
617633
* @param exclude excluded Node
618634
* @param didCallFromAutoComplete true if method called from AutoComplete
635+
* @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
619636
* @returns array of applicable schemas
620637
*/
621638
public getMatchingSchemas(
622639
schema: JSONSchema,
623640
focusOffset = -1,
624641
exclude: ASTNode = null,
625-
didCallFromAutoComplete?: boolean
642+
didCallFromAutoComplete?: boolean,
643+
gracefulMatches?: boolean
626644
): IApplicableSchema[] {
627645
const matchingSchemas = new SchemaCollector(focusOffset, exclude);
628646
if (this.root && schema) {
@@ -631,17 +649,21 @@ export class JSONDocument {
631649
disableAdditionalProperties: this.disableAdditionalProperties,
632650
uri: this.uri,
633651
callFromAutoComplete: didCallFromAutoComplete,
652+
gracefulMatches: gracefulMatches,
634653
});
635654
}
636655
return matchingSchemas.schemas;
637656
}
638657
}
658+
639659
interface Options {
640660
isKubernetes: boolean;
641661
disableAdditionalProperties: boolean;
642662
uri: string;
643663
callFromAutoComplete?: boolean;
664+
gracefulMatches?: boolean;
644665
}
666+
645667
function validate(
646668
node: ASTNode,
647669
schema: JSONSchema,
@@ -651,7 +673,7 @@ function validate(
651673
options: Options
652674
// eslint-disable-next-line @typescript-eslint/no-explicit-any
653675
): any {
654-
const { isKubernetes, callFromAutoComplete } = options;
676+
const { isKubernetes, callFromAutoComplete, gracefulMatches } = options;
655677
if (!node) {
656678
return;
657679
}
@@ -942,6 +964,7 @@ function validate(
942964
});
943965
}
944966
}
967+
945968
function getExclusiveLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined {
946969
if (isNumber(exclusive)) {
947970
return exclusive;
@@ -951,12 +974,14 @@ function validate(
951974
}
952975
return undefined;
953976
}
977+
954978
function getLimit(limit: number | undefined, exclusive: boolean | number | undefined): number | undefined {
955979
if (!isBoolean(exclusive) || !exclusive) {
956980
return limit;
957981
}
958982
return undefined;
959983
}
984+
960985
const exclusiveMinimum = getExclusiveLimit(schema.minimum, schema.exclusiveMinimum);
961986
if (isNumber(exclusiveMinimum) && val <= exclusiveMinimum) {
962987
validationResult.problems.push({
@@ -1086,6 +1111,7 @@ function validate(
10861111
}
10871112
}
10881113
}
1114+
10891115
function _validateArrayNode(
10901116
node: ArrayASTNode,
10911117
schema: JSONSchema,
@@ -1247,7 +1273,12 @@ function validate(
12471273
for (const propertyName of schema.required) {
12481274
if (seenKeys[propertyName] === undefined) {
12491275
const keyNode = node.parent && node.parent.type === 'property' && node.parent.keyNode;
1250-
const location = keyNode ? { offset: keyNode.offset, length: keyNode.length } : { offset: node.offset, length: 1 };
1276+
const location = keyNode
1277+
? { offset: keyNode.offset, length: keyNode.length }
1278+
: {
1279+
offset: node.offset,
1280+
length: 1,
1281+
};
12511282
validationResult.problems.push({
12521283
location: location,
12531284
severity: DiagnosticSeverity.Warning,
@@ -1490,10 +1521,14 @@ function validate(
14901521
return bestMatch;
14911522
}
14921523

1524+
function gracefulMatchFilter(maxOneMatch: boolean, propertiesValueMatches: number): boolean {
1525+
return gracefulMatches && !maxOneMatch && callFromAutoComplete && propertiesValueMatches > 0;
1526+
}
1527+
14931528
//genericComparison tries to find the best matching schema using a generic comparison
14941529
function genericComparison(
14951530
node: ASTNode,
1496-
maxOneMatch,
1531+
maxOneMatch: boolean,
14971532
subValidationResult: ValidationResult,
14981533
bestMatch: IValidationMatch,
14991534
subSchema,
@@ -1522,7 +1557,8 @@ function validate(
15221557
};
15231558
} else if (
15241559
compareResult === 0 ||
1525-
((node.value === null || node.type === 'null') && node.length === 0) // node with no value can match any schema potentially
1560+
((node.value === null || node.type === 'null') && node.length === 0) || // node with no value can match any schema potentially
1561+
gracefulMatchFilter(maxOneMatch, subValidationResult.propertiesValueMatches)
15261562
) {
15271563
// there's already a best matching but we are as good
15281564
mergeValidationMatches(bestMatch, subMatchingSchemas, subValidationResult);

src/languageservice/services/yamlCompletion.ts

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

102-
async doComplete(document: TextDocument, position: Position, isKubernetes = false, doComplete = true): Promise<CompletionList> {
102+
async doComplete(
103+
document: TextDocument,
104+
position: Position,
105+
isKubernetes = false,
106+
doComplete = true,
107+
gracefulMatches = false
108+
): Promise<CompletionList> {
103109
const result = CompletionList.create([], false);
104110
if (!this.completionEnabled) {
105111
return result;
@@ -534,7 +540,8 @@ export class YamlCompletion {
534540
collector,
535541
textBuffer,
536542
overwriteRange,
537-
doComplete
543+
doComplete,
544+
gracefulMatches
538545
);
539546

540547
if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') {
@@ -549,7 +556,7 @@ export class YamlCompletion {
549556

550557
// proposals for values
551558
const types: { [type: string]: boolean } = {};
552-
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete);
559+
this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete, gracefulMatches);
553560
} catch (err) {
554561
this.telemetry?.sendError('yaml.completion.error', err);
555562
}
@@ -689,9 +696,10 @@ export class YamlCompletion {
689696
collector: CompletionsCollector,
690697
textBuffer: TextBuffer,
691698
overwriteRange: Range,
692-
doComplete: boolean
699+
doComplete: boolean,
700+
gracefulMatches: boolean
693701
): void {
694-
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete);
702+
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete, gracefulMatches);
695703
const existingKey = textBuffer.getText(overwriteRange);
696704
const lineContent = textBuffer.getLineContent(overwriteRange.start.line);
697705
const hasOnlyWhitespace = lineContent.trim().length === 0;
@@ -891,7 +899,8 @@ export class YamlCompletion {
891899
document: TextDocument,
892900
collector: CompletionsCollector,
893901
types: { [type: string]: boolean },
894-
doComplete: boolean
902+
doComplete: boolean,
903+
gracefulMatches: boolean
895904
): void {
896905
let parentKey: string = null;
897906

@@ -915,7 +924,7 @@ export class YamlCompletion {
915924

916925
if (node && (parentKey !== null || isSeq(node))) {
917926
const separatorAfter = '';
918-
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete);
927+
const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete, gracefulMatches);
919928
for (const s of matchingSchemas) {
920929
if (s.node.internalNode === node && !s.inverted && s.schema) {
921930
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)