Skip to content

Commit 7d09ac6

Browse files
Merge branch 'main' into update-vscode-json-languageservice
2 parents 0b6b08d + cd20ba6 commit 7d09ac6

File tree

14 files changed

+282
-43
lines changed

14 files changed

+282
-43
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"url": "https://github.com/redhat-developer/yaml-language-server.git"
2727
},
2828
"dependencies": {
29-
"ajv": "^8.11.0",
29+
"ajv": "^8.17.1",
30+
"ajv-draft-04": "^1.0.0",
3031
"lodash": "4.17.21",
3132
"prettier": "^3.5.0",
3233
"request-light": "^0.5.7",

src/languageserver/handlers/settingsHandlers.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,12 @@ export class SettingsHandler {
163163
if (enableFormatter) {
164164
if (!this.yamlSettings.formatterRegistration) {
165165
this.yamlSettings.formatterRegistration = this.connection.client.register(DocumentFormattingRequest.type, {
166-
documentSelector: [{ language: 'yaml' }],
166+
documentSelector: [
167+
{ language: 'yaml' },
168+
{ language: 'dockercompose' },
169+
{ language: 'github-actions-workflow' },
170+
{ pattern: '*.y(a)ml' },
171+
],
167172
});
168173
}
169174
} else if (this.yamlSettings.formatterRegistration) {

src/languageservice/parser/ast-converter.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,18 @@ import {
3232

3333
type NodeRange = [number, number, number];
3434

35-
const maxRefCount = 1000;
36-
let refDepth = 0;
37-
const seenAlias = new Set<Alias>();
35+
// Exported for tests
36+
export const aliasDepth = {
37+
maxRefCount: 1000,
38+
currentRefDepth: 0,
39+
aliasResolutionCache: new Map<Alias, ASTNode>(),
40+
};
3841

3942
export function convertAST(parent: ASTNode, node: YamlNode, doc: Document, lineCounter: LineCounter): ASTNode | undefined {
4043
if (!parent) {
4144
// first invocation
42-
refDepth = 0;
45+
aliasDepth.currentRefDepth = 0;
46+
aliasDepth.aliasResolutionCache = new Map();
4347
}
4448

4549
if (!node) {
@@ -57,11 +61,8 @@ export function convertAST(parent: ASTNode, node: YamlNode, doc: Document, lineC
5761
if (isScalar(node)) {
5862
return convertScalar(node, parent);
5963
}
60-
if (isAlias(node) && !seenAlias.has(node) && refDepth < maxRefCount) {
61-
seenAlias.add(node);
62-
const converted = convertAlias(node, parent, doc, lineCounter);
63-
seenAlias.delete(node);
64-
return converted;
64+
if (isAlias(node) && aliasDepth.currentRefDepth < aliasDepth.maxRefCount) {
65+
return convertAlias(node, parent, doc, lineCounter);
6566
} else {
6667
return;
6768
}
@@ -154,15 +155,23 @@ function convertScalar(node: Scalar, parent: ASTNode): ASTNode {
154155
}
155156

156157
function convertAlias(node: Alias, parent: ASTNode, doc: Document, lineCounter: LineCounter): ASTNode {
157-
refDepth++;
158+
if (aliasDepth.aliasResolutionCache.has(node)) {
159+
return aliasDepth.aliasResolutionCache.get(node);
160+
}
161+
162+
aliasDepth.currentRefDepth++;
158163
const resolvedNode = node.resolve(doc);
164+
let ans: ASTNode;
159165
if (resolvedNode) {
160-
return convertAST(parent, resolvedNode, doc, lineCounter);
166+
ans = convertAST(parent, resolvedNode, doc, lineCounter);
161167
} else {
162168
const resultNode = new StringASTNodeImpl(parent, node, ...toOffsetLength(node.range));
163169
resultNode.value = node.source;
164-
return resultNode;
170+
ans = resultNode;
165171
}
172+
aliasDepth.currentRefDepth--;
173+
aliasDepth.aliasResolutionCache.set(node, ans);
174+
return ans;
166175
}
167176

168177
export function toOffsetLength(range: NodeRange): [number, number] {

src/languageservice/parser/jsonParser07.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@ function validate(
876876
const val = getNodeValue(node);
877877
let enumValueMatch = false;
878878
for (const e of schema.enum) {
879-
if (val === e || isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, e)) {
879+
if (equals(val, e, node.type) || isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, e)) {
880880
enumValueMatch = true;
881881
break;
882882
}
@@ -908,7 +908,7 @@ function validate(
908908

909909
if (isDefined(schema.const)) {
910910
const val = getNodeValue(node);
911-
if (!equals(val, schema.const) && !isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, schema.const)) {
911+
if (!equals(val, schema.const, node.type) && !isAutoCompleteEqualMaybe(callFromAutoComplete, node, val, schema.const)) {
912912
validationResult.problems.push({
913913
location: { offset: node.offset, length: node.length },
914914
severity: DiagnosticSeverity.Warning,

src/languageservice/services/validation/yaml-style.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export class YAMLStyleValidator implements AdditionalValidator {
4242
}
4343

4444
private getRangeOf(document: TextDocument, node: FlowCollection): Range {
45-
return Range.create(document.positionAt(node.start.offset), document.positionAt(node.end.pop().offset));
45+
const endOffset = node.end[0].offset;
46+
let endPosition = document.positionAt(endOffset);
47+
endPosition = { character: endPosition.character + 1, line: endPosition.line };
48+
return Range.create(document.positionAt(node.start.offset), endPosition);
4649
}
4750
}

src/languageservice/services/yamlHover.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { setKubernetesParserOption } from '../parser/isKubernetes';
1212
import { TextDocument } from 'vscode-languageserver-textdocument';
1313
import { yamlDocumentsCache } from '../parser/yaml-documents';
1414
import { SingleYAMLDocument } from '../parser/yamlParser07';
15-
import { IApplicableSchema } from '../parser/jsonParser07';
15+
import { getNodeValue, IApplicableSchema } from '../parser/jsonParser07';
1616
import { JSONSchema } from '../jsonSchema';
1717
import { URI } from 'vscode-uri';
1818
import * as path from 'path';
@@ -114,12 +114,13 @@ export class YAMLHover {
114114
let markdownEnumDescriptions: string[] = [];
115115
const markdownExamples: string[] = [];
116116
const markdownEnums: markdownEnum[] = [];
117-
117+
let enumIdx: number | undefined = undefined;
118118
matchingSchemas.every((s) => {
119119
if ((s.node === node || (node.type === 'property' && node.valueNode === s.node)) && !s.inverted && s.schema) {
120120
title = title || s.schema.title || s.schema.closestTitle;
121121
markdownDescription = markdownDescription || s.schema.markdownDescription || this.toMarkdown(s.schema.description);
122122
if (s.schema.enum) {
123+
enumIdx = s.schema.enum.indexOf(getNodeValue(node));
123124
if (s.schema.markdownEnumDescriptions) {
124125
markdownEnumDescriptions = s.schema.markdownEnumDescriptions;
125126
} else if (s.schema.enumDescriptions) {
@@ -174,6 +175,9 @@ export class YAMLHover {
174175
if (markdownEnums.length !== 0) {
175176
result = ensureLineBreak(result);
176177
result += 'Allowed Values:\n\n';
178+
if (enumIdx) {
179+
markdownEnums.unshift(markdownEnums.splice(enumIdx, 1)[0]);
180+
}
177181
markdownEnums.forEach((me) => {
178182
if (me.description) {
179183
result += `* \`${toMarkdownCodeBlock(me.value)}\`: ${me.description}\n`;

src/languageservice/services/yamlSchemaService.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ import { JSONSchemaDescriptionExt } from '../../requestTypes';
2727
import { SchemaVersions } from '../yamlTypes';
2828

2929
import Ajv, { DefinedError } from 'ajv';
30+
import Ajv4 from 'ajv-draft-04';
3031
import { getSchemaTitle } from '../utils/schemaUtils';
3132
import { SchemaConfiguration } from 'vscode-json-languageservice';
3233

3334
const ajv = new Ajv();
35+
const ajv4 = new Ajv4();
3436

3537
const localize = nls.loadMessageBundle();
3638

@@ -39,6 +41,11 @@ const localize = nls.loadMessageBundle();
3941
const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json');
4042
const schema07Validator = ajv.compile(jsonSchema07);
4143

44+
// eslint-disable-next-line @typescript-eslint/no-var-requires
45+
const jsonSchema04 = require('ajv-draft-04/dist/refs/json-schema-draft-04.json');
46+
const schema04Validator = ajv4.compile(jsonSchema04);
47+
const SCHEMA_04_URI_WITH_HTTPS = ajv4.defaultMeta().replace('http://', 'https://');
48+
4249
export declare type CustomSchemaProvider = (uri: string) => Promise<string | string[]>;
4350

4451
export enum MODIFICATION_ACTIONS {
@@ -162,9 +169,13 @@ export class YAMLSchemaService extends JSONSchemaService {
162169
let schema: JSONSchema = schemaToResolve.schema;
163170
const contextService = this.contextService;
164171

165-
if (!schema07Validator(schema)) {
172+
const validator =
173+
this.normalizeId(schema.$schema) === ajv4.defaultMeta() || this.normalizeId(schema.$schema) === SCHEMA_04_URI_WITH_HTTPS
174+
? schema04Validator
175+
: schema07Validator;
176+
if (!validator(schema)) {
166177
const errs: string[] = [];
167-
for (const err of schema07Validator.errors as DefinedError[]) {
178+
for (const err of validator.errors as DefinedError[]) {
168179
errs.push(`${err.instancePath} : ${err.message}`);
169180
}
170181
resolveErrors.push(`Schema '${getSchemaTitle(schemaToResolve.schema, schemaURL)}' is not valid:\n${errs.join('\n')}`);
@@ -643,7 +654,12 @@ export class YAMLSchemaService extends JSONSchemaService {
643654
const requestService = this.requestService;
644655
return super.loadSchema(schemaUri).then((unresolvedJsonSchema: UnresolvedSchema) => {
645656
// If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
646-
if (unresolvedJsonSchema.errors && unresolvedJsonSchema.schema === undefined) {
657+
// If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
658+
// contain a number instead of being undefined, so we need to check for that too.
659+
if (
660+
unresolvedJsonSchema.errors &&
661+
(unresolvedJsonSchema.schema === undefined || typeof unresolvedJsonSchema.schema === 'number')
662+
) {
647663
return requestService(schemaUri).then(
648664
(content) => {
649665
if (!content) {

src/languageservice/utils/objects.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
7-
export function equals(one: any, other: any): boolean {
7+
export function equals(one: any, other: any, type?: any): boolean {
8+
if (type === 'boolean') {
9+
one = JSON.parse(one);
10+
}
811
if (one === other) {
912
return true;
1013
}

test/hover.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,9 +617,9 @@ Source: [${SCHEMA_ID}](file:///${SCHEMA_ID})`
617617
618618
Allowed Values:
619619
620+
* \`non\`
620621
* \`cat\`
621622
* \`dog\`: Canis familiaris
622-
* \`non\`
623623
* \`bird\`
624624
* \`fish\`: Special fish
625625

test/schemaValidation.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,42 @@ describe('Validation Tests', () => {
189189
})
190190
.then(done, done);
191191
});
192+
193+
it('Test that boolean value can be used in enum', (done) => {
194+
schemaProvider.addSchema(SCHEMA_ID, {
195+
type: 'object',
196+
properties: {
197+
analytics: {
198+
enum: [true, false],
199+
},
200+
},
201+
});
202+
const content = 'analytics: true';
203+
const validator = parseSetup(content);
204+
validator
205+
.then(function (result) {
206+
assert.deepStrictEqual(result, []);
207+
})
208+
.then(done, done);
209+
});
210+
211+
it('Test that boolean value can be used in const', (done) => {
212+
schemaProvider.addSchema(SCHEMA_ID, {
213+
type: 'object',
214+
properties: {
215+
analytics: {
216+
const: true,
217+
},
218+
},
219+
});
220+
const content = 'analytics: true';
221+
const validator = parseSetup(content);
222+
validator
223+
.then(function (result) {
224+
assert.deepStrictEqual(result, []);
225+
})
226+
.then(done, done);
227+
});
192228
});
193229

194230
describe('String tests', () => {
@@ -2094,4 +2130,87 @@ obj:
20942130
const result = await parseSetup(content);
20952131
assert.equal(result.length, 0);
20962132
});
2133+
2134+
it('value should match as per schema const on boolean', async () => {
2135+
schemaProvider.addSchema(SCHEMA_ID, {
2136+
type: 'object',
2137+
properties: {
2138+
prop: {
2139+
const: true,
2140+
type: 'boolean',
2141+
},
2142+
},
2143+
});
2144+
2145+
let content = `prop: false`;
2146+
let result = await parseSetup(content);
2147+
expect(result.length).to.eq(1);
2148+
expect(result[0].message).to.eq('Value must be true.');
2149+
2150+
content = `prop: true`;
2151+
result = await parseSetup(content);
2152+
expect(result.length).to.eq(0);
2153+
});
2154+
2155+
it('draft-04 schema', async () => {
2156+
const schema: JSONSchema = {
2157+
$schema: 'http://json-schema.org/draft-04/schema#',
2158+
type: 'object',
2159+
properties: {
2160+
myProperty: {
2161+
$ref: '#/definitions/Interface%3Ctype%3E',
2162+
},
2163+
},
2164+
definitions: {
2165+
'Interface<type>': {
2166+
type: 'object',
2167+
properties: {
2168+
foo: {
2169+
type: 'string',
2170+
},
2171+
multipleOf: {
2172+
type: 'number',
2173+
minimum: 0,
2174+
exclusiveMinimum: true,
2175+
},
2176+
},
2177+
},
2178+
},
2179+
};
2180+
schemaProvider.addSchema(SCHEMA_ID, schema);
2181+
const content = `myProperty:\n foo: bar\n multipleOf: 1`;
2182+
const result = await parseSetup(content);
2183+
assert.equal(result.length, 0);
2184+
});
2185+
2186+
it('draft-04 schema with https in metaschema URI', async () => {
2187+
const schema: JSONSchema = {
2188+
$schema: 'https://json-schema.org/draft-04/schema#',
2189+
type: 'object',
2190+
properties: {
2191+
myProperty: {
2192+
$ref: '#/definitions/Interface%3Ctype%3E',
2193+
},
2194+
},
2195+
definitions: {
2196+
'Interface<type>': {
2197+
type: 'object',
2198+
properties: {
2199+
foo: {
2200+
type: 'string',
2201+
},
2202+
multipleOf: {
2203+
type: 'number',
2204+
minimum: 0,
2205+
exclusiveMinimum: true,
2206+
},
2207+
},
2208+
},
2209+
},
2210+
};
2211+
schemaProvider.addSchema(SCHEMA_ID, schema);
2212+
const content = `myProperty:\n foo: bar\n multipleOf: 1`;
2213+
const result = await parseSetup(content);
2214+
assert.equal(result.length, 0);
2215+
});
20972216
});

0 commit comments

Comments
 (0)