Skip to content

feat: add ref validation rules #4989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/apidom-ls/src/config/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@ enum ApilintCodes {

OPENAPI2_REFERENCE = 3240000,
OPENAPI2_REFERENCE_FIELD_$REF_FORMAT_URI = 3240100,
OPENAPI2_REFERENCE_NOT_USED = 3240300,

OPENAPI3_0 = 5000000,

Expand Down Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions packages/apidom-ls/src/config/common/schema/lint/$ref--not-used.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions packages/apidom-ls/src/config/common/schema/lint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,13 +82,16 @@ 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,
allowedFieldsOpenAPI3_0Lint,
$idFormatURILint,
$refValidLint,
$refNoSiblingsLint,
$refNotUsedLint,
$ref3RequestBodiesLint,
additionalItemsNonArrayLint,
additionalItemsTypeLint,
additionalItemsTypeOpenAPI3_1__AsyncAPI2Lint,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions packages/apidom-ls/src/config/openapi/header/lint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions packages/apidom-ls/src/config/openapi/parameter/lint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/apidom-ls/src/config/openapi/schema/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -154,6 +156,8 @@ const schemaLints = [
uniqueItemsTypeLint,
writeOnlyTypeLint,
exampleDeprecatedLint,
$refNotUsedLint,
$ref3RequestBodiesLint,
];

export default schemaLints;
36 changes: 36 additions & 0 deletions packages/apidom-ls/src/services/validation/linter-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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) => {
Expand Down
Loading