Skip to content
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
7 changes: 7 additions & 0 deletions packages/apidom-ast/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ export type { YamlAnchorOptions } from './yaml/nodes/YamlAnchor.ts';
export { YamlStyle, YamlStyleGroup } from './yaml/nodes/YamlStyle.ts';
export { default as YamlFailsafeSchema } from './yaml/schemas/failsafe/index.ts';
export { default as YamlJsonSchema } from './yaml/schemas/json/index.ts';
export {
formatFlowPlain,
formatFlowSingleQuoted,
formatFlowDoubleQuoted,
formatBlockLiteral,
formatBlockFolded,
} from './yaml/schemas/canonical-format.ts';
export { default as YamlReferenceManager } from './yaml/anchors-aliases/ReferenceManager.ts';
export {
isAlias as isYamlAlias,
Expand Down
5 changes: 5 additions & 0 deletions packages/apidom-ast/src/yaml/schemas/canonical-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const removeQuotes = curry((quoteType, val) =>
/**
* Formats Flow Scalar Plain style.
* https://yaml.org/spec/1.2/spec.html#id2788859
* @public
*/
export const formatFlowPlain = pipe(
normalizeLineBreaks,
Expand All @@ -123,6 +124,7 @@ export const formatFlowPlain = pipe(
/**
* Formats Flow Scalar Single-Quoted style.
* https://yaml.org/spec/1.2/spec.html#id2788097
* @public
*/

export const formatFlowSingleQuoted = pipe(
Expand All @@ -138,6 +140,7 @@ export const formatFlowSingleQuoted = pipe(
/**
* Formats Flow Scalar Double-Quoted style.
* https://yaml.org/spec/1.2/spec.html#id2787109
* @public
*/
export const formatFlowDoubleQuoted = pipe(
normalizeLineBreaks,
Expand All @@ -154,6 +157,7 @@ export const formatFlowDoubleQuoted = pipe(
/**
* Formats Block Scalar Literal style.
* https://yaml.org/spec/1.2/spec.html#id2795688
* @public
*/
export const formatBlockLiteral = (content: string): string => {
const indentation = getIndentation(content);
Expand All @@ -170,6 +174,7 @@ export const formatBlockLiteral = (content: string): string => {
/**
* Formats BLock Scalar Folded style.
* https://yaml.org/spec/1.2/spec.html#id2796251
* @public
*/
export const formatBlockFolded = (content: string): string => {
const indentation = getIndentation(content);
Expand Down
1 change: 1 addition & 0 deletions packages/apidom-ls/src/apidom-language-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ export interface ValidationContext {
referenceValidationMode?: ReferenceValidationMode;
referenceValidationSequentialProcessing?: boolean;
referenceValidationContinueOnError?: boolean;
signal?: AbortSignal;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const inMultipleBody = {
message: 'Multiple body parameters are not allowed',
severity: DiagnosticSeverity.Error,
linterFunction: 'apilintPropertyUniqueSiblingValue',
linterParams: ['parameters', 'in'],
linterParams: ['parameters', 'in', 'propertySiblingValues'],
marker: 'key',
markerTarget: 'in',
target: 'in',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const parametersTypeLint: LinterMeta = {
message: 'Name must be unique among all parameters',
severity: DiagnosticSeverity.Error,
linterFunction: 'apilintPropertyUniqueSiblingValue',
linterParams: ['parameters', 'name'],
linterParams: ['parameters', 'name', 'propertySiblingValues'],
marker: 'key',
markerTarget: 'name',
target: 'name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const nameUniqueLint: LinterMeta = {
message: 'Tag Objects must have unique `name` field values.',
severity: DiagnosticSeverity.Error,
linterFunction: 'apilintPropertyUniqueSiblingValue',
linterParams: ['tags', 'name'],
linterParams: ['tags', 'name', 'propertySiblingValues'],
marker: 'value',
target: 'name',
markerTarget: 'name',
Expand Down
139 changes: 90 additions & 49 deletions packages/apidom-ls/src/services/validation/linter-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1037,63 +1037,102 @@ export const standardLinterfunctions: FunctionItem[] = [
element: Element,
elementOrClasses: string[],
key: string,
propertyValues: Map<string, string[]>,
propertyValues: Map<string, Set<unknown>>,
): boolean => {
const api = root(element);
const value = toValue(element);
const cacheKey = elementOrClasses.join(',');

if (!propertyValues.has(cacheKey) && api) {
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);
}
if (!propertyValues.has(cacheKey)) {
const api = root(element);

const cachedValues = propertyValues.get(cacheKey) ?? [];
if (api) {
const counts = new Map<unknown, number>();

if (cachedValues.filter((cachedValue) => cachedValue === value).length > 1) {
return false;
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));
counts.set(elValue, (counts.get(elValue) ?? 0) + 1);
}
}, api);

const duplicates = new Set<unknown>();
counts.forEach((count, val) => {
if (count > 1) duplicates.add(val);
});
propertyValues.set(cacheKey, duplicates);
}
}

return true;
const duplicates = propertyValues.get(cacheKey);
return duplicates === undefined || !duplicates.has(toValue(element));
},
},
{
functionName: 'apilintPropertyUniqueSiblingValue',
function: (element, elementOrClasses, key) => {
function: (
element,
elementOrClasses,
key,
propertySiblingValues: WeakMap<Element, Map<string, Set<unknown>>>,
) => {
const parent = element.parent?.parent?.parent;

if (!parent) {
return true;
}

const value = toValue(element);
const cacheKey = `${elementOrClasses}:${key}`;

const filterSiblingsOAS2 = (
el: Element & { key?: { content?: string }; content: { value?: string } },
) => isString(el) && el.key?.content === key && toValue(el.content.value) === value;
let containerCache = propertySiblingValues.get(parent);

const filterSiblingsOAS3 = (el: Element) =>
isObject(el) && el.hasKey(key) && toValue(el.get(key)) === value;
if (!containerCache) {
containerCache = new Map();
propertySiblingValues.set(parent, containerCache);
}

const elements = filter((el: Element) => {
const classes: string[] = toValue(el.getMetaProperty('classes', []));
if (!containerCache.has(cacheKey)) {
const counts = new Map<unknown, number>();

return (
(elementOrClasses.includes(el.element) ||
classes.every((v) => elementOrClasses.includes(v))) &&
(filterSiblingsOAS2(el) || filterSiblingsOAS3(el))
);
}, element.parent?.parent?.parent);
return elements.length <= 1;
traverse((el: Element) => {
const classes: string[] = toValue(el.getMetaProperty('classes', []));
const typeMatches =
elementOrClasses.includes(el.element) ||
classes.every((v: string) => elementOrClasses.includes(v));

if (typeMatches) {
if (isObject(el) && el.hasKey(key)) {
const elValue = toValue(el.get(key));
counts.set(elValue, (counts.get(elValue) ?? 0) + 1);
} else if (
isString(el) &&
(el as Element & { key?: { content?: string } }).key?.content === key
) {
const elValue = toValue(
(el as Element & { content: { value?: string } }).content.value,
);
counts.set(elValue, (counts.get(elValue) ?? 0) + 1);
}
}
}, parent);

const duplicates = new Set<unknown>();
counts.forEach((count, val) => {
if (count > 1) duplicates.add(val);
});

containerCache.set(cacheKey, duplicates);
}

return !containerCache.get(cacheKey)!.has(value);
},
},
{
Expand Down Expand Up @@ -1448,7 +1487,7 @@ export const standardLinterfunctions: FunctionItem[] = [
},
{
functionName: 'apilintReferenceNotUsed',
function: (element: Element & { content?: { key?: string } }, referenceNames: string[]) => {
function: (element: Element & { content?: { key?: string } }, referenceNames: Set<string>) => {
const elParent: Element = element.parent?.parent?.parent?.parent;
if (
(typeof elParent?.hasKey !== 'function' || !elParent.hasKey('schemas')) &&
Expand All @@ -1457,7 +1496,7 @@ export const standardLinterfunctions: FunctionItem[] = [
return true;
}

if (referenceNames.length === 0) {
if (referenceNames.size === 0) {
const api = root(element);
const isReferenceElement = (el: Element & { content?: { key?: string } }) => {
if (!isObjectElement(el) || !el.hasKey('$ref')) {
Expand All @@ -1473,14 +1512,16 @@ export const standardLinterfunctions: FunctionItem[] = [
return isReferenceElement(el);
}, api);

referenceNames.push(
...referenceElements.map((refElement: ObjectElement) =>
toValue(refElement.get('$ref')).split('/').at(-1),
),
);
referenceElements.forEach((refElement) => {
referenceNames.add(
toValue((refElement as ObjectElement).get('$ref'))
.split('/')
.at(-1),
);
});
}

return referenceNames.includes(toValue(element.parent.key));
return referenceNames.has(toValue(element.parent.key));
},
},
{
Expand Down
Loading