diff --git a/packages/apidom-ls/src/config/codes.ts b/packages/apidom-ls/src/config/codes.ts index 1aa0dd003a..4f9be6323f 100644 --- a/packages/apidom-ls/src/config/codes.ts +++ b/packages/apidom-ls/src/config/codes.ts @@ -701,6 +701,9 @@ enum ApilintCodes { OPENAPI2_OPERATION_FIELD_SECURITY_TYPE = 3081300, OPENAPI2_OPERATION_FIELD_SECURITY_ITEMS_TYPE, + OPENAPI2_PARAMETER = 3090000, + OPENAPI2_PARAMETER_FIELD_IS_DEFINED_WITHIN_PATH_TEMPLATE = 3090100, + OPENAPI3_0 = 5000000, OPENAPI3_0_OPENAPI_VALUE_PATTERN_3_0_0 = 5000100, 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 fc233c0fb3..f67817a8f7 100644 --- a/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/index.ts @@ -21,6 +21,7 @@ import examplesMutuallyExclusiveLint from './examples--mutually-exclusive'; import contentValuesTypeLint from './content--values-type'; import contentAllowedFields3_0Lint from './content--allowed-fields-3-0'; import contentAllowedFields3_1Lint from './content--allowed-fields-3-1'; +import isDefinedWithinPathTemplate from './is-defined-within-path-template'; const lints = [ nameTypeLint, @@ -46,6 +47,7 @@ const lints = [ requiredFieldsLint, allowedFields3_0Lint, allowedFields3_1Lint, + isDefinedWithinPathTemplate, ]; export default lints; diff --git a/packages/apidom-ls/src/config/openapi/parameter/lint/is-defined-within-path-template.ts b/packages/apidom-ls/src/config/openapi/parameter/lint/is-defined-within-path-template.ts new file mode 100644 index 0000000000..80fa136eac --- /dev/null +++ b/packages/apidom-ls/src/config/openapi/parameter/lint/is-defined-within-path-template.ts @@ -0,0 +1,23 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +import ApilintCodes from '../../../codes'; +import { LinterMeta } from '../../../../apidom-language-types'; +import { OpenAPI2, OpenAPI3 } from '../../target-specs'; + +const isDefinedWithinPathTemplate: LinterMeta = { + code: ApilintCodes.OPENAPI2_PARAMETER_FIELD_IS_DEFINED_WITHIN_PATH_TEMPLATE, + source: 'apilint', + message: 'parameter is not defined within path template', + severity: DiagnosticSeverity.Error, + linterFunction: 'apilintOpenAPIParameterFieldIsDefinedWithinPathTemplate', + marker: 'value', + targetSpecs: [...OpenAPI2, ...OpenAPI3], + conditions: [ + { + function: 'apilintOpenAPIPathTemplateWellFormed', + params: [true], + }, + ], +}; + +export default isDefinedWithinPathTemplate; diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index b453b8ac6f..0c9aa4d02a 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -1069,4 +1069,30 @@ export const standardLinterfunctions: FunctionItem[] = [ return true; }, }, + { + functionName: 'apilintOpenAPIParameterFieldIsDefinedWithinPathTemplate', + function: (element: Element) => { + if (element.element === 'parameter') { + const allowedLocations = ['path', 'query']; + const parameterName = toValue((element as ObjectElement).get('name')); + const parameterLocation = toValue((element as ObjectElement).get('in')); + + const isAncestorOfOperationElement = (el: Element): boolean => + el.parent.parent.parent.element === 'operation'; + + if (isAncestorOfOperationElement(element)) { + const pathTemplate: string = toValue( + (element.parent.parent.parent.parent.parent.parent as MemberElement).key, + ); + + return ( + pathTemplate.includes(parameterName) && allowedLocations.includes(parameterLocation) + ); + + // TODO: handle when parameter is not ancestor of operation element + } + } + return true; + }, + }, ]; diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml new file mode 100644 index 0000000000..ad473f3aee --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-2-0.yaml @@ -0,0 +1,39 @@ +swagger: '2.0' +info: + title: Foo + version: 0.1.0 +parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/{bar_id}: + delete: + summary: Delete foo bar id + operationId: deleteFooBar + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml new file mode 100644 index 0000000000..7052584be3 --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-0.yaml @@ -0,0 +1,40 @@ +openapi: 3.0.0 +info: + title: Foo + version: 0.1.0 +components: + parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/{bar_id}: + delete: + summary: Delete foo bar id + operationId: deleteFooBar + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} diff --git a/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml new file mode 100644 index 0000000000..97c5d0ea9a --- /dev/null +++ b/packages/apidom-ls/test/fixtures/validation/oas/parameter-defined-within-path-template-3-1.yaml @@ -0,0 +1,40 @@ +openapi: 3.1.0 +info: + title: Foo + version: 0.1.0 +components: + parameters: + test_id: + name: test_id + in: path + required: true + schema: + type: string + format: uuid + title: Test Id +paths: + /foo/{bar_id}: + delete: + summary: Delete foo bar id + operationId: deleteFooBar + parameters: + - name: foo_id + in: path + required: true + schema: + type: string + format: uuid + title: Foo Id + - name: bar_id + in: path + required: true + schema: + type: string + format: uuid + title: Bar Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} diff --git a/packages/apidom-ls/test/validate.ts b/packages/apidom-ls/test/validate.ts index 38084135b7..45e49c3151 100644 --- a/packages/apidom-ls/test/validate.ts +++ b/packages/apidom-ls/test/validate.ts @@ -3549,4 +3549,130 @@ describe('apidom-ls-validate', function () { languageService.terminate(); }); + + it('oas 2.0 / yaml - parameter object should be defined within path template', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-defined-within-path-template-2-0.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-defined-within-path-template-2-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 19, character: 10 }, end: { line: 25, character: 25 } }, + message: 'parameter is not defined within path template', + severity: 1, + code: 3090100, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); + + it('oas 3.0 / yaml - parameter object should be defined within path template', async function () { + const validationContext: ValidationContext = { + comments: DiagnosticSeverity.Error, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-defined-within-path-template-3-0.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-defined-within-path-template-3-0.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 20, character: 10 }, end: { line: 26, character: 25 } }, + message: 'parameter is not defined within path template', + severity: 1, + code: 3090100, + 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, + maxNumberOfProblems: 100, + relatedInformation: false, + }; + + const spec = fs + .readFileSync( + path.join( + __dirname, + 'fixtures', + 'validation', + 'oas', + 'parameter-defined-within-path-template-3-1.yaml', + ), + ) + .toString(); + const doc: TextDocument = TextDocument.create( + 'foo://bar/parameter-defined-within-path-template-3-1.yaml', + 'yaml', + 0, + spec, + ); + + const languageService: LanguageService = getLanguageService(contextNoSchema); + + const result = await languageService.doValidation(doc, validationContext); + const expected: Diagnostic[] = [ + { + range: { start: { line: 20, character: 10 }, end: { line: 26, character: 25 } }, + message: 'parameter is not defined within path template', + severity: 1, + code: 3090100, + source: 'apilint', + }, + ]; + assert.deepEqual(result, expected as Diagnostic[]); + + languageService.terminate(); + }); });