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 000000000..a41e3c0dc --- /dev/null +++ b/packages/apidom-ls/src/config/asyncapi/target-specs.ts @@ -0,0 +1,17 @@ +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, +]; diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index 1846e2692..01980407c 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 5329c9f04..ebc83ab27 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,10 @@ 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 missingCoreFieldsOpenAPI2_0Lint from './missing-core-fields-openapi-2-0'; +import missingCoreFieldsOpenAPI3_0Lint from './missing-core-fields-openapi-3-0'; +import missingCoreFieldsOpenAPI3_1Lint from './missing-core-fields-openapi-3-1'; +import missingCoreFieldsAsyncAPI2Lint from './missing-core-fields-asyncapi-2'; import multipleOfTypeLint from './multiple-of--type'; import notTypeLint from './not--type'; import nullableNotRecommendedLint from './nullable--not-recommended'; @@ -102,6 +106,10 @@ const schemaLints = [ minPropertiesNonObjectLint, minPropertiesTypeLint, minimumPatternLint, + missingCoreFieldsOpenAPI2_0Lint, + missingCoreFieldsOpenAPI3_0Lint, + missingCoreFieldsOpenAPI3_1Lint, + missingCoreFieldsAsyncAPI2Lint, multipleOfTypeLint, notTypeLint, nullableNotRecommendedLint, diff --git a/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-asyncapi-2.ts b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-asyncapi-2.ts new file mode 100644 index 000000000..72a53bf6d --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-asyncapi-2.ts @@ -0,0 +1,73 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { AsyncAPI2 } from '../../../asyncapi/target-specs'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const missingCoreFieldsAsyncAPI2Lint: LinterMeta = { + code: ApilintCodes.SCHEMA_MISSING_CORE_FIELDS, + source: 'apilint', + message: 'Schema does not include any Schema Object keywords', + severity: DiagnosticSeverity.Hint, + linterFunction: 'existAnyOfFields', + linterParams: [ + [ + '$id', + '$schema', + '$comment', + '$ref', + 'if', + 'then', + 'else', + 'contentEncoding', + 'contentMediaType', + 'contains', + 'propertyNames', + 'const', + 'examples', + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'additionalItems', + 'items', + 'maxItems', + 'minItems', + 'uniqueItems', + 'patternProperties', + 'dependencies', + 'definitions', + 'maxProperties', + 'minProperties', + 'required', + 'properties', + 'additionalProperties', + 'enum', + 'type', + 'allOf', + 'anyOf', + 'oneOf', + 'not', + 'title', + 'description', + 'default', + 'format', + 'readOnly', + 'writeOnly', + 'discriminator', + 'externalDocs', + 'deprecated', + ], + true, + 'boolean', + ], + marker: 'key', + targetSpecs: [...AsyncAPI2], +}; + +export default missingCoreFieldsAsyncAPI2Lint; diff --git a/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-2-0.ts b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-2-0.ts new file mode 100644 index 000000000..10865732e --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-2-0.ts @@ -0,0 +1,55 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { OpenAPI2 } from '../../../openapi/target-specs'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const missingCoreFieldsOpenAPI2Lint: LinterMeta = { + code: ApilintCodes.SCHEMA_MISSING_CORE_FIELDS, + source: 'apilint', + message: 'Schema does not include any Schema Object keywords', + severity: DiagnosticSeverity.Hint, + linterFunction: 'existAnyOfFields', + linterParams: [ + [ + '$ref', + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'additionalItems', + 'items', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'properties', + 'additionalProperties', + 'enum', + 'type', + 'allOf', + 'title', + 'description', + 'default', + 'format', + 'readOnly', + 'discriminator', + 'externalDocs', + 'xml', + 'example', + ], + true, + 'boolean', + ], + marker: 'key', + targetSpecs: [...OpenAPI2], +}; + +export default missingCoreFieldsOpenAPI2Lint; diff --git a/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-3-0.ts b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-3-0.ts new file mode 100644 index 000000000..f7eeae5fa --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-3-0.ts @@ -0,0 +1,61 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { OpenAPI30 } from '../../../openapi/target-specs'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const missingCoreFieldsOpenAPI3_0Lint: LinterMeta = { + code: ApilintCodes.SCHEMA_MISSING_CORE_FIELDS, + source: 'apilint', + message: 'Schema does not include any Schema Object keywords', + severity: DiagnosticSeverity.Hint, + linterFunction: 'existAnyOfFields', + linterParams: [ + [ + '$ref', + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'additionalItems', + 'items', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'properties', + 'additionalProperties', + 'enum', + 'type', + 'allOf', + 'anyOf', + 'oneOf', + 'not', + 'title', + 'description', + 'default', + 'format', + 'readOnly', + 'nullable', + 'discriminator', + 'externalDocs', + 'writeOnly', + 'xml', + 'example', + 'deprecated', + ], + true, + 'boolean', + ], + marker: 'key', + targetSpecs: [...OpenAPI30], +}; + +export default missingCoreFieldsOpenAPI3_0Lint; diff --git a/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-3-1.ts b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-3-1.ts new file mode 100644 index 000000000..27160c5ae --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/missing-core-fields-openapi-3-1.ts @@ -0,0 +1,87 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { OpenAPI31 } from '../../../openapi/target-specs'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const missingCoreFieldsOpenAPI3_1Lint: LinterMeta = { + code: ApilintCodes.SCHEMA_MISSING_CORE_FIELDS, + source: 'apilint', + message: 'Schema does not include any Schema Object keywords', + severity: DiagnosticSeverity.Hint, + linterFunction: 'existAnyOfFields', + linterParams: [ + [ + '$ref', + '$schema', + '$id', + '$vocabulary', + '$anchor', + '$dynamicAnchor', + '$defs', + '$comment', + 'if', + 'then', + 'else', + 'dependentSchemas', + 'prefixItems', + 'contains', + 'patternProperties', + 'propertyNames', + 'unevaluatedProperties', + 'unevaluatedItems', + 'const', + 'maxContains', + 'minContains', + 'dependencies', + 'dependentRequired', + 'examples', + 'contentEncoding', + 'contentMediaType', + 'contentSchema', + 'definitions', + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'additionalItems', + 'items', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'properties', + 'additionalProperties', + 'enum', + 'type', + 'allOf', + 'anyOf', + 'oneOf', + 'not', + 'title', + 'description', + 'default', + 'format', + 'readOnly', + 'discriminator', + 'externalDocs', + 'writeOnly', + 'xml', + 'example', + 'deprecated', + ], + true, + 'boolean', + ], + marker: 'key', + targetSpecs: [...OpenAPI31], +}; + +export default missingCoreFieldsOpenAPI3_1Lint; diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index efe0b2573..32fdc0826 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -172,6 +172,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 000000000..e6c09aeb8 --- /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 + $schema: 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 000000000..766ecd1e6 --- /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 619542fe8..0a7833374 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 Schema Object 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 6f7b093db..c76946d68 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 Schema Object 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 227ba6609..b1b9efbaa 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3696,4 +3696,63 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + + it('oas / yaml - schema should have at least one 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 Schema Object keywords', + severity: 4, + code: 10072, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('asyncapi / yaml - schema should have at least one 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 Schema Object keywords', + severity: 4, + code: 10072, + source: 'apilint', + }, + ]; + console.log(JSON.stringify(result, null, 2)); + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); });