diff --git a/src/languageservice/jsonSchema.ts b/src/languageservice/jsonSchema.ts index 4c4fcd7f..4385fb2e 100644 --- a/src/languageservice/jsonSchema.ts +++ b/src/languageservice/jsonSchema.ts @@ -36,6 +36,7 @@ export interface JSONSchema { multipleOf?: number; required?: string[]; $ref?: string; + _$ref?: string; anyOf?: JSONSchemaRef[]; allOf?: JSONSchemaRef[]; oneOf?: JSONSchemaRef[]; diff --git a/src/languageservice/parser/jsonParser07.ts b/src/languageservice/parser/jsonParser07.ts index 8691d27d..55ad2b93 100644 --- a/src/languageservice/parser/jsonParser07.ts +++ b/src/languageservice/parser/jsonParser07.ts @@ -7,6 +7,7 @@ import * as Json from 'jsonc-parser'; import { JSONSchema, JSONSchemaRef } from '../jsonSchema'; import { isNumber, equals, isString, isDefined, isBoolean } from '../utils/objects'; +import { getSchemaTypeName } from '../utils/schemaUtils'; import { ASTNode, ObjectASTNode, @@ -23,6 +24,8 @@ import { URI } from 'vscode-uri'; import { DiagnosticSeverity, Range } from 'vscode-languageserver-types'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { Diagnostic } from 'vscode-languageserver'; +import { MissingRequiredPropWarning, TypeMismatchWarning, ConstWarning } from '../../../test/utils/errorMessages'; +import { isArrayEqual } from '../utils/arrUtils'; const localize = nls.loadMessageBundle(); @@ -55,18 +58,32 @@ const formats = { }; export const YAML_SOURCE = 'YAML'; +const YAML_SCHEMA_PREFIX = 'yaml-schema: '; +export enum ProblemType { + missingRequiredPropWarning = 'missingRequiredPropWarning', + typeMismatchWarning = 'typeMismatchWarning', + constWarning = 'constWarning', +} + +const ProblemTypeMessages: Record = { + [ProblemType.missingRequiredPropWarning]: MissingRequiredPropWarning, + [ProblemType.typeMismatchWarning]: TypeMismatchWarning, + [ProblemType.constWarning]: ConstWarning, +}; export interface IProblem { location: IRange; severity: DiagnosticSeverity; code?: ErrorCode; message: string; source?: string; - schemaUri?: string; + problemType?: ProblemType; + problemArgs?: string[]; + schemaUri?: string[]; } interface DiagnosticExt extends Diagnostic { - schemaUri?: string; + schemaUri?: string[]; } export abstract class ASTNodeImpl { @@ -345,6 +362,35 @@ export class ValidationResult { } } + /** + * Merge multiple warnings with same problemType together + * @param subValidationResult another possible result + */ + public mergeWarningGeneric(subValidationResult: ValidationResult, problemTypesToMerge: ProblemType[]): void { + if (this.problems?.length) { + for (const problemType of problemTypesToMerge) { + const bestResults = this.problems.filter((p) => p.problemType === problemType); + for (const bestResult of bestResults) { + const mergingResult = subValidationResult.problems?.find( + (p) => + p.problemType === problemType && + bestResult.location.offset === p.location.offset && + (problemType !== ProblemType.missingRequiredPropWarning || isArrayEqual(p.problemArgs, bestResult.problemArgs)) // missingProp is merged only with same problemArg + ); + if (mergingResult) { + if (mergingResult.problemArgs.length) { + mergingResult.problemArgs + .filter((p) => !bestResult.problemArgs.includes(p)) + .forEach((p) => bestResult.problemArgs.push(p)); + bestResult.message = getWarningMessage(bestResult.problemType, bestResult.problemArgs); + } + this.mergeSources(mergingResult, bestResult); + } + } + } + } + } + public mergePropertyMatch(propertyValidationResult: ValidationResult): void { this.merge(propertyValidationResult); this.propertiesMatches++; @@ -359,6 +405,16 @@ export class ValidationResult { } } + private mergeSources(mergingResult: IProblem, bestResult: IProblem): void { + const mergingSource = mergingResult.source.replace(YAML_SCHEMA_PREFIX, ''); + if (!bestResult.source.includes(mergingSource)) { + bestResult.source = bestResult.source + ' | ' + mergingSource; + } + if (!bestResult.schemaUri.includes(mergingResult.schemaUri[0])) { + bestResult.schemaUri = bestResult.schemaUri.concat(mergingResult.schemaUri); + } + } + public compareGeneric(other: ValidationResult): number { const hasProblems = this.hasProblems(); if (hasProblems !== other.hasProblems()) { @@ -537,12 +593,16 @@ function validate( } } else if (schema.type) { if (!matchesType(schema.type)) { + //get more specific name than just object + const schemaType = schema.type === 'object' ? getSchemaTypeName(schema) : schema.type; validationResult.problems.push({ location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, - message: schema.errorMessage || localize('typeMismatchWarning', 'Incorrect type. Expected "{0}".', schema.type), + message: schema.errorMessage || getWarningMessage(ProblemType.typeMismatchWarning, [schemaType]), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), + problemType: ProblemType.typeMismatchWarning, + problemArgs: [schemaType], }); } } @@ -704,9 +764,11 @@ function validate( location: { offset: node.offset, length: node.length }, severity: DiagnosticSeverity.Warning, code: ErrorCode.EnumValueMismatch, - message: schema.errorMessage || localize('constWarning', 'Value must be {0}.', JSON.stringify(schema.const)), + problemType: ProblemType.constWarning, + message: schema.errorMessage || getWarningMessage(ProblemType.constWarning, [JSON.stringify(schema.const)]), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), + problemArgs: [JSON.stringify(schema.const)], }); validationResult.enumValueMatch = false; } else { @@ -1050,9 +1112,11 @@ function validate( validationResult.problems.push({ location: location, severity: DiagnosticSeverity.Warning, - message: localize('MissingRequiredPropWarning', 'Missing property "{0}".', propertyName), + message: getWarningMessage(ProblemType.missingRequiredPropWarning, [propertyName]), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), + problemArgs: [propertyName], + problemType: ProblemType.missingRequiredPropWarning, }); } } @@ -1277,8 +1341,21 @@ function validate( } //genericComparison tries to find the best matching schema using a generic comparison - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas): any { + function genericComparison( + maxOneMatch, + subValidationResult: ValidationResult, + bestMatch: { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + }, + subSchema, + subMatchingSchemas + ): { + schema: JSONSchema; + validationResult: ValidationResult; + matchingSchemas: ISchemaCollector; + } { if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) { // no errors, both are equally good matches bestMatch.matchingSchemas.merge(subMatchingSchemas); @@ -1297,6 +1374,11 @@ function validate( // there's already a best matching but we are as good bestMatch.matchingSchemas.merge(subMatchingSchemas); bestMatch.validationResult.mergeEnumValues(subValidationResult); + bestMatch.validationResult.mergeWarningGeneric(subValidationResult, [ + ProblemType.missingRequiredPropWarning, + ProblemType.typeMismatchWarning, + ProblemType.constWarning, + ]); } } return bestMatch; @@ -1321,14 +1403,18 @@ function getSchemaSource(schema: JSONSchema, originalSchema: JSONSchema): string } } if (label) { - return `yaml-schema: ${label}`; + return `${YAML_SCHEMA_PREFIX}${label}`; } } return YAML_SOURCE; } -function getSchemaUri(schema: JSONSchema, originalSchema: JSONSchema): string | undefined { +function getSchemaUri(schema: JSONSchema, originalSchema: JSONSchema): string[] { const uriString = schema.url ?? originalSchema.url; - return uriString; + return uriString ? [uriString] : []; +} + +function getWarningMessage(problemType: ProblemType, args: string[]): string { + return localize(problemType, ProblemTypeMessages[problemType], args.join(' | ')); } diff --git a/src/languageservice/services/yamlCodeActions.ts b/src/languageservice/services/yamlCodeActions.ts index 663ee925..3c63914b 100644 --- a/src/languageservice/services/yamlCodeActions.ts +++ b/src/languageservice/services/yamlCodeActions.ts @@ -6,10 +6,11 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; import { ClientCapabilities, CodeAction, CodeActionParams, Command, Connection, Diagnostic } from 'vscode-languageserver'; import { YamlCommands } from '../../commands'; +import * as path from 'path'; import { CommandExecutor } from '../../languageserver/commandExecutor'; interface YamlDiagnosticData { - schemaUri: string; + schemaUri: string[]; } export class YamlCodeActions { constructor(commandExecutor: CommandExecutor, connection: Connection, private readonly clientCapabilities: ClientCapabilities) { @@ -47,18 +48,20 @@ export class YamlCodeActions { } const schemaUriToDiagnostic = new Map(); for (const diagnostic of diagnostics) { - const schemaUri = (diagnostic.data as YamlDiagnosticData)?.schemaUri; - if (schemaUri && (schemaUri.startsWith('file') || schemaUri.startsWith('https'))) { - if (!schemaUriToDiagnostic.has(schemaUri)) { - schemaUriToDiagnostic.set(schemaUri, []); + const schemaUri = (diagnostic.data as YamlDiagnosticData)?.schemaUri || []; + for (const schemaUriStr of schemaUri) { + if (schemaUriStr && (schemaUriStr.startsWith('file') || schemaUriStr.startsWith('https'))) { + if (!schemaUriToDiagnostic.has(schemaUriStr)) { + schemaUriToDiagnostic.set(schemaUriStr, []); + } + schemaUriToDiagnostic.get(schemaUriStr).push(diagnostic); } - schemaUriToDiagnostic.get(schemaUri).push(diagnostic); } } const result = []; for (const schemaUri of schemaUriToDiagnostic.keys()) { const action = CodeAction.create( - 'Jump to schema location', + `Jump to schema location (${path.basename(schemaUri)})`, Command.create('JumpToSchema', YamlCommands.JUMP_TO_SCHEMA, schemaUri) ); action.diagnostics = schemaUriToDiagnostic.get(schemaUri); diff --git a/src/languageservice/services/yamlSchemaService.ts b/src/languageservice/services/yamlSchemaService.ts index 28ca9985..06aae076 100644 --- a/src/languageservice/services/yamlSchemaService.ts +++ b/src/languageservice/services/yamlSchemaService.ts @@ -228,6 +228,8 @@ export class YAMLSchemaService extends JSONSchemaService { while (next.$ref) { const ref = next.$ref; const segments = ref.split('#', 2); + //return back removed $ref. We lost info about referenced type without it. + next._$ref = next.$ref; delete next.$ref; if (segments[0].length > 0) { openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies)); diff --git a/src/languageservice/utils/arrUtils.ts b/src/languageservice/utils/arrUtils.ts index 61ce8ff6..7c0adacf 100644 --- a/src/languageservice/utils/arrUtils.ts +++ b/src/languageservice/utils/arrUtils.ts @@ -72,3 +72,17 @@ export function filterInvalidCustomTags(customTags: string[]): string[] { return false; }); } +export function isArrayEqual(fst: Array, snd: Array): boolean { + if (!snd) { + return false; + } + if (snd.length !== fst.length) { + return false; + } + for (let index = fst.length - 1; index >= 0; index--) { + if (fst[index] !== snd[index]) { + return false; + } + } + return true; +} diff --git a/src/languageservice/utils/schemaUtils.ts b/src/languageservice/utils/schemaUtils.ts new file mode 100644 index 00000000..fbb66d2a --- /dev/null +++ b/src/languageservice/utils/schemaUtils.ts @@ -0,0 +1,37 @@ +import { JSONSchema } from '../jsonSchema'; + +export function getSchemaTypeName(schema: JSONSchema): string { + if (schema.$id) { + const type = getSchemaRefTypeTitle(schema.$id); + return type; + } + if (schema.$ref || schema._$ref) { + const type = getSchemaRefTypeTitle(schema.$ref || schema._$ref); + return type; + } + const typeStr = schema.title || (Array.isArray(schema.type) ? schema.type.join(' | ') : schema.type); //object + return typeStr; +} + +/** + * Get type name from reference url + * @param $ref reference to the same file OR to the another component OR to the section in another component: + * `schema-name.schema.json` -> schema-name + * `custom-scheme://shared-schema.json#/definitions/SomeType` -> SomeType + * `custom-scheme://schema-name.schema.json` -> schema-name + * `shared-schema.schema.json#/definitions/SomeType` -> SomeType + * `file:///Users/user/Documents/project/schemas/schema-name.schema.json` -> schema-name + * `#/definitions/SomeType` -> SomeType + * `#/definitions/io.k8s.api.apps.v1.DaemonSetSpec` => io.k8s.api.apps.v1.DaemonSetSpec + * `file:///default_schema_id.yaml` => default_schema_id.yaml + * test: https://regex101.com/r/ZpuXxk/1 + */ +export function getSchemaRefTypeTitle($ref: string): string { + const match = $ref.match(/^(?:.*\/)?(.*?)(?:\.schema\.json)?$/); + let type = !!match && match[1]; + if (!type) { + type = 'typeNotFound'; + console.error(`$ref (${$ref}) not parsed properly`); + } + return type; +} diff --git a/test/fixtures/testMultipleSimilarSchema.json b/test/fixtures/testMultipleSimilarSchema.json new file mode 100644 index 00000000..7e56a60f --- /dev/null +++ b/test/fixtures/testMultipleSimilarSchema.json @@ -0,0 +1,79 @@ +{ + "sharedSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "type1": { + "properties": { + "objA": { + "type": "object" + }, + "propA": { + "type": "string" + }, + "constA": { + "type": "string", + "const": "constForType1" + } + }, + "required": [ + "objA", + "propA", + "constA" + ], + "type": "object" + } + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "type2": { + "properties": { + "obj2": { + "type": "object" + } + }, + "required": [ + "obj2" + ], + "type": "object" + }, + "type3": { + "properties": { + "objA": { + "type": "object" + }, + "propA": { + "type": "string" + }, + "constA": { + "type": "string", + "const": "constForType3" + } + }, + "required": [ + "objA", + "propA", + "constA" + ], + "type": "object" + } + }, + "properties": { + "test_anyOf_objects": { + "anyOf": [ + { + "$ref": "sharedSchema.json#/definitions/type1" + }, + { + "$ref": "#/definitions/type2" + }, + { + "$ref": "#/definitions/type3" + } + ] + } + }, + "type": "object" + } +} \ No newline at end of file diff --git a/test/schema.test.ts b/test/schema.test.ts index ff99d124..ef3f09e1 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -81,6 +81,7 @@ describe('JSON Schema', () => { id: 'https://myschemastore/child', type: 'bool', description: 'Test description', + _$ref: 'https://myschemastore/child', url: 'https://myschemastore/child', }); }) @@ -128,6 +129,7 @@ describe('JSON Schema', () => { type: 'object', required: ['$ref'], properties: { $ref: { type: 'string' } }, + _$ref: '#/definitions/jsonReference', }); }) .then( @@ -177,16 +179,19 @@ describe('JSON Schema', () => { assert.deepEqual(fs.schema.properties['p1'], { type: 'string', enum: ['object'], + _$ref: 'schema2.json#/definitions/hello', url: 'https://myschemastore/main/schema2.json', }); assert.deepEqual(fs.schema.properties['p2'], { type: 'string', enum: ['object'], + _$ref: './schema2.json#/definitions/hello', url: 'https://myschemastore/main/schema2.json', }); assert.deepEqual(fs.schema.properties['p3'], { type: 'string', enum: ['object'], + _$ref: '/main/schema2.json#/definitions/hello', url: 'https://myschemastore/main/schema2.json', }); }) diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index 19ac50f8..127183d8 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -17,12 +17,14 @@ import { propertyIsNotAllowed, } from './utils/errorMessages'; import * as assert from 'assert'; +import * as path from 'path'; import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver'; import { expect } from 'chai'; import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings'; import { ValidationHandler } from '../src/languageserver/handlers/validationHandlers'; import { LanguageService } from '../src/languageservice/yamlLanguageService'; import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls'; +import { IProblem } from '../src/languageservice/parser/jsonParser07'; describe('Validation Tests', () => { let languageSettingsSetup: ServiceSetup; @@ -969,6 +971,11 @@ describe('Validation Tests', () => { }); describe('Multiple schema for single file', () => { + after(() => { + // remove Kubernetes setting not to affect next tests + languageService.configure(languageSettingsSetup.withKubernetes(false).languageSettings); + yamlSettings.specificValidatorPaths = []; + }); it('should add proper source to diagnostic', async () => { const content = ` abandoned: v1 @@ -1107,4 +1114,88 @@ describe('Validation Tests', () => { expect(result[0].message).to.eq('String is not a URI: URI expected.'); }); }); + + describe('Multiple similar schemas validation', () => { + const sharedSchemaId = 'sharedSchema.json'; + before(() => { + // remove Kubernetes setting set by previous test + languageService.configure(languageSettingsSetup.withKubernetes(false).languageSettings); + yamlSettings.specificValidatorPaths = []; + }); + afterEach(() => { + languageService.deleteSchema(sharedSchemaId); + }); + it('should distinguish types in error "Incorrect type (Expected "type1 | type2 | type3")"', async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); + + languageService.addSchema(sharedSchemaId, schema.sharedSchema); + languageService.addSchema(SCHEMA_ID, schema.schema); + const content = 'test_anyOf_objects:\n '; + const result = await parseSetup(content); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].message, 'Incorrect type. Expected "type1 | type2 | type3".'); + assert.strictEqual(result[0].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); + assert.deepStrictEqual((result[0].data as IProblem).schemaUri, [ + 'file:///sharedSchema.json', + 'file:///default_schema_id.yaml', + ]); + }); + it('should combine types in "Incorrect type error"', async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); + + languageService.addSchema(sharedSchemaId, schema.sharedSchema); + languageService.addSchema(SCHEMA_ID, schema.schema); + const content = 'test_anyOf_objects:\n propA:'; + const result = await parseSetup(content); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[2].message, 'Incorrect type. Expected "string".'); + assert.strictEqual(result[2].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); + }); + it('should combine const value', async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); + + languageService.addSchema(sharedSchemaId, schema.sharedSchema); + languageService.addSchema(SCHEMA_ID, schema.schema); + const content = 'test_anyOf_objects:\n constA:'; + const result = await parseSetup(content); + + assert.strictEqual(result.length, 4); + assert.strictEqual(result[3].message, 'Value must be "constForType1" | "constForType3".'); + assert.strictEqual(result[3].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); + }); + it('should distinguish types in error: "Missing property from multiple schemas"', async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); + + languageService.addSchema(sharedSchemaId, schema.sharedSchema); + languageService.addSchema(SCHEMA_ID, schema.schema); + const content = 'test_anyOf_objects:\n someProp:'; + const result = await parseSetup(content); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].message, 'Missing property "objA".'); + assert.strictEqual(result[0].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); + assert.deepStrictEqual((result[0].data as IProblem).schemaUri, [ + 'file:///sharedSchema.json', + 'file:///default_schema_id.yaml', + ]); + assert.strictEqual(result[1].message, 'Missing property "propA".'); + assert.strictEqual(result[1].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); + assert.deepStrictEqual((result[1].data as IProblem).schemaUri, [ + 'file:///sharedSchema.json', + 'file:///default_schema_id.yaml', + ]); + assert.strictEqual(result[2].message, 'Missing property "constA".'); + assert.strictEqual(result[2].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); + assert.deepStrictEqual((result[2].data as IProblem).schemaUri, [ + 'file:///sharedSchema.json', + 'file:///default_schema_id.yaml', + ]); + }); + }); }); diff --git a/test/utils/errorMessages.ts b/test/utils/errorMessages.ts index d892f5e6..f96d8665 100644 --- a/test/utils/errorMessages.ts +++ b/test/utils/errorMessages.ts @@ -15,6 +15,9 @@ export const NumberTypeError = 'Incorrect type. Expected "number".'; export const BooleanTypeError = 'Incorrect type. Expected "boolean".'; export const ArrayTypeError = 'Incorrect type. Expected "array".'; export const ObjectTypeError = 'Incorrect type. Expected "object".'; +export const TypeMismatchWarning = 'Incorrect type. Expected "{0}".'; +export const MissingRequiredPropWarning = 'Missing property "{0}".'; +export const ConstWarning = 'Value must be {0}.'; export function propertyIsNotAllowed(name: string): string { return `Property ${name} is not allowed.`; diff --git a/test/utils/serviceSetup.ts b/test/utils/serviceSetup.ts index 749b8b26..99e1d5c9 100644 --- a/test/utils/serviceSetup.ts +++ b/test/utils/serviceSetup.ts @@ -40,8 +40,8 @@ export class ServiceSetup { return this; } - withKubernetes(): ServiceSetup { - this.languageSettings.isKubernetes = true; + withKubernetes(allow = true): ServiceSetup { + this.languageSettings.isKubernetes = allow; return this; } diff --git a/test/utils/verifyError.ts b/test/utils/verifyError.ts index 806dafd5..f7054806 100644 --- a/test/utils/verifyError.ts +++ b/test/utils/verifyError.ts @@ -26,10 +26,10 @@ export function createDiagnosticWithData( endCharacter: number, severity: DiagnosticSeverity = 1, source = 'YAML', - schemaUri: string + schemaUri: string | string[] ): Diagnostic { const diagnostic: Diagnostic = createExpectedError(message, startLine, startCharacter, endLine, endCharacter, severity, source); - diagnostic.data = { schemaUri }; + diagnostic.data = { schemaUri: typeof schemaUri === 'string' ? [schemaUri] : schemaUri }; return diagnostic; } diff --git a/test/yamlCodeActions.test.ts b/test/yamlCodeActions.test.ts index bb92af1d..06f334e2 100644 --- a/test/yamlCodeActions.test.ts +++ b/test/yamlCodeActions.test.ts @@ -25,6 +25,7 @@ const expect = chai.expect; chai.use(sinonChai); const JSON_SCHEMA_LOCAL = 'file://some/path/schema.json'; +const JSON_SCHEMA2_LOCAL = 'file://some/path/schema2.json'; describe('CodeActions Tests', () => { const sandbox = sinon.createSandbox(); @@ -85,11 +86,39 @@ describe('CodeActions Tests', () => { const result = actions.getCodeAction(doc, params); const codeAction = CodeAction.create( - 'Jump to schema location', + 'Jump to schema location (schema.json)', Command.create('JumpToSchema', YamlCommands.JUMP_TO_SCHEMA, JSON_SCHEMA_LOCAL) ); codeAction.diagnostics = diagnostics; expect(result[0]).to.deep.equal(codeAction); }); + + it('should provide multiple action if diagnostic has uri for multiple schemas', () => { + const doc = setupTextDocument(''); + const diagnostics = [ + createDiagnosticWithData('foo', 0, 0, 0, 0, 1, JSON_SCHEMA_LOCAL, [JSON_SCHEMA_LOCAL, JSON_SCHEMA2_LOCAL]), + ]; + const params: CodeActionParams = { + context: CodeActionContext.create(diagnostics), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + clientCapabilities.window = { showDocument: { support: true } }; + const actions = new YamlCodeActions(commandExecutor, ({} as unknown) as Connection, clientCapabilities); + const result = actions.getCodeAction(doc, params); + + const codeAction = CodeAction.create( + 'Jump to schema location (schema.json)', + Command.create('JumpToSchema', YamlCommands.JUMP_TO_SCHEMA, JSON_SCHEMA_LOCAL) + ); + const codeAction2 = CodeAction.create( + 'Jump to schema location (schema2.json)', + Command.create('JumpToSchema', YamlCommands.JUMP_TO_SCHEMA, JSON_SCHEMA2_LOCAL) + ); + codeAction.diagnostics = diagnostics; + codeAction2.diagnostics = diagnostics; + expect(result[0]).to.deep.equal(codeAction); + expect(result[1]).to.deep.equal(codeAction2); + }); }); });