diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index 0ce246d7c3..fbaad4db32 100644 --- a/packages/apidom-ls/src/config/codes.ts +++ b/packages/apidom-ls/src/config/codes.ts @@ -717,6 +717,7 @@ enum ApilintCodes { OPENAPI2_PARAMETER_FIELD_IN_EQUALS = 3100200, OPENAPI2_PARAMETER_FIELD_IN_REQUIRED, OPENAPI2_PARAMETER_FIELD_IN_VALID, + OPENAPI2_PARAMETER_FIELD_IN_MULTIPLE_BODY, OPENAPI2_PARAMETER_FIELD_DESCRIPTION_TYPE = 3100300, OPENAPI2_PARAMETER_FIELD_REQUIRED_TYPE = 3100400, OPENAPI2_PARAMETER_FIELD_REQUIRED_REQUIRED, @@ -740,6 +741,7 @@ enum ApilintCodes { OPENAPI2_PARAMETER_FIELD_ENUM_TYPE = 3101800, OPENAPI2_PARAMETER_FIELD_MULTIPLE_OF_TYPE = 3101900, OPENAPI2_PARAMETER_FIELD_IN_PATH_TEMPLATE = 3102000, + OPENAPI2_PARAMETER_FIELD_NAME_UNIQUE = 3103000, OPENAPI2_ITEMS = 3110000, OPENAPI2_ITEMS_FIELD_TYPE_EQUALS = 3110100, @@ -976,6 +978,8 @@ enum ApilintCodes { OPENAPI3_0_PARAMETER_FIELD_IN_TYPE = 5150300, OPENAPI3_0_PARAMETER_FIELD_IN_REQUIRED, OPENAPI3_0_PARAMETER_FIELD_IN_EQUALS, + OPENAPI3_0_PARAMETER_FIELD_IN_AUTHORIZATION, + OPENAPI3_0_PARAMETER_FIELD_IN_CONTENT_TYPE, OPENAPI3_0_PARAMETER_FIELD_DESCRIPTION_TYPE = 5150400, OPENAPI3_0_PARAMETER_FIELD_REQUIRED_TYPE = 5150500, OPENAPI3_0_PARAMETER_FIELD_REQUIRED_REQUIRED, diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/in--accept.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/in--accept.ts new file mode 100644 index 0000000000..f435f9eccd --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/in--accept.ts @@ -0,0 +1,31 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import { OpenAPI3 } from '../../target-specs.ts'; +import ApilintCodes from '../../../codes.ts'; + +const inAcceptLint = { + code: ApilintCodes.OPENAPI3_0_PARAMETER_FIELD_IN_CONTENT_TYPE, + source: 'apilint', + message: + 'Header Parameter named "Accept" is ignored. The values for the "Accept" header are defined by `requestBody.content.`', + severity: DiagnosticSeverity.Warning, + linterParams: ['^(?!\\b(A|a)ccept\\b).*$'], + linterFunction: 'apilintValueRegex', + marker: 'value', + target: 'name', + data: {}, + conditions: [ + { + targets: [ + { + path: 'in', + }, + ], + function: 'apilintContainsValue', + params: ['header'], + }, + ], + targetSpecs: OpenAPI3, +}; + +export default inAcceptLint; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/in--authorization.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/in--authorization.ts new file mode 100644 index 0000000000..80a1ce274a --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/in--authorization.ts @@ -0,0 +1,31 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import { OpenAPI3 } from '../../target-specs.ts'; +import ApilintCodes from '../../../codes.ts'; + +const inAuthorizationLint = { + code: ApilintCodes.OPENAPI3_0_PARAMETER_FIELD_IN_AUTHORIZATION, + source: 'apilint', + message: + 'Header Parameter named "Authorization" is ignored. Use the "securitySchemes" and "security" sections instead to define authorization', + severity: DiagnosticSeverity.Warning, + linterParams: ['^(?!\\b(A|a)uthorization\\b).*$'], + linterFunction: 'apilintValueRegex', + marker: 'value', + target: 'name', + data: {}, + conditions: [ + { + targets: [ + { + path: 'in', + }, + ], + function: 'apilintContainsValue', + params: ['header'], + }, + ], + targetSpecs: OpenAPI3, +}; + +export default inAuthorizationLint; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/in--content-type.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/in--content-type.ts new file mode 100644 index 0000000000..d4db11f932 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/in--content-type.ts @@ -0,0 +1,31 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import { OpenAPI3 } from '../../target-specs.ts'; +import ApilintCodes from '../../../codes.ts'; + +const inContentTypeLint = { + code: ApilintCodes.OPENAPI3_0_PARAMETER_FIELD_IN_CONTENT_TYPE, + source: 'apilint', + message: + 'Header Parameter named "Content-Type" is ignored. The values for the "Content-Type" header are defined by `requestBody.content.`', + severity: DiagnosticSeverity.Warning, + linterParams: ['^(?!\\b(C|c)ontent-(T|t)ype\\b).*$'], + linterFunction: 'apilintValueRegex', + marker: 'value', + target: 'name', + data: {}, + conditions: [ + { + targets: [ + { + path: 'in', + }, + ], + function: 'apilintContainsValue', + params: ['header'], + }, + ], + targetSpecs: OpenAPI3, +}; + +export default inContentTypeLint; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/in--multiple-body.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/in--multiple-body.ts new file mode 100644 index 0000000000..80ac42001c --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/in--multiple-body.ts @@ -0,0 +1,27 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import { OpenAPI2 } from '../../target-specs.ts'; +import ApilintCodes from '../../../codes.ts'; + +const inMultipleBody = { + code: ApilintCodes.OPENAPI2_PARAMETER_FIELD_IN_MULTIPLE_BODY, + source: 'apilint', + message: 'Multiple body parameters are not allowed', + severity: DiagnosticSeverity.Error, + linterFunction: 'apilintPropertyUniqueSiblingValue', + linterParams: ['parameters', 'in'], + marker: 'key', + markerTarget: 'in', + target: 'in', + conditions: [ + { + targets: [{ path: 'in' }], + function: 'apilintContainsValue', + params: ['body'], + }, + ], + data: {}, + targetSpecs: OpenAPI2, +}; + +export default inMultipleBody; 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..1ef0023df1 100644 --- a/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts @@ -42,12 +42,18 @@ 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 uniqueNameLint from './unique-name.ts'; +import inAuthorizationLint from './in--authorization.ts'; +import inContentTypeLint from './in--content-type.ts'; +import inAcceptLint from './in--accept.ts'; +import inMultipleBody from './in--multiple-body.ts'; const lints = [ nameTypeLint, nameRequiredLint, inEquals2_0Lint, inEquals3_0__3_1Lint, + inAuthorizationLint, inRequiredLint, inValidLint, descriptionTypeLint, @@ -81,6 +87,7 @@ const lints = [ maxLengthTypeLint, minLengthTypeLint, uniqueItemsTypeLint, + uniqueNameLint, enumTypeLint, multipleOfTypeLint, requiredFields3_0__3_1Lint, @@ -88,6 +95,9 @@ const lints = [ allowedFields3_0Lint, allowedFields3_1Lint, inPathTemplateLint, + inContentTypeLint, + inAcceptLint, + inMultipleBody, ]; export default lints; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/unique-name.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/unique-name.ts new file mode 100644 index 0000000000..15de868919 --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/unique-name.ts @@ -0,0 +1,21 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes.ts'; +import { LinterMeta } from '../../../../apidom-language-types.ts'; +import { OpenAPI2, OpenAPI3 } from '../../target-specs.ts'; + +const parametersTypeLint: LinterMeta = { + code: ApilintCodes.OPENAPI2_PARAMETER_FIELD_NAME_UNIQUE, + source: 'apilint', + message: 'Name must be unique among all parameters', + severity: DiagnosticSeverity.Error, + linterFunction: 'apilintPropertyUniqueSiblingValue', + linterParams: ['parameters', 'name'], + marker: 'key', + markerTarget: 'name', + target: 'name', + data: {}, + targetSpecs: [...OpenAPI2, ...OpenAPI3], +}; + +export default parametersTypeLint; diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 196d248c27..9171020607 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -795,6 +795,30 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'apilintPropertyUniqueSiblingValue', + function: (element, elementOrClasses, key) => { + const value = toValue(element); + + const filterSiblingsOAS2 = ( + el: Element & { key?: { content?: string }; content: { value?: string } }, + ) => isString(el) && el.key?.content === key && toValue(el.content.value) === value; + + const filterSiblingsOAS3 = (el: Element) => + isObject(el) && el.hasKey(key) && toValue(el.get(key)) === value; + + const elements = filter((el: Element) => { + const classes: string[] = toValue(el.getMetaProperty('classes', [])); + + return ( + (elementOrClasses.includes(el.element) || + classes.every((v) => elementOrClasses.includes(v))) && + (filterSiblingsOAS2(el) || filterSiblingsOAS3(el)) + ); + }, element.parent?.parent?.parent); + return elements.length <= 1; + }, + }, { functionName: 'apilintChannelParameterExist', function: (element: Element): boolean => { diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-accept-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-accept-3-0.yaml new file mode 100644 index 0000000000..09a9d1b42c --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-accept-3-0.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /: + get: + responses: + default: + description: 'test' + parameters: + - in: header + name: accept + content: + application/xml: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-authorization-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-authorization-3-0.yaml new file mode 100644 index 0000000000..8084abb481 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-authorization-3-0.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /: + get: + responses: + default: + description: 'test' + parameters: + - in: header + name: Authorization + content: + application/xml: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-content-type-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-content-type-3-0.yaml new file mode 100644 index 0000000000..605172396d --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-content-type-3-0.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /: + get: + responses: + default: + description: 'test' + parameters: + - in: header + name: content-type + content: + application/xml: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-multiple-body.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-multiple-body.yaml new file mode 100644 index 0000000000..78f366d0b9 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-in-multiple-body.yaml @@ -0,0 +1,19 @@ +swagger: '2.0' +info: + title: 'test' + version: 1.0.0 +paths: + /: + get: + responses: + default: + description: 'test' + consumes: + - multipart/form-data + parameters: + - in: body + name: Else1 + schema: {} + - in: body + name: Else2 + schema: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-unique-name-2-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-unique-name-2-0.yaml new file mode 100644 index 0000000000..51fcd46379 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-unique-name-2-0.yaml @@ -0,0 +1,21 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: 'test' +paths: + /pets: + get: + responses: + parameters: + - name: pathLevel + type: string + in: query + description: tags to filter by + schema: + type: string + - name: pathLevel + type: string + in: query + description: tags to filter by + schema: + type: string diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-unique-name-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-unique-name-3-0.yaml new file mode 100644 index 0000000000..5bcb126e6d --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-unique-name-3-0.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: 'test' +paths: + /pets: + get: + responses: + parameters: + - name: pathLevel + in: query + description: tags to filter by + schema: + type: string + - name: pathLevel + in: query + description: tags to filter by + schema: + type: string diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index fb4564857a..8e8249faf8 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3651,6 +3651,342 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + it('oas 3.0 / yaml - parameter name should be unique', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'parameter-unique-name-3-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-unique-name-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 3103000, + data: {}, + message: 'Name must be unique among all parameters', + range: { + end: { + character: 14, + line: 9, + }, + start: { + character: 10, + line: 9, + }, + }, + severity: 1, + source: 'apilint', + }, + { + code: 3103000, + data: {}, + message: 'Name must be unique among all parameters', + range: { + end: { + character: 14, + line: 14, + }, + start: { + character: 10, + line: 14, + }, + }, + severity: 1, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 2.0 / yaml - parameter name should be unique', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'parameter-unique-name-2-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-unique-name-2-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 3103000, + data: {}, + message: 'Name must be unique among all parameters', + range: { + end: { + character: 14, + line: 9, + }, + start: { + character: 10, + line: 9, + }, + }, + severity: 1, + source: 'apilint', + }, + { + code: 3103000, + data: {}, + message: 'Name must be unique among all parameters', + range: { + end: { + character: 14, + line: 15, + }, + start: { + character: 10, + line: 15, + }, + }, + severity: 1, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - parameter Authorization is ignored', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-in-authorization-3-0.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-in-authorization-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 5150303, + data: {}, + message: + 'Header Parameter named "Authorization" is ignored. Use the "securitySchemes" and "security" sections instead to define authorization', + range: { + end: { + character: 29, + line: 12, + }, + start: { + character: 16, + line: 12, + }, + }, + severity: 2, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - parameter content-type is ignored', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'parameter-in-content-type-3-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-in-authorization-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 5150304, + data: {}, + message: + 'Header Parameter named "Content-Type" is ignored. The values for the "Content-Type" header are defined by `requestBody.content.`', + range: { + end: { + character: 28, + line: 12, + }, + start: { + character: 16, + line: 12, + }, + }, + severity: 2, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - parameter accept is ignored', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'parameter-in-accept-3-0.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-in-accept-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 5150304, + data: {}, + message: + 'Header Parameter named "Accept" is ignored. The values for the "Accept" header are defined by `requestBody.content.`', + range: { + end: { + character: 22, + line: 12, + }, + start: { + character: 16, + line: 12, + }, + }, + severity: 2, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 2.0 / yaml - Multiple body parameters are not allowed', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join(__dirname, 'fixtures', 'validation', 'oas', 'parameter-in-multiple-body.yaml'), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-in-multiple-body.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + code: 3100203, + data: {}, + message: 'Multiple body parameters are not allowed', + range: { + end: { + character: 12, + line: 13, + }, + start: { + character: 10, + line: 13, + }, + }, + severity: 1, + source: 'apilint', + }, + { + code: 3100203, + data: {}, + message: 'Multiple body parameters are not allowed', + range: { + end: { + character: 12, + line: 16, + }, + start: { + character: 10, + line: 16, + }, + }, + severity: 1, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + it('oas 3.1 / yaml - parameter object should be defined within path template', async function () { const validationContext: ValidationContext = { comments: DiagnosticSeverity.Error,