diff --git a/src/languageservice/parser/jsonParser07.ts b/src/languageservice/parser/jsonParser07.ts index b4fdfe4b..c17f4352 100644 --- a/src/languageservice/parser/jsonParser07.ts +++ b/src/languageservice/parser/jsonParser07.ts @@ -888,6 +888,7 @@ function validate( ), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), + data: { values: schema.enum }, }); } } @@ -907,6 +908,7 @@ function validate( source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), problemArgs: [JSON.stringify(schema.const)], + data: { values: [schema.const] }, }); validationResult.enumValueMatch = false; } else { @@ -1385,6 +1387,7 @@ function validate( length: propertyNode.keyNode.length, }, severity: DiagnosticSeverity.Warning, + code: ErrorCode.PropertyExpected, message: schema.errorMessage || localize('DisallowedExtraPropWarning', MSG_PROPERTY_NOT_ALLOWED, propertyName), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), diff --git a/src/languageservice/services/yamlCodeActions.ts b/src/languageservice/services/yamlCodeActions.ts index c0ef20b5..da0c7eea 100644 --- a/src/languageservice/services/yamlCodeActions.ts +++ b/src/languageservice/services/yamlCodeActions.ts @@ -28,9 +28,12 @@ import { FlowStyleRewriter } from '../utils/flow-style-rewriter'; import { ASTNode } from '../jsonASTTypes'; import * as _ from 'lodash'; import { SourceToken } from 'yaml/dist/parse/cst'; +import { ErrorCode } from 'vscode-json-languageservice'; interface YamlDiagnosticData { schemaUri: string[]; + values?: string[]; + properties?: string[]; } export class YamlCodeActions { private indentation = ' '; @@ -54,6 +57,7 @@ export class YamlCodeActions { result.push(...this.getUnusedAnchorsDelete(params.context.diagnostics, document)); result.push(...this.getConvertToBlockStyleActions(params.context.diagnostics, document)); result.push(...this.getKeyOrderActions(params.context.diagnostics, document)); + result.push(...this.getQuickFixForPropertyOrValueMismatch(params.context.diagnostics, document)); return result; } @@ -221,7 +225,7 @@ export class YamlCodeActions { const results: CodeAction[] = []; for (const diagnostic of diagnostics) { if (diagnostic.code === 'flowMap' || diagnostic.code === 'flowSeq') { - const node = getNodeforDiagnostic(document, diagnostic); + const node = getNodeForDiagnostic(document, diagnostic); if (isMap(node.internalNode) || isSeq(node.internalNode)) { const blockTypeDescription = isMap(node.internalNode) ? 'map' : 'sequence'; const rewriter = new FlowStyleRewriter(this.indentation); @@ -242,7 +246,7 @@ export class YamlCodeActions { const results: CodeAction[] = []; for (const diagnostic of diagnostics) { if (diagnostic?.code === 'mapKeyOrder') { - let node = getNodeforDiagnostic(document, diagnostic); + let node = getNodeForDiagnostic(document, diagnostic); while (node && node.type !== 'object') { node = node.parent; } @@ -292,8 +296,8 @@ export class YamlCodeActions { item.value.end.splice(newLineIndex, 1); } } else if (item.value?.type === 'block-scalar') { - const nwline = item.value.props.find((p) => p.type === 'newline'); - if (!nwline) { + const newline = item.value.props.find((p) => p.type === 'newline'); + if (!newline) { item.value.props.push({ type: 'newline', indent: 0, offset: item.value.offset, source: '\n' } as SourceToken); } } @@ -312,9 +316,52 @@ export class YamlCodeActions { } return results; } + + /** + * Check if diagnostic contains info for quick fix + * Supports Enum/Const/Property mismatch + */ + private getPossibleQuickFixValues(diagnostic: Diagnostic): string[] | undefined { + if (typeof diagnostic.data !== 'object') { + return; + } + if ( + diagnostic.code === ErrorCode.EnumValueMismatch && + 'values' in diagnostic.data && + Array.isArray((diagnostic.data as YamlDiagnosticData).values) + ) { + return (diagnostic.data as YamlDiagnosticData).values; + } else if ( + diagnostic.code === ErrorCode.PropertyExpected && + 'properties' in diagnostic.data && + Array.isArray((diagnostic.data as YamlDiagnosticData).properties) + ) { + return (diagnostic.data as YamlDiagnosticData).properties; + } + } + + private getQuickFixForPropertyOrValueMismatch(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] { + const results: CodeAction[] = []; + for (const diagnostic of diagnostics) { + const values = this.getPossibleQuickFixValues(diagnostic); + if (!values?.length) { + continue; + } + for (const value of values) { + results.push( + CodeAction.create( + value, + createWorkspaceEdit(document.uri, [TextEdit.replace(diagnostic.range, value)]), + CodeActionKind.QuickFix + ) + ); + } + } + return results; + } } -function getNodeforDiagnostic(document: TextDocument, diagnostic: Diagnostic): ASTNode { +function getNodeForDiagnostic(document: TextDocument, diagnostic: Diagnostic): ASTNode { const yamlDocuments = yamlDocumentsCache.getYamlDocument(document); const startOffset = document.offsetAt(diagnostic.range.start); const yamlDoc = matchOffsetToDocument(startOffset, yamlDocuments); diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index 71268db7..829310c1 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -26,6 +26,7 @@ import { KUBERNETES_SCHEMA_URL } from '../src/languageservice/utils/schemaUrls'; import { IProblem } from '../src/languageservice/parser/jsonParser07'; import { JSONSchema } from '../src/languageservice/jsonSchema'; import { TestTelemetry } from './utils/testsTypes'; +import { ErrorCode } from 'vscode-json-languageservice'; describe('Validation Tests', () => { let languageSettingsSetup: ServiceSetup; @@ -396,7 +397,8 @@ describe('Validation Tests', () => { 4, DiagnosticSeverity.Error, `yaml-schema: file:///${SCHEMA_ID}`, - `file:///${SCHEMA_ID}` + `file:///${SCHEMA_ID}`, + ErrorCode.PropertyExpected ) ); }) @@ -1289,7 +1291,7 @@ obj: 4, 18, DiagnosticSeverity.Error, - 'yaml-schema: Package', + 'yaml-schema: Composer Package', 'https://raw.githubusercontent.com/composer/composer/master/res/composer-schema.json' ) ); @@ -1312,6 +1314,7 @@ obj: DiagnosticSeverity.Error, 'yaml-schema: Drone CI configuration file', 'https://json.schemastore.org/drone', + ErrorCode.PropertyExpected, { properties: [ 'type', diff --git a/test/utils/verifyError.ts b/test/utils/verifyError.ts index ea7f17c5..d2769c74 100644 --- a/test/utils/verifyError.ts +++ b/test/utils/verifyError.ts @@ -39,9 +39,19 @@ export function createDiagnosticWithData( severity: DiagnosticSeverity = 1, source = 'YAML', schemaUri: string | string[], + code: string | number = ErrorCode.Undefined, data: Record = {} ): Diagnostic { - const diagnostic: Diagnostic = createExpectedError(message, startLine, startCharacter, endLine, endCharacter, severity, source); + const diagnostic: Diagnostic = createExpectedError( + message, + startLine, + startCharacter, + endLine, + endCharacter, + severity, + source, + code + ); diagnostic.data = { schemaUri: typeof schemaUri === 'string' ? [schemaUri] : schemaUri, ...data }; return diagnostic; } diff --git a/test/yamlCodeActions.test.ts b/test/yamlCodeActions.test.ts index aa2b72ce..447cfdcb 100644 --- a/test/yamlCodeActions.test.ts +++ b/test/yamlCodeActions.test.ts @@ -22,6 +22,7 @@ import { setupTextDocument, TEST_URI } from './utils/testHelper'; import { createDiagnosticWithData, createExpectedError, createUnusedAnchorDiagnostic } from './utils/verifyError'; import { YamlCommands } from '../src/commands'; import { LanguageSettings } from '../src'; +import { ErrorCode } from 'vscode-json-languageservice'; const expect = chai.expect; chai.use(sinonChai); @@ -377,4 +378,58 @@ animals: [dog , cat , mouse] `; ]); }); }); + + describe('Enum value or property mismatch quick fix', () => { + it('should generate proper action for enum mismatch', () => { + const doc = setupTextDocument('foo: value1'); + const diagnostic = createDiagnosticWithData( + 'message', + 0, + 5, + 0, + 11, + DiagnosticSeverity.Hint, + 'YAML', + 'schemaUri', + ErrorCode.EnumValueMismatch, + { values: ['valueX', 'valueY'] } + ); + const params: CodeActionParams = { + context: CodeActionContext.create([diagnostic]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result.map((r) => r.title)).deep.equal(['valueX', 'valueY']); + expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(0, 5, 0, 11), 'valueX')]); + }); + + it('should generate proper action for wrong property', () => { + const doc = setupTextDocument('foo: value1'); + const diagnostic = createDiagnosticWithData( + 'message', + 0, + 0, + 0, + 3, + DiagnosticSeverity.Hint, + 'YAML', + 'schemaUri', + ErrorCode.PropertyExpected, + { + properties: ['fooX', 'fooY'], + } + ); + const params: CodeActionParams = { + context: CodeActionContext.create([diagnostic]), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result.map((r) => r.title)).deep.equal(['fooX', 'fooY']); + expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.replace(Range.create(0, 0, 0, 3), 'fooX')]); + }); + }); });