From 4080236b842ab1fa920e6357f9866e84ae43eb4c Mon Sep 17 00:00:00 2001 From: frantuma Date: Mon, 18 Dec 2023 19:11:37 +0100 Subject: [PATCH] feat(apidom-ls): create schema rule for missing core keywords Fix #3549 --- .../src/config/asyncapi/target-specs.ts | 18 ++++ packages/apidom-ls/src/config/codes.ts | 1 + .../src/config/common/schema/lint/index.ts | 2 + .../missing-core-fields-oas-3-0-asyncapi.ts | 91 +++++++++++++++++++ .../services/validation/linter-functions.ts | 24 +++++ .../validation/asyncapi/issue3549.yaml | 44 +++++++++ .../fixtures/validation/oas/issue3549.yaml | 24 +++++ packages/apidom-ls/test/openapi-json.ts | 7 ++ packages/apidom-ls/test/openapi-yaml.ts | 7 ++ packages/apidom-ls/test/validate.ts | 58 ++++++++++++ 10 files changed, 276 insertions(+) create mode 100644 packages/apidom-ls/src/config/asyncapi/target-specs.ts create mode 100644 packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-oas-3-0-asyncapi.ts create mode 100644 packages/apidom-ls/test/fixtures/validation/asyncapi/issue3549.yaml create mode 100644 packages/apidom-ls/test/fixtures/validation/oas/issue3549.yaml diff --git a/packages/apidom-ls/src/config/asyncapi/target-specs.ts b/packages/apidom-ls/src/config/asyncapi/target-specs.ts new file mode 100644 index 0000000000..a490f020a2 --- /dev/null +++ b/packages/apidom-ls/src/config/asyncapi/target-specs.ts @@ -0,0 +1,18 @@ +export const AsyncAPI200 = [{ namespace: 'asyncapi', version: '2.0.0' }]; +export const AsyncAPI210 = [{ namespace: 'asyncapi', version: '2.1.0' }]; +export const AsyncAPI220 = [{ namespace: 'asyncapi', version: '2.2.0' }]; +export const AsyncAPI230 = [{ namespace: 'asyncapi', version: '2.3.0' }]; +export const AsyncAPI240 = [{ namespace: 'asyncapi', version: '2.4.0' }]; +export const AsyncAPI250 = [{ namespace: 'asyncapi', version: '2.5.0' }]; +export const AsyncAPI260 = [{ namespace: 'asyncapi', version: '2.6.0' }]; + +export const AsyncAPI2 = [ + ...AsyncAPI200, + ...AsyncAPI210, + ...AsyncAPI220, + ...AsyncAPI230, + ...AsyncAPI240, + ...AsyncAPI250, + ...AsyncAPI260, +]; +export const AsyncAPI = [...AsyncAPI2]; diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index 325b2e7014..72e641850c 100644 --- a/packages/apidom-ls/src/config/codes.ts +++ b/packages/apidom-ls/src/config/codes.ts @@ -66,6 +66,7 @@ enum ApilintCodes { SCHEMA_EXAMPLE_DEPRECATED, SCHEMA_TYPE_OPENAPI_3_0, SCHEMA_NULLABLE_NOT_RECOMMENDED, + SCHEMA_MISSING_CORE_FIELDS, DUPLICATE_KEYS = 14999, NOT_ALLOWED_FIELDS = 15000, diff --git a/packages/apidom-ls/src/config/common/schema/lint/index.ts b/packages/apidom-ls/src/config/common/schema/lint/index.ts index 5329c9f04d..35b5ead7e9 100644 --- a/packages/apidom-ls/src/config/common/schema/lint/index.ts +++ b/packages/apidom-ls/src/config/common/schema/lint/index.ts @@ -36,6 +36,7 @@ import minLengthTypeLint from './min-length--type'; import minPropertiesNonObjectLint from './min-properties--non-object'; import minPropertiesTypeLint from './min-properties--type'; import minimumPatternLint from './minimum--pattern'; +import missingCoreFieldsOpenAPI3_0AndAsyncAPILint from './missing-core-fields-oas-3-0-asyncapi'; import multipleOfTypeLint from './multiple-of--type'; import notTypeLint from './not--type'; import nullableNotRecommendedLint from './nullable--not-recommended'; @@ -102,6 +103,7 @@ const schemaLints = [ minPropertiesNonObjectLint, minPropertiesTypeLint, minimumPatternLint, + missingCoreFieldsOpenAPI3_0AndAsyncAPILint, multipleOfTypeLint, notTypeLint, nullableNotRecommendedLint, diff --git a/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-oas-3-0-asyncapi.ts b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-oas-3-0-asyncapi.ts new file mode 100644 index 0000000000..e7d61514ce --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-oas-3-0-asyncapi.ts @@ -0,0 +1,91 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { OpenAPI3 } from '../../../openapi/target-specs'; +import { AsyncAPI } from '../../../asyncapi/target-specs'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const missingCoreFieldsOpenAPI3_0AndAsyncAPILint: LinterMeta = { + code: ApilintCodes.SCHEMA_MISSING_CORE_FIELDS, + source: 'apilint', + message: 'Schema does not include any JSON Schema core keywords', + severity: DiagnosticSeverity.Hint, + linterFunction: 'existAnyOfFields', + linterParams: [ + [ + '$anchor', + '$comment', + '$dynamicAnchor', + '$id', + '$ref', + '$schema', + '$vocabulary', + 'additionalItems', + 'additionalProperties', + 'allOf', + 'anyOf', + 'booleanSchemaValue', + 'const', + 'contains', + 'contentEncoding', + 'contentMediaType', + 'contentSchema', + 'default', + 'dependentSchemas', + 'deprecated', + 'description', + 'discriminator', + 'else', + 'examples', + 'exclusiveMaximum', + 'exclusiveMaximumValue', + 'exclusiveMinimum', + 'exclusiveMinimumValue', + 'externalDocs', + 'format', + 'if', + 'items', + 'dependentRequired', + 'maxContains', + 'maximum', + 'maxItems', + 'maxLength', + 'maxProperties', + 'minContains', + 'minimum', + 'minItems', + 'minLength', + 'minProperties', + 'multipleOf', + 'name', + 'not', + 'nullable', + 'extensions', + 'oneOf', + 'pattern', + 'patternProperties', + 'prefixItems', + 'properties', + 'propertyNames', + 'enum', + 'example', + 'readOnly', + 'required', + 'then', + 'title', + 'type', + 'unevaluatedItems', + 'unevaluatedProperties', + 'uniqueItems', + 'writeOnly', + 'xml', + ], + true, + 'boolean', + ], + marker: 'key', + targetSpecs: [...OpenAPI3, ...AsyncAPI], +}; + +export default missingCoreFieldsOpenAPI3_0AndAsyncAPILint; diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index b453b8ac6f..09d33ae3a0 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -171,6 +171,30 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'existAnyOfFields', + function: ( + element: Element, + keys: string[], + allowEmpty: boolean, + skipIfType?: string, + ): boolean => { + if (element && isObject(element)) { + if (skipIfType && isType(element, skipIfType)) { + return true; + } + if (!element.keys() || element.keys().length === 0) { + return allowEmpty; + } + for (const key of keys) { + if (element.hasKey(key)) { + return true; + } + } + } + return false; + }, + }, { functionName: 'allowedFields', function: ( diff --git a/packages/apidom-ls/test/fixtures/validation/asyncapi/issue3549.yaml b/packages/apidom-ls/test/fixtures/validation/asyncapi/issue3549.yaml new file mode 100644 index 0000000000..172f1493b8 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/asyncapi/issue3549.yaml @@ -0,0 +1,44 @@ +asyncapi: 2.1.0 +info: + version: '1.0.0' + title: missing schema keywords + description: 'desc' + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + +servers: + production: + url: mqtt://test.mosquitto.org + protocol: mqtt + description: Test MQTT broker + +channels: + user/signedup: + publish: + operationId: onUserSignUp + message: + $ref : '#/components/messages/UserSignedUp' + +components: + messages: + UserSignedUp: + name: userSignedUp + title: User signed up event + summary: Inform about a new user registration in the system + contentType: application/json + payload: + $ref: '#/components/schemas/userSignedUpPayload' + + schemas: + userSignedUpPayload: + type: object + properties: + anyOf: + otherKey: foo + good: + otherKey: foo + $dynamicAnchor: good + boolFalse: false + boolTrue: true + emptyObject: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/issue3549.yaml b/packages/apidom-ls/test/fixtures/validation/oas/issue3549.yaml new file mode 100644 index 0000000000..766ecd1e63 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/issue3549.yaml @@ -0,0 +1,24 @@ +openapi: 3.1.0 +info: + title: missing schema keywords + version: 1.0.0 +paths: + /a: + get: + operationId: aget + responses: + '200': + description: aget + content: + application/json: + schema: + type: object + properties: + anyOf: + otherKey: foo + good: + otherKey: foo + $dynamicAnchor: good + boolFalse: false + boolTrue: true + emptyObject: {} diff --git a/packages/apidom-ls/test/openapi-json.ts b/packages/apidom-ls/test/openapi-json.ts index 9e215671aa..916e467697 100644 --- a/packages/apidom-ls/test/openapi-json.ts +++ b/packages/apidom-ls/test/openapi-json.ts @@ -509,6 +509,13 @@ describe('apidom-ls', function () { code: 7030101, source: 'apilint', }, + { + range: { start: { line: 30, character: 10 }, end: { line: 30, character: 14 } }, + message: 'Schema does not include any JSON Schema core keywords', + severity: 4, + code: 10072, + source: 'apilint', + }, { range: { start: { line: 185, character: 20 }, end: { line: 190, character: 7 } }, message: 'parameters must be an array', diff --git a/packages/apidom-ls/test/openapi-yaml.ts b/packages/apidom-ls/test/openapi-yaml.ts index 022c29ec01..ecdbf11faf 100644 --- a/packages/apidom-ls/test/openapi-yaml.ts +++ b/packages/apidom-ls/test/openapi-yaml.ts @@ -517,6 +517,13 @@ describe('apidom-ls-yaml', function () { code: 7030101, source: 'apilint', }, + { + range: { start: { line: 28, character: 8 }, end: { line: 28, character: 10 } }, + message: 'Schema does not include any JSON Schema core keywords', + severity: 4, + code: 10072, + source: 'apilint', + }, { range: { start: { line: 128, character: 6 }, end: { line: 132, character: 0 } }, message: 'parameters must be an array', diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 8e4aa66588..1f1e7269cf 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3549,4 +3549,62 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + + it('oas / yaml - schema should have at least one JSON Schema core keyword - issue #3549', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'validation', 'oas', 'issue3549.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create('foo://bar/issue3549.yaml', 'yaml', 0, spec); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 16, character: 18 }, end: { line: 16, character: 23 } }, + message: 'Schema does not include any JSON Schema core keywords', + severity: 4, + code: 10072, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('asyncapi / yaml - schema should have at least one JSON Schema core keyword - issue #3549', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'validation', 'asyncapi', 'issue3549.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create('foo://bar/issue3549.yaml', 'yaml', 0, spec); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 36, character: 8 }, end: { line: 36, character: 13 } }, + message: 'Schema does not include any JSON Schema core keywords', + severity: 4, + code: 10072, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); });