From bb2f0ccb008450f65970bbb7c071c3c53b8f7036 Mon Sep 17 00:00:00 2001 From: Oliwia Rogala Date: Fri, 28 Nov 2025 10:21:26 +0100 Subject: [PATCH] perf(ls): add cache for validation functions --- .../message-trait/lint/message-id--unique.ts | 2 +- .../message/lint/message-id--unique.ts | 2 +- .../lint/operation-id--unique.ts | 2 +- .../operation/lint/operation-id--unique.ts | 2 +- .../common/schema/lint/$ref--not-used.ts | 2 +- .../operation/lint/operation-id--unique.ts | 2 +- .../services/validation/linter-functions.ts | 82 +++++++++++++------ .../services/validation/validation-service.ts | 21 ++++- 8 files changed, 84 insertions(+), 31 deletions(-) diff --git a/packages/apidom-ls/src/config/asyncapi/message-trait/lint/message-id--unique.ts b/packages/apidom-ls/src/config/asyncapi/message-trait/lint/message-id--unique.ts index 8601eb38e3..b585132ff5 100644 --- a/packages/apidom-ls/src/config/asyncapi/message-trait/lint/message-id--unique.ts +++ b/packages/apidom-ls/src/config/asyncapi/message-trait/lint/message-id--unique.ts @@ -9,7 +9,7 @@ const messageIdUniqueLint: LinterMeta = { message: "messageID' must be unique among all messages", severity: DiagnosticSeverity.Error, linterFunction: 'apilintPropertyUniqueValue', - linterParams: [['message', 'messageTrait'], 'messageId'], + linterParams: [['message', 'messageTrait'], 'messageId', 'propertyValues'], marker: 'key', markerTarget: 'messageId', target: 'messageId', diff --git a/packages/apidom-ls/src/config/asyncapi/message/lint/message-id--unique.ts b/packages/apidom-ls/src/config/asyncapi/message/lint/message-id--unique.ts index 8a91976c88..8678f5e00a 100644 --- a/packages/apidom-ls/src/config/asyncapi/message/lint/message-id--unique.ts +++ b/packages/apidom-ls/src/config/asyncapi/message/lint/message-id--unique.ts @@ -9,7 +9,7 @@ const messageIdUniqueLint: LinterMeta = { message: "messageID' must be unique among all messages", severity: DiagnosticSeverity.Error, linterFunction: 'apilintPropertyUniqueValue', - linterParams: [['message', 'messageTrait'], 'messageId'], + linterParams: [['message', 'messageTrait'], 'messageId', 'propertyValues'], marker: 'key', markerTarget: 'messageId', target: 'messageId', diff --git a/packages/apidom-ls/src/config/asyncapi/operation-trait/lint/operation-id--unique.ts b/packages/apidom-ls/src/config/asyncapi/operation-trait/lint/operation-id--unique.ts index a5892ae48b..5b63c04d3e 100644 --- a/packages/apidom-ls/src/config/asyncapi/operation-trait/lint/operation-id--unique.ts +++ b/packages/apidom-ls/src/config/asyncapi/operation-trait/lint/operation-id--unique.ts @@ -9,7 +9,7 @@ const operationIdUniqueLint: LinterMeta = { message: "operationId' must be unique among all operations", severity: DiagnosticSeverity.Error, linterFunction: 'apilintPropertyUniqueValue', - linterParams: [['operation', 'operationTrait'], 'operationId'], + linterParams: [['operation', 'operationTrait'], 'operationId', 'propertyValues'], marker: 'key', markerTarget: 'operationId', target: 'operationId', diff --git a/packages/apidom-ls/src/config/asyncapi/operation/lint/operation-id--unique.ts b/packages/apidom-ls/src/config/asyncapi/operation/lint/operation-id--unique.ts index 08855232fa..aaefe3c5bb 100644 --- a/packages/apidom-ls/src/config/asyncapi/operation/lint/operation-id--unique.ts +++ b/packages/apidom-ls/src/config/asyncapi/operation/lint/operation-id--unique.ts @@ -9,7 +9,7 @@ const operationIdUniqueLint: LinterMeta = { message: "operationId' must be unique among all operations", severity: DiagnosticSeverity.Error, linterFunction: 'apilintPropertyUniqueValue', - linterParams: [['operation', 'operationTrait'], 'operationId'], + linterParams: [['operation', 'operationTrait'], 'operationId', 'propertyValues'], marker: 'key', markerTarget: 'operationId', target: 'operationId', 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 index 058744d011..1fd5b9a922 100644 --- 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 @@ -10,7 +10,7 @@ const $refNotUsedLint: LinterMeta = { message: 'Definition was declared but never used in document', severity: DiagnosticSeverity.Warning, linterFunction: 'apilintReferenceNotUsed', - linterParams: ['string'], + linterParams: ['referenceNames'], marker: 'key', data: {}, targetSpecs: [...OpenAPI2, ...OpenAPI30], diff --git a/packages/apidom-ls/src/config/openapi/operation/lint/operation-id--unique.ts b/packages/apidom-ls/src/config/openapi/operation/lint/operation-id--unique.ts index 07df6dd687..a50d5919ff 100644 --- a/packages/apidom-ls/src/config/openapi/operation/lint/operation-id--unique.ts +++ b/packages/apidom-ls/src/config/openapi/operation/lint/operation-id--unique.ts @@ -10,7 +10,7 @@ const operationIdUniqueLint: LinterMeta = { message: "operationId' must be unique among all operations", severity: DiagnosticSeverity.Error, linterFunction: 'apilintPropertyUniqueValue', - linterParams: [['operation'], 'operationId'], + linterParams: [['operation'], 'operationId', 'propertyValues'], marker: 'key', markerTarget: 'operationId', target: 'operationId', diff --git a/packages/apidom-ls/src/services/validation/linter-functions.ts b/packages/apidom-ls/src/services/validation/linter-functions.ts index 58ca2565b1..b31f15d0ee 100644 --- a/packages/apidom-ls/src/services/validation/linter-functions.ts +++ b/packages/apidom-ls/src/services/validation/linter-functions.ts @@ -10,6 +10,8 @@ import { ObjectElement, isArrayElement, includesClasses, + isObjectElement, + traverse, } from '@swagger-api/apidom-core'; import { URIFragmentIdentifier } from '@swagger-api/apidom-json-pointer/modern'; import { CompletionItem } from 'vscode-languageserver-types'; @@ -956,22 +958,42 @@ export const standardLinterfunctions: FunctionItem[] = [ }, { functionName: 'apilintPropertyUniqueValue', - function: (element: Element, elementOrClasses: string[], key: string): boolean => { + function: ( + element: Element, + elementOrClasses: string[], + key: string, + propertyValues: Map, + ): boolean => { const api = root(element); const value = toValue(element); - const elements: ArraySlice = filter((el: Element) => { - const classes: string[] = toValue(el.getMetaProperty('classes', [])); - return ( - (elementOrClasses.includes(el.element) || - classes.every((v) => elementOrClasses.includes(v))) && - isObject(el) && - el.hasKey(key) && - toValue(el.get(key)) === value - ); - }, api); - if (elements.length > 1) { + const cacheKey = elementOrClasses.join(','); + + if (!propertyValues.has(cacheKey)) { + traverse((el: Element) => { + const classes: ArrayElement = el.getMetaProperty('classes', []); + if ( + (elementOrClasses.includes(el.element) || + classes.filter((classElement: Element) => + elementOrClasses.includes(toValue(classElement)), + ).length === classes.length) && + isObject(el) && + el.hasKey(key) + ) { + const elValue = toValue(el.get(key)); + const cachedValues = propertyValues.get(cacheKey) ?? []; + + cachedValues.push(elValue); + propertyValues.set(cacheKey, cachedValues); + } + }, api); + } + + const cachedValues = propertyValues.get(cacheKey) ?? []; + + if (cachedValues.filter((cachedValue) => cachedValue === value).length > 1) { return false; } + return true; }, }, @@ -1327,7 +1349,7 @@ export const standardLinterfunctions: FunctionItem[] = [ }, { functionName: 'apilintReferenceNotUsed', - function: (element: Element & { content?: { key?: string } }) => { + function: (element: Element & { content?: { key?: string } }, referenceNames: string[]) => { const elParent: Element = element.parent?.parent?.parent?.parent; if ( (typeof elParent?.hasKey !== 'function' || !elParent.hasKey('schemas')) && @@ -1336,17 +1358,29 @@ export const standardLinterfunctions: FunctionItem[] = [ 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 + if (referenceNames.length === 0) { + const api = root(element); + const isReferenceElement = (el: Element & { content?: { key?: string } }) => { + if (!isObjectElement(el) || !el.hasKey('$ref')) { + return false; + } + + const $ref = el.get('$ref'); + + return isStringElement($ref) && toValue($ref).startsWith('#'); + }; + + const referenceElements = filter((el) => { + return isReferenceElement(el); + }, api); + + referenceNames.push( + ...referenceElements.map((refElement: ObjectElement) => + toValue(refElement.get('$ref')).split('/').at(-1), + ), + ); + } + return referenceNames.includes(toValue(element.parent.key)); }, }, diff --git a/packages/apidom-ls/src/services/validation/validation-service.ts b/packages/apidom-ls/src/services/validation/validation-service.ts index e0a2ef3df0..49abb4688a 100644 --- a/packages/apidom-ls/src/services/validation/validation-service.ts +++ b/packages/apidom-ls/src/services/validation/validation-service.ts @@ -76,6 +76,10 @@ export class DefaultValidationService implements ValidationService { private lintingRulesSemanticCache: Map = new Map(); + private referenceNamesCache: string[] = []; + + private propertyValuesCache: Map = new Map(); + public constructor() { this.validationEnabled = true; this.commentSeverity = undefined; @@ -838,6 +842,9 @@ export class DefaultValidationService implements ValidationService { } } + this.referenceNamesCache = []; + this.propertyValuesCache.clear(); + return diagnostics; } @@ -894,7 +901,19 @@ export class DefaultValidationService implements ValidationService { Array.isArray(meta.linterParams) && meta.linterParams.length > 0 ) { - const params = [targetElement].concat(meta.linterParams); + const params = [targetElement].concat( + meta.linterParams.map((param) => { + if (param === 'referenceNames') { + return this.referenceNamesCache; + } + + if (param === 'propertyValues') { + return this.propertyValuesCache; + } + + return param; + }), + ); lintRes = lintFunc(...params) as boolean; } else { lintRes = lintFunc(targetElement) as boolean;