diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index 0ce246d7c3..1e39e60355 100644 --- a/packages/apidom-ls/src/config/codes.ts +++ b/packages/apidom-ls/src/config/codes.ts @@ -841,6 +841,7 @@ enum ApilintCodes { OPENAPI2_REFERENCE = 3240000, OPENAPI2_REFERENCE_FIELD_$REF_FORMAT_URI = 3240100, + OPENAPI2_REFERENCE_NOT_USED = 3240300, OPENAPI3_0 = 5000000, @@ -1066,6 +1067,11 @@ enum ApilintCodes { OPENAPI3_0_REFERENCE = 5260000, OPENAPI3_0_REFERENCE_FIELD_$REF_FORMAT_URI = 5260100, OPENAPI3_0_REFERENCE_FIELD_$REF_NO_SIBLINGS, + OPENAPI3_0_REFERENCE_FIELD_$REF_REQUEST_BODIES = 5260200, + OPENAPI3_0_REFERENCE_FIELD_$REF_REQUEST_BODIES_NAMING, + OPENAPI3_0_REFERENCE_FIELD_$REF_REQUEST_BODIES_NAMING_SCHEMA, + OPENAPI3_0_REFERENCE_FIELD_$REF_HEADER = 5260300, + OPENAPI3_0_REFERENCE_FIELD_$REF_PARAMETER = 5260400, OPENAPI3_0_LINK = 5270000, OPENAPI3_0_LINK_FIELD_OPERATION_REF_FORMAT_URI = 5270100, diff --git a/packages/apidom-ls/src/config/common/schema/lint/$ref--not-used.ts b/packages/apidom-ls/src/config/common/schema/lint/$ref--not-used.ts new file mode 100644 index 0000000000..a810e98391 --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/$ref--not-used.ts @@ -0,0 +1,19 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI2, OpenAPI3, OpenAPI31 } from '../../../openapi/target-specs.ts'; + +const $refNotUsedLint: LinterMeta = { + code: ApilintCodes.OPENAPI2_REFERENCE_NOT_USED, + source: 'apilint', + message: 'Definition was declared but never used in document', + severity: DiagnosticSeverity.Warning, + linterFunction: 'apilintReferenceNotUsed', + linterParams: ['string'], + marker: 'key', + data: {}, + targetSpecs: [...OpenAPI2, ...OpenAPI3, ...OpenAPI31], +}; + +export default $refNotUsedLint; diff --git a/packages/apidom-ls/src/config/common/schema/lint/$ref-3-0--request-bodies.ts b/packages/apidom-ls/src/config/common/schema/lint/$ref-3-0--request-bodies.ts new file mode 100644 index 0000000000..165b28a11c --- /dev/null +++ b/packages/apidom-ls/src/config/common/schema/lint/$ref-3-0--request-bodies.ts @@ -0,0 +1,28 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI3 } from '../../../openapi/target-specs.ts'; + +const $ref3RequestBodiesLint: LinterMeta = { + code: ApilintCodes.OPENAPI3_0_REFERENCE_FIELD_$REF_REQUEST_BODIES, + source: 'apilint', + message: + 'requestBody schema $refs must point to a position where a Schema Object can be legally placed', + severity: DiagnosticSeverity.Error, + linterFunction: 'parentExistFields', + linterParams: [['requestBody']], + conditions: [ + { + targets: [{ path: '$ref' }], + function: 'apilintValueRegex', + params: ['^(?!.*#/components/schemas).*$'], + }, + ], + marker: 'value', + target: '$ref', + data: {}, + targetSpecs: OpenAPI3, +}; + +export default $ref3RequestBodiesLint; 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 76426b5749..b8ab3f4a64 100644 --- a/packages/apidom-ls/src/config/common/schema/lint/index.ts +++ b/packages/apidom-ls/src/config/common/schema/lint/index.ts @@ -3,6 +3,7 @@ import allowedFieldsOpenAPI3_0Lint from './allowed-fields-openapi-3-0.ts'; import $idFormatURILint from './$id--format-uri.ts'; import $refValidLint from './$ref--valid.ts'; import $refNoSiblingsLint from './$ref--no-siblings.ts'; +import $ref3RequestBodiesLint from './$ref-3-0--request-bodies.ts'; import additionalItemsNonArrayLint from './additional-items--non-array.ts'; import additionalItemsTypeLint from './additional-items--type.ts'; import additionalItemsTypeOpenAPI3_1__AsyncAPI2Lint from './additional-items--type-openapi-3-1--asyncapi-2.ts'; @@ -81,6 +82,7 @@ import uniqueItemsNonArrayLint from './unique-items--non-array.ts'; import uniqueItemsTypeLint from './unique-items--type.ts'; import writeOnlyTypeLint from './write-only--type.ts'; import exampleDeprecatedLint from './example--deprecated.ts'; +import $refNotUsedLint from './$ref--not-used.ts'; const schemaLints = [ allowedFieldsOpenAPI2_0Lint, @@ -88,6 +90,8 @@ const schemaLints = [ $idFormatURILint, $refValidLint, $refNoSiblingsLint, + $refNotUsedLint, + $ref3RequestBodiesLint, additionalItemsNonArrayLint, additionalItemsTypeLint, additionalItemsTypeOpenAPI3_1__AsyncAPI2Lint, diff --git a/packages/apidom-ls/src/config/openapi/header/lint/$ref-3-0--header.ts b/packages/apidom-ls/src/config/openapi/header/lint/$ref-3-0--header.ts new file mode 100644 index 0000000000..fec5d97d49 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/header/lint/$ref-3-0--header.ts @@ -0,0 +1,27 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI3 } from '../../target-specs.ts'; + +const $ref3HeaderNamingLint: LinterMeta = { + code: ApilintCodes.OPENAPI3_0_REFERENCE_FIELD_$REF_HEADER, + source: 'apilint', + message: 'OAS3 header $Ref should point to Header Object', + severity: DiagnosticSeverity.Error, + linterFunction: 'parentExistFields', + linterParams: [['header']], + conditions: [ + { + targets: [{ path: '$ref' }], + function: 'apilintValueRegex', + params: ['^(?!.*#/components/headers).*$'], + }, + ], + marker: 'value', + target: '$ref', + data: {}, + targetSpecs: OpenAPI3, +}; + +export default $ref3HeaderNamingLint; diff --git a/packages/apidom-ls/src/config/openapi/header/lint/index.ts b/packages/apidom-ls/src/config/openapi/header/lint/index.ts index c4ebe7ccfa..fc56c3ae47 100644 --- a/packages/apidom-ls/src/config/openapi/header/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/header/lint/index.ts @@ -35,8 +35,10 @@ import minItemsTypeLint from './min-items--type.ts'; import uniqueItemsTypeLint from './unique-items--type.ts'; import enumTypeLint from './enum--type.ts'; import multipleOfTypeLint from './multiple-of--type.ts'; +import $ref3HeaderNamingLint from './$ref-3-0--header.ts'; const lints = [ + $ref3HeaderNamingLint, descriptionTypeLint, requiredTypeLint, deprecatedTypeLint, diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/$ref-3-0--parameter.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/$ref-3-0--parameter.ts new file mode 100644 index 0000000000..3e3ec0f43a --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/$ref-3-0--parameter.ts @@ -0,0 +1,27 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI3 } from '../../target-specs.ts'; + +const $ref3ParameterNamingLint: LinterMeta = { + code: ApilintCodes.OPENAPI3_0_REFERENCE_FIELD_$REF_PARAMETER, + source: 'apilint', + message: 'OAS3 parameter $Ref should point to Parameter Object', + severity: DiagnosticSeverity.Error, + linterFunction: 'apilintValueRegex', + linterParams: ['^(.*#/components/parameters).*$'], + marker: 'value', + target: '$ref', + conditions: [ + { + targets: [{ path: '$ref' }], + function: 'parentExistFields', + params: [['paths']], + }, + ], + data: {}, + targetSpecs: OpenAPI3, +}; + +export default $ref3ParameterNamingLint; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts index 6e2bfd1f76..690566dcb4 100644 --- a/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts @@ -42,8 +42,10 @@ import uniqueItemsTypeLint from './unique-items--type.ts'; import enumTypeLint from './enum--type.ts'; import multipleOfTypeLint from './multiple-of--type.ts'; import inPathTemplateLint from './in-path-template.ts'; +import $ref3ParameterNamingLint from './$ref-3-0--parameter.ts'; const lints = [ + $ref3ParameterNamingLint, nameTypeLint, nameRequiredLint, inEquals2_0Lint, diff --git a/packages/apidom-ls/src/config/openapi/request-body/lint/$ref-3-0--request-bodies-naming-schema.ts b/packages/apidom-ls/src/config/openapi/request-body/lint/$ref-3-0--request-bodies-naming-schema.ts new file mode 100644 index 0000000000..934bdece81 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/request-body/lint/$ref-3-0--request-bodies-naming-schema.ts @@ -0,0 +1,28 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI3 } from '../../target-specs.ts'; + +const $ref3RequestBodiesNamingSchemaLint: LinterMeta = { + code: ApilintCodes.OPENAPI3_0_REFERENCE_FIELD_$REF_REQUEST_BODIES_NAMING_SCHEMA, + source: 'apilint', + message: + "requestBody $refs cannot point to '#/components/schemas/…', they must point to '#/components/requestBodies/…'", + severity: DiagnosticSeverity.Error, + linterFunction: 'parentExistFields', + linterParams: [['requestBodies']], + conditions: [ + { + targets: [{ path: '$ref' }], + function: 'apilintValueRegex', + params: ['^(.*#/components/schemas).*$'], + }, + ], + marker: 'value', + target: '$ref', + data: {}, + targetSpecs: OpenAPI3, +}; + +export default $ref3RequestBodiesNamingSchemaLint; diff --git a/packages/apidom-ls/src/config/openapi/request-body/lint/$ref-3-0--request-bodies-naming.ts b/packages/apidom-ls/src/config/openapi/request-body/lint/$ref-3-0--request-bodies-naming.ts new file mode 100644 index 0000000000..204db6868b --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/request-body/lint/$ref-3-0--request-bodies-naming.ts @@ -0,0 +1,27 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI3 } from '../../target-specs.ts'; + +const $ref3RequestBodiesNamingLint: LinterMeta = { + code: ApilintCodes.OPENAPI3_0_REFERENCE_FIELD_$REF_REQUEST_BODIES_NAMING, + source: 'apilint', + message: 'requestBody $refs must point to a position where a requestBody can be legally placed', + severity: DiagnosticSeverity.Error, + linterFunction: 'parentExistFields', + linterParams: [['requestBodies']], + conditions: [ + { + targets: [{ path: '$ref' }], + function: 'apilintValueRegex', + params: ['^(?!.*#/components/(requestBodies|schemas)).*$'], + }, + ], + marker: 'value', + target: '$ref', + data: {}, + targetSpecs: OpenAPI3, +}; + +export default $ref3RequestBodiesNamingLint; diff --git a/packages/apidom-ls/src/config/openapi/request-body/lint/index.ts b/packages/apidom-ls/src/config/openapi/request-body/lint/index.ts index 8f74dd5336..0fadc4cbca 100644 --- a/packages/apidom-ls/src/config/openapi/request-body/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/request-body/lint/index.ts @@ -4,8 +4,12 @@ import descriptionTypeLint from './description--type.ts'; import contentValuesTypeLint from './content--values-type.ts'; import contentRequiredLint from './content--required.ts'; import requiredTypeLint from './required--type.ts'; +import $ref3RequestBodiesNamingLint from './$ref-3-0--request-bodies-naming.ts'; +import $ref3RequestBodiesNamingSchemaLint from './$ref-3-0--request-bodies-naming-schema.ts'; const lints = [ + $ref3RequestBodiesNamingLint, + $ref3RequestBodiesNamingSchemaLint, descriptionTypeLint, contentRequiredLint, contentValuesTypeLint, diff --git a/packages/apidom-ls/src/config/openapi/schema/lint.ts b/packages/apidom-ls/src/config/openapi/schema/lint.ts index 546fa7d593..d87f9b9671 100644 --- a/packages/apidom-ls/src/config/openapi/schema/lint.ts +++ b/packages/apidom-ls/src/config/openapi/schema/lint.ts @@ -75,6 +75,8 @@ import uniqueItemsNonArrayLint from '../../common/schema/lint/unique-items--non- import uniqueItemsTypeLint from '../../common/schema/lint/unique-items--type.ts'; import writeOnlyTypeLint from '../../common/schema/lint/write-only--type.ts'; import exampleDeprecatedLint from '../../common/schema/lint/example--deprecated.ts'; +import $refNotUsedLint from '../../common/schema/lint/$ref--not-used.ts'; +import $ref3RequestBodiesLint from '../../common/schema/lint/$ref-3-0--request-bodies.ts'; import { OpenAPI31 } from '../target-specs.ts'; const schemaLints = [ @@ -154,6 +156,8 @@ const schemaLints = [ uniqueItemsTypeLint, writeOnlyTypeLint, exampleDeprecatedLint, + $refNotUsedLint, + $ref3RequestBodiesLint, ]; export default schemaLints; diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 196d248c27..3de5f1cda3 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -176,6 +176,20 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'parentExistFields', + function: (element: Element, keys: string[]): boolean => { + const parent = element?.parent?.parent?.parent?.parent; + if (parent && isObject(parent)) { + for (const key of keys) { + if (!parent.hasKey(key)) { + return false; + } + } + } + return true; + }, + }, { functionName: 'existAnyOfFields', function: (element: Element, keys: string[], allowEmpty: boolean): boolean => { @@ -1089,6 +1103,28 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'apilintReferenceNotUsed', + function: (element: Element & { content?: { key?: string } }) => { + const elParent: Element = element.parent?.parent?.parent?.parent; + if (typeof elParent.hasKey !== 'function' || !elParent.hasKey('schemas')) { + return true; + } + + const api = root(element); + const isReferenceElement = (el: Element & { content?: { key?: string } }) => + toValue(el.content.key) === '$ref'; + const referenceElements = filter((el) => { + return isReferenceElement(el); + }, api); + const referenceNames = referenceElements.map((refElement: Element) => + // @ts-expect-error + toValue(refElement.content.value).split('/').at(-1), + ); + // @ts-expect-error + return referenceNames.includes(toValue(element.parent.key)); + }, + }, { functionName: 'apilintOpenAPIParameterInPathTemplate', function: (element: Element) => { diff --git a/packages/apidom-ls/test/custom-rules.ts b/packages/apidom-ls/test/custom-rules.ts index fbf1cb060f..0b814e5491 100644 --- a/packages/apidom-ls/test/custom-rules.ts +++ b/packages/apidom-ls/test/custom-rules.ts @@ -87,8 +87,8 @@ describe('apidom-ls-validate-custom-rules', function () { }, { range: { - start: { line: 13, character: 8 }, - end: { line: 13, character: 17 }, + start: { line: 15, character: 8 }, + end: { line: 15, character: 17 }, }, message: 'properties MUST follow camelCase', severity: 1, @@ -98,8 +98,8 @@ describe('apidom-ls-validate-custom-rules', function () { }, { range: { - start: { line: 18, character: 4 }, - end: { line: 18, character: 7 }, + start: { line: 20, character: 4 }, + end: { line: 20, character: 7 }, }, message: 'Parameter Object must contain one of the following fields: content, schema', severity: 1, @@ -118,8 +118,8 @@ describe('apidom-ls-validate-custom-rules', function () { }, { range: { - start: { line: 20, character: 6 }, - end: { line: 20, character: 10 }, + start: { line: 22, character: 6 }, + end: { line: 22, character: 10 }, }, message: 'parameter names MUST follow camelCase', severity: 1, @@ -129,8 +129,8 @@ describe('apidom-ls-validate-custom-rules', function () { }, { range: { - start: { line: 21, character: 4 }, - end: { line: 21, character: 8 }, + start: { line: 23, character: 4 }, + end: { line: 23, character: 8 }, }, message: 'Parameter Object must contain one of the following fields: content, schema', severity: 1, @@ -149,8 +149,8 @@ describe('apidom-ls-validate-custom-rules', function () { }, { range: { - start: { line: 24, character: 4 }, - end: { line: 24, character: 7 }, + start: { line: 26, character: 4 }, + end: { line: 26, character: 7 }, }, message: 'Parameter Object must contain one of the following fields: content, schema', severity: 1, @@ -200,7 +200,7 @@ describe('apidom-ls-validate-custom-rules', function () { const result = await languageService.doValidation(docOpenapi, validationContext); const expected: Diagnostic[] = [ { - range: { start: { line: 23, character: 6 }, end: { line: 23, character: 10 } }, + range: { start: { line: 25, character: 6 }, end: { line: 25, character: 10 } }, message: 'parameter names MUST follow camelCase', severity: 1, code: 20002, @@ -208,7 +208,7 @@ describe('apidom-ls-validate-custom-rules', function () { data: {}, }, { - range: { start: { line: 32, character: 6 }, end: { line: 32, character: 10 } }, + range: { start: { line: 34, character: 6 }, end: { line: 34, character: 10 } }, message: 'parameter names MUST follow camelCase', severity: 1, code: 20002, @@ -216,7 +216,7 @@ describe('apidom-ls-validate-custom-rules', function () { data: {}, }, { - range: { start: { line: 16, character: 8 }, end: { line: 16, character: 17 } }, + range: { start: { line: 18, character: 8 }, end: { line: 18, character: 17 } }, message: 'keys MUST follow camelCase', severity: 1, code: 20001, @@ -224,7 +224,7 @@ describe('apidom-ls-validate-custom-rules', function () { data: {}, }, { - range: { start: { line: 31, character: 4 }, end: { line: 31, character: 7 } }, + range: { start: { line: 33, character: 4 }, end: { line: 33, character: 7 } }, message: 'keys MUST follow camelCase', severity: 1, code: 20001, diff --git a/packages/apidom-ls/test/fixtures/custom-rules-jsonpath.yaml b/packages/apidom-ls/test/fixtures/custom-rules-jsonpath.yaml index 0c75fee51b..8422270627 100644 --- a/packages/apidom-ls/test/fixtures/custom-rules-jsonpath.yaml +++ b/packages/apidom-ls/test/fixtures/custom-rules-jsonpath.yaml @@ -9,6 +9,8 @@ paths: /keb-ab: get: operationId: kebab + /endpoint: + $ref: '#/components/schemas/User' components: schemas: User: diff --git a/packages/apidom-ls/test/fixtures/custom-rules-simple.yaml b/packages/apidom-ls/test/fixtures/custom-rules-simple.yaml index 2d20156918..b4a7e5afa3 100644 --- a/packages/apidom-ls/test/fixtures/custom-rules-simple.yaml +++ b/packages/apidom-ls/test/fixtures/custom-rules-simple.yaml @@ -6,6 +6,8 @@ paths: /keb-ab: get: operationId: kebab + /endpoint: + $ref: '#/components/schemas/User' components: schemas: User: diff --git a/packages/apidom-ls/test/fixtures/deref/invalid.json b/packages/apidom-ls/test/fixtures/deref/invalid.json index 31409623f1..da5d524634 100644 --- a/packages/apidom-ls/test/fixtures/deref/invalid.json +++ b/packages/apidom-ls/test/fixtures/deref/invalid.json @@ -48,6 +48,11 @@ "schema": { "$ref": "#/components/schemas/exampleRef" } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/petRef" + } } } } diff --git a/packages/apidom-ls/test/fixtures/deref/valid-same-ref.json b/packages/apidom-ls/test/fixtures/deref/valid-same-ref.json index c8edcc2b79..ae89e60014 100644 --- a/packages/apidom-ls/test/fixtures/deref/valid-same-ref.json +++ b/packages/apidom-ls/test/fixtures/deref/valid-same-ref.json @@ -4,6 +4,14 @@ "title": "test", "version": "1.0" }, + "paths": { + "/a": { + "$ref": "#/components/schemas/a" + }, + "/b": { + "$ref": "#/components/schemas/b" + } + }, "components": { "schemas": { "a": { diff --git a/packages/apidom-ls/test/fixtures/validation/oas/ref-header.yaml b/packages/apidom-ls/test/fixtures/validation/oas/ref-header.yaml new file mode 100644 index 0000000000..91390a8858 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/ref-header.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /foo: + get: + responses: + '200': + description: OK + headers: + X-MyHeader: + $ref: '#/components/schemas/MyHeader' +components: + headers: + MyHeader: + description: ID of the user + required: true + schema: + type: string + schemas: + MyHeader: + description: ID of the user diff --git a/packages/apidom-ls/test/fixtures/validation/oas/ref-not-used.yaml b/packages/apidom-ls/test/fixtures/validation/oas/ref-not-used.yaml new file mode 100644 index 0000000000..69c14d6737 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/ref-not-used.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.0 +info: + title: 'test' + version: 1.0.0 +paths: + /test: + get: + responses: + default: + description: test +components: + schemas: + notUsedSchema: + type: object diff --git a/packages/apidom-ls/test/fixtures/validation/oas/ref-parameter.yaml b/packages/apidom-ls/test/fixtures/validation/oas/ref-parameter.yaml new file mode 100644 index 0000000000..5195498bd5 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/ref-parameter.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /foo: + parameters: + - $ref: '#/components/headers/MyHeader' +components: + headers: + MyHeader: + description: ID of the user + required: true + schema: + type: string + diff --git a/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies-naming-schema.yaml b/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies-naming-schema.yaml new file mode 100644 index 0000000000..a1acea4592 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies-naming-schema.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /: + post: + responses: + default: + description: 'test' + operationId: myId + requestBody: + $ref: '#/components/schemas/MyBody' +components: + schemas: + MyBody: + description: 'test' + requestBodies: + MyBody: + content: + application/json: + schema: + type: string diff --git a/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies-naming.yaml b/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies-naming.yaml new file mode 100644 index 0000000000..7844936188 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies-naming.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /: + post: + responses: + default: + description: 'test' + operationId: myId + requestBody: + $ref: '#/components/headers/MyBody' +components: + headers: + MyBody: + schema: + description: 'description' + requestBodies: + MyBody: + content: + application/json: + schema: + type: string diff --git a/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies.yaml b/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies.yaml new file mode 100644 index 0000000000..495b508a99 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/ref-request-bodies.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /: + post: + responses: + default: + description: 'test' + operationId: myId + requestBody: + content: + application/json: + schema: + $ref: '#/components/requestBodies/MyBody/content/application~1json/schema' +components: + requestBodies: + MyBody: + content: + application/json: + schema: + type: string diff --git a/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.0-petstore.yaml b/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.0-petstore.yaml index 43c1b2cf2a..6ad7526050 100644 --- a/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.0-petstore.yaml +++ b/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.0-petstore.yaml @@ -706,6 +706,8 @@ components: description: User Status format: int32 example: 1 + items: + $ref: '#/components/schemas/Customer' xml: name: user Tag: diff --git a/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.1-basic.yaml b/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.1-basic.yaml index 6f3df74a4c..8ad2a3a10e 100644 --- a/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.1-basic.yaml +++ b/packages/apidom-ls/test/fixtures/validation/oas/valid/oas-3.1-basic.yaml @@ -25,6 +25,13 @@ paths: application/json: schema: $ref: '#/components/schemas/foo' + patch: + operationId: bpatch + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/bar' components: schemas: foo: diff --git a/packages/apidom-ls/test/openapi-json.ts b/packages/apidom-ls/test/openapi-json.ts index 63ac92d4cb..a3273e4a50 100644 --- a/packages/apidom-ls/test/openapi-json.ts +++ b/packages/apidom-ls/test/openapi-json.ts @@ -512,6 +512,23 @@ describe('apidom-ls', function () { code: 7030101, source: 'apilint', }, + { + code: 3240300, + data: {}, + message: 'Definition was declared but never used in document', + range: { + end: { + character: 15, + line: 27, + }, + start: { + character: 6, + line: 27, + }, + }, + severity: 2, + source: 'apilint', + }, { range: { start: { line: 30, character: 10 }, end: { line: 30, character: 14 } }, message: 'Schema does not include any Schema Object keywords', diff --git a/packages/apidom-ls/test/openapi-yaml.ts b/packages/apidom-ls/test/openapi-yaml.ts index a4f68efaae..975e199f70 100644 --- a/packages/apidom-ls/test/openapi-yaml.ts +++ b/packages/apidom-ls/test/openapi-yaml.ts @@ -520,6 +520,23 @@ describe('apidom-ls-yaml', function () { code: 7030101, source: 'apilint', }, + { + code: 3240300, + data: {}, + message: 'Definition was declared but never used in document', + range: { + end: { + character: 11, + line: 25, + }, + start: { + character: 4, + line: 25, + }, + }, + severity: 2, + source: 'apilint', + }, { range: { start: { line: 28, character: 8 }, end: { line: 28, character: 10 } }, message: 'Schema does not include any Schema Object keywords', diff --git a/packages/apidom-ls/test/validate-references.ts b/packages/apidom-ls/test/validate-references.ts index b6a5a22020..0efd8013ce 100644 --- a/packages/apidom-ls/test/validate-references.ts +++ b/packages/apidom-ls/test/validate-references.ts @@ -114,7 +114,7 @@ describe('reference validation', function () { }, }, { - range: { start: { line: 63, character: 16 }, end: { line: 63, character: 99 } }, + range: { start: { line: 68, character: 16 }, end: { line: 68, character: 99 } }, message: 'Reference Error - JSONPointerKeyError: Invalid object key "PetInvalid" at position 2 in "/components/schemas/PetInvalid": key not found in object', severity: 1, @@ -214,7 +214,7 @@ describe('reference validation', function () { }, }, { - range: { start: { line: 63, character: 16 }, end: { line: 63, character: 99 } }, + range: { start: { line: 68, character: 16 }, end: { line: 68, character: 99 } }, message: 'Reference Error - JSONPointerKeyError: Invalid object key "PetInvalid" at position 2 in "/components/schemas/PetInvalid": key not found in object', severity: 1, diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index fb4564857a..1c379f7f45 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3001,6 +3001,320 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + it('oas / yaml - ref is defined but not used', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'validation', 'oas', 'ref-not-used.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create('foo://bar/ref-not-used.yaml', 'yaml', 0, spec); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + result[0].code = 'test'; + const expected: Diagnostic[] = [ + { + range: { start: { line: 12, character: 4 }, end: { line: 12, character: 17 } }, + message: 'Definition was declared but never used in document', + severity: 2, + code: 'test', + source: 'apilint', + data: {}, + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - requestBody $refs must point to a position where can be legally placed', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'ref-request-bodies.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/ref-request-bodies.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + result[0].code = 'test'; + const expected: Diagnostic[] = [ + { + range: { start: { line: 15, character: 20 }, end: { line: 15, character: 88 } }, + message: + 'requestBody schema $refs must point to a position where a Schema Object can be legally placed', + severity: 1, + code: 'test', + source: 'apilint', + data: {}, + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - requestBody $refs must point to a position naming', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'ref-request-bodies-naming.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/ref-request-bodies-naming.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + result[0].code = 'test'; + result[1].code = 'test'; + const expected: Diagnostic[] = [ + { + range: { start: { line: 12, character: 14 }, end: { line: 12, character: 43 } }, + message: 'local reference not found', + severity: 1, + code: 'test', + source: 'apilint', + data: { + quickFix: [ + { + message: 'update to #/components/requestBodies/MyBody', + action: 'updateValue', + functionParams: ['#/components/requestBodies/MyBody'], + }, + ], + }, + }, + { + range: { start: { line: 12, character: 14 }, end: { line: 12, character: 43 } }, + message: + 'requestBody $refs must point to a position where a requestBody can be legally placed', + severity: 1, + code: 'test', + source: 'apilint', + data: {}, + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - requestBody $refs must point to a position naming schema', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'ref-request-bodies-naming-schema.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/ref-request-bodies-naming-schema.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + result[0].code = 'test'; + result[1].code = 'test'; + const expected: Diagnostic[] = [ + { + range: { start: { line: 12, character: 14 }, end: { line: 12, character: 43 } }, + message: 'local reference not found', + severity: 1, + code: 'test', + source: 'apilint', + data: { + quickFix: [ + { + message: 'update to #/components/requestBodies/MyBody', + action: 'updateValue', + functionParams: ['#/components/requestBodies/MyBody'], + }, + ], + }, + }, + { + range: { start: { line: 12, character: 14 }, end: { line: 12, character: 43 } }, + message: + "requestBody $refs cannot point to '#/components/schemas/…', they must point to '#/components/requestBodies/…'", + severity: 1, + code: 'test', + source: 'apilint', + data: {}, + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - OAS3 header $Ref should point to Header Object', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'validation', 'oas', 'ref-header.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create('foo://bar/ref-header.yaml', 'yaml', 0, spec); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + const result = await languageService.doValidation(doc, validationContext); + + result[0].code = 'test'; + + const expected: Diagnostic[] = [ + { + code: 'test', + data: { + quickFix: [ + { + action: 'updateValue', + functionParams: ['#/components/headers/MyHeader'], + message: 'update to #/components/headers/MyHeader', + }, + ], + }, + message: 'local reference not found', + range: { + end: { + character: 51, + line: 12, + }, + start: { + character: 20, + line: 12, + }, + }, + severity: 1, + source: 'apilint', + }, + { + code: 5260300, + data: {}, + message: 'OAS3 header $Ref should point to Header Object', + range: { + end: { + character: 51, + line: 12, + }, + start: { + character: 20, + line: 12, + }, + }, + severity: 1, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - OAS3 parameter $Ref should point to Parameter Object', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync(path.join(__dirname, 'fixtures', 'validation', 'oas', 'ref-parameter.yaml')) + .toString(); + const doc: TextDocument = TextDocument.create('foo://bar/ref-parameter.yaml', 'yaml', 0, spec); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + const result = await languageService.doValidation(doc, validationContext); + + result[0].code = 'test'; + + const expected: Diagnostic[] = [ + { + code: 'test', + data: { + quickFix: [], + }, + message: 'local reference not found', + range: { + end: { + character: 45, + line: 7, + }, + start: { + character: 14, + line: 7, + }, + }, + severity: 1, + source: 'apilint', + }, + { + code: 5260400, + data: {}, + message: 'OAS3 parameter $Ref should point to Parameter Object', + range: { + end: { + character: 45, + line: 7, + }, + start: { + character: 14, + line: 7, + }, + }, + severity: 1, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + it('oas / yaml - test editor issue 3626 / inidrect ref', async function () { const validationContext: ValidationContext = { comments: DiagnosticSeverity.Error, @@ -3022,7 +3336,25 @@ describe('apidom-ls-validate', function () { const result = await languageService.doValidation(doc, validationContext); result[0].code = 'test'; + result[1].code = 'test'; const expected: Diagnostic[] = [ + { + code: 'test', + data: {}, + message: 'Definition was declared but never used in document', + range: { + end: { + character: 7, + line: 31, + }, + start: { + character: 4, + line: 31, + }, + }, + severity: 2, + source: 'apilint', + }, { range: { start: { line: 39, character: 12 }, end: { line: 39, character: 50 } }, message: 'local reference not found', @@ -3173,6 +3505,14 @@ describe('apidom-ls-validate', function () { ], }, }, + { + range: { start: { line: 10, character: 4 }, end: { line: 10, character: 7 } }, + message: 'Definition was declared but never used in document', + severity: 2, + code: 3240300, + source: 'apilint', + data: {}, + }, ]; assert.deepEqual(result, expected as Diagnostic[]);