From d4a33ddb8664e68bf8fb5fe2300f833d829ceddf Mon Sep 17 00:00:00 2001 From: Maruf Rasully Date: Thu, 25 Apr 2024 06:54:26 +0200 Subject: [PATCH 1/7] fix: support cc for absolute path in meta path --- .../completion/providers/context-path.ts | 236 +++---- .../completion/providers/meta-path.ts | 382 +++++------ .../services/completion/providers/utils.ts | 139 ++++ packages/fe/src/types/completion.ts | 24 + packages/fe/src/types/index.ts | 12 + packages/fe/src/utils/metadata.ts | 21 +- packages/fe/src/utils/misc.ts | 81 ++- packages/fe/src/utils/path.ts | 22 + .../completion/providers/context-path.test.ts | 222 +++---- .../completion/providers/meta-path.test.ts | 607 +++++++++++++----- .../validators/wrong-filterbar-id.test.ts | 2 +- 11 files changed, 1127 insertions(+), 621 deletions(-) create mode 100644 packages/fe/src/services/completion/providers/utils.ts diff --git a/packages/fe/src/services/completion/providers/context-path.ts b/packages/fe/src/services/completion/providers/context-path.ts index 52da27e17..b71109617 100644 --- a/packages/fe/src/services/completion/providers/context-path.ts +++ b/packages/fe/src/services/completion/providers/context-path.ts @@ -1,37 +1,17 @@ import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; -import { - getPathConstraintsForControl, - getNextPossibleContextPathTargets, - getRootElements, - resolvePathTarget, -} from "../../../utils"; +import { getPathConstraintsForControl } from "../../../utils"; import { AnnotationTargetInXMLAttributeValueCompletion, SAP_FE_MACROS, } from "../../../types"; import { UI5AttributeValueCompletionOptions } from "./index"; +import { CompletionSuggestion } from "../../../types/completion"; import { - EntityContainer, - EntitySet, - EntityType, - NavigationProperty, - Singleton, -} from "@sap-ux/vocabularies-types"; -import { getAffectedRange } from "../utils"; -import { AnnotationTargetInXMLAttributeValueTypeName } from "../../../types/completion"; - -type ApplicableMetadataElement = - | EntityContainer - | EntitySet - | EntityType - | Singleton - | NavigationProperty; - -interface CompletionSuggestion { - element: ApplicableMetadataElement; - isLastSegment: boolean; -} + getNavigationSuggestion, + getRootElementSuggestions, + suggestionToTargetCompletion, +} from "./utils"; /** * Suggests values for macros contextPath @@ -46,150 +26,84 @@ export function contextPathSuggestions({ attribute, context.ui5Model ); + if (!ui5Property) { + return []; + } + const metaPathStartsWithAbsolutePath = element.attributes.find( + (i) => i.key === "metaPath" && i.value?.startsWith("/") + ); + // no CC for contextPath if metaPath starts with absolute path + if (metaPathStartsWithAbsolutePath) { + return []; + } if ( - ui5Property?.library === SAP_FE_MACROS && - ui5Property.parent?.name === "Chart" && - ui5Property.name === "contextPath" + !["contextPath"].includes(ui5Property.name) || + ui5Property?.library !== SAP_FE_MACROS || + ui5Property.parent?.name !== "Chart" ) { - const mainServicePath = context.manifestDetails.mainServicePath; - const service = mainServicePath - ? context.services[mainServicePath] - : undefined; - if (!service) { - return []; - } - const metadata = service.convertedMetadata; - const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( - element.name, - ui5Property - ); - const isPropertyPath = expectedTypes.includes("Property"); - const suggestions: CompletionSuggestion[] = []; - const segments = (attribute.value || "").split("/"); - const precedingSegments = (prefix || "").split("/"); - const completionSegmentIndex = precedingSegments.length - 1; - precedingSegments.pop(); - const completionSegmentOffset = - precedingSegments.join("/").length + (precedingSegments.length ? 1 : 0); - const isAbsolutePath = segments.length > 1 && !segments[0]; - if (!isAbsolutePath && completionSegmentIndex > 0) { - // relative paths are not supported - return []; - } - if (expectedAnnotations.length + expectedTypes.length === 0) { - return []; - } + return []; + } - const isNextSegmentPossible = ( - currentTarget: EntitySet | EntityType | Singleton | EntityContainer, - milestones: string[] = [] - ): boolean => { - return ( - getNextPossibleContextPathTargets( - service.convertedMetadata, - currentTarget, - { - allowedTerms: expectedAnnotations, - allowedTargets: expectedTypes, - isPropertyPath, - }, - [...milestones, currentTarget.fullyQualifiedName] - ).length > 0 - ); - }; + const mainServicePath = context.manifestDetails.mainServicePath; + const service = mainServicePath + ? context.services[mainServicePath] + : undefined; + if (!service) { + return []; + } + const metadata = service.convertedMetadata; + const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( + element.name, + ui5Property + ); + if (expectedAnnotations.length + expectedTypes.length === 0) { + return []; + } + const isPropertyPath = expectedTypes.includes("Property"); + const suggestions: CompletionSuggestion[] = []; + const segments = (attribute.value || "").split("/"); + const precedingSegments = (prefix || "").split("/"); + const completionSegmentIndex = precedingSegments.length - 1; + precedingSegments.pop(); + const completionSegmentOffset = + precedingSegments.join("/").length + (precedingSegments.length ? 1 : 0); + const isAbsolutePath = segments.length > 1 && !segments[0]; + if (!isAbsolutePath && completionSegmentIndex > 0) { + // relative paths are not supported + return []; + } - if (completionSegmentIndex < 2) { - // completion for root element - const roots = getRootElements( + if (completionSegmentIndex < 2) { + // completion for root element + suggestions.push( + ...getRootElementSuggestions( metadata, expectedAnnotations, expectedTypes, isPropertyPath - ); - suggestions.push( - ...roots.map((root) => ({ - element: root, - isLastSegment: !isNextSegmentPossible(root), - })) - ); - } else { - // completion for navigation property segment - const precedingPath = segments.slice(0, completionSegmentIndex).join("/"); - const { target, isCollection, milestones } = resolvePathTarget( - service.convertedMetadata, - precedingPath - ); - if (!target) { - // target not resolved or path leads to collection - no further segments possible - return []; - } else if (target._type === "Property") { - // no further segments possible after entity property, container is not supported - return []; - } else { - const possibleTargets = getNextPossibleContextPathTargets( - service.convertedMetadata, - target, - { - allowedTerms: expectedAnnotations, - allowedTargets: expectedTypes, - isPropertyPath, - isCollection: isCollection ? false : undefined, - }, - milestones - ); - suggestions.push( - ...possibleTargets.map((t) => { - const entityType = - t._type === "NavigationProperty" ? t.targetType : t.entityType; - return { - element: t, - isLastSegment: !isNextSegmentPossible(entityType, milestones), - }; - }) - ); - } - } - - const sortMap: Record = { - EntityContainer: "Z", - EntityType: "A", - EntitySet: "B", - Singleton: "C", - NavigationProperty: "N", - }; - - const getSuggestionText = (suggestion: CompletionSuggestion): string => { - const isFullyQualifiedName = [ - "EntityContainer", - "EntitySet", - "Singleton", - ].includes(suggestion.element._type); - return `${completionSegmentIndex === 0 ? "/" : ""}${ - isFullyQualifiedName && completionSegmentIndex < 2 - ? suggestion.element.fullyQualifiedName - : suggestion.element.name - }`; + ) + ); + } else { + // completion for navigation property segment + const precedingPath = segments.slice(0, completionSegmentIndex).join("/"); + const options = { + allowedTerms: expectedAnnotations, + allowedTargets: expectedTypes, + isPropertyPath, }; - - return suggestions.map((suggestion) => { - const text = getSuggestionText(suggestion); - return { - type: AnnotationTargetInXMLAttributeValueTypeName, - node: { - kind: suggestion.element._type, - name: text, - text, - affectedRange: getAffectedRange( - attribute.syntax.value, - completionSegmentOffset - ), - commitCharacters: suggestion.isLastSegment ? [] : ["/"], - commitCharactersRequired: true, - sortText: sortMap[suggestion.element._type] + text, - }, - }; - }); + suggestions.push( + ...getNavigationSuggestion( + service.convertedMetadata, + precedingPath, + options + ) + ); } - return []; + return suggestionToTargetCompletion( + attribute, + suggestions, + completionSegmentIndex, + completionSegmentOffset + ); } diff --git a/packages/fe/src/services/completion/providers/meta-path.ts b/packages/fe/src/services/completion/providers/meta-path.ts index 5dd719946..ebc27f36d 100644 --- a/packages/fe/src/services/completion/providers/meta-path.ts +++ b/packages/fe/src/services/completion/providers/meta-path.ts @@ -6,12 +6,12 @@ import { getNextPossiblePathTargets, resolvePathTarget, normalizePath, - getContextPath, + ResolvedPathTargetType, + resolveContextPath, } from "../../../utils"; import type { UI5AttributeValueCompletionOptions } from "./index"; import type { - EntityContainer, EntitySet, Singleton, EntityType, @@ -20,15 +20,22 @@ import type { } from "@sap-ux/vocabularies-types"; import { AnnotationPathInXMLAttributeValueCompletion, - PropertyPathInXMLAttributeValueCompletion, + ContextPathOrigin, SAP_FE_MACROS, } from "../../../types"; import { Range } from "vscode-languageserver-types"; import { getAffectedRange } from "../utils"; import { AnnotationPathInXMLAttributeValueTypeName, + AnnotationTargetInXMLAttributeValueTypeName, + MetaPathSuggestion, PropertyPathInXMLAttributeValueTypeName, } from "../../../types/completion"; +import { + getRootElementSuggestions, + sortMap, + suggestionToTargetCompletion, +} from "./utils"; export interface CompletionItem { name: string; @@ -45,14 +52,8 @@ export function metaPathSuggestions({ attribute, context, prefix, -}: UI5AttributeValueCompletionOptions): ( - | AnnotationPathInXMLAttributeValueCompletion - | PropertyPathInXMLAttributeValueCompletion -)[] { - const result: ( - | AnnotationPathInXMLAttributeValueCompletion - | PropertyPathInXMLAttributeValueCompletion - )[] = []; +}: UI5AttributeValueCompletionOptions): MetaPathSuggestion[] { + const result: MetaPathSuggestion[] = []; const ui5Property = getUI5PropertyByXMLAttributeKey( attribute, context.ui5Model @@ -64,7 +65,6 @@ export function metaPathSuggestions({ return []; } const contextPathAttr = getElementAttributeValue(element, "contextPath"); - let contextPath = getContextPath(contextPathAttr, context); const mainServicePath = context.manifestDetails.mainServicePath; const service = mainServicePath @@ -74,206 +74,220 @@ export function metaPathSuggestions({ return []; } - const entitySet = - context.manifestDetails.customViews[context.customViewId || ""] - ?.entitySet ?? ""; - const metadata = service.convertedMetadata; let baseType: EntityType | undefined; - let base: - | EntityContainer - | EntitySet - | EntityType - | Singleton - | Property - | undefined; - let isNavSegmentsAllowed = true; + let base: ResolvedPathTargetType | undefined; + // navigation is only allowed when contextPath in xml attribute is undefined + const isNavSegmentsAllowed = typeof contextPathAttr === "undefined"; + const segments = (attribute.value || "").split("/"); + const precedingSegments = (prefix || "").split("/"); + const completionSegmentIndex = precedingSegments.length - 1; + precedingSegments.pop(); + const completionSegmentOffset = + precedingSegments.join("/").length + (precedingSegments.length ? 1 : 0); + const isAbsolutePath = segments.length > 1 && !segments[0]; + const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( + element.name, + ui5Property + ); + if (expectedAnnotations.length + expectedTypes.length === 0) { + return []; + } + const precedingPath = segments.slice(0, completionSegmentIndex).join("/"); - if (typeof contextPath === "string") { - if (!contextPath.startsWith("/")) { - return []; - } - ({ target: base, targetStructuredType: baseType } = resolvePathTarget( + const isPropertiesAllowed = expectedTypes.includes("Property"); + + // no CC if contextPath is defined in xml and CC is request after absolute path e.g `/` + if (contextPathAttr && isAbsolutePath && completionSegmentIndex === 1) { + return []; + } + const resolvedContext = resolveContextPath( + context, + element, + isAbsolutePath, + precedingPath + ); + + if ( + (!contextPathAttr && isAbsolutePath && completionSegmentIndex === 1) || + !resolvedContext + ) { + // for first absolute segment e.g / + const suggestions = getRootElementSuggestions( metadata, - normalizePath(contextPath) - )); - isNavSegmentsAllowed = typeof contextPathAttr === "undefined"; - } else { - contextPath = `/${entitySet}`; + expectedAnnotations, + expectedTypes, + isPropertiesAllowed + ); + const targetSuggestion = suggestionToTargetCompletion( + attribute, + suggestions, + completionSegmentIndex, + completionSegmentOffset + ); + return targetSuggestion; + } + const { contextPath, origin } = resolvedContext; + if (origin === ContextPathOrigin.entitySetInManifest) { base = service.convertedMetadata.entitySets.find( - (e) => e.name === entitySet + (e) => `/${e.name}` === contextPath ); baseType = base?.entityType; } + let isCollection; - if (baseType) { - const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( - element.name, - ui5Property - ); - const isPropertiesAllowed = expectedTypes.includes("Property"); - const segments = (attribute.value || "").split("/"); - const precedingSegments = (prefix || "").split("/"); - const completionSegmentIndex = precedingSegments.length - 1; - precedingSegments.pop(); - const completionSegmentOffset = - precedingSegments.join("/").length + (precedingSegments.length ? 1 : 0); - const isAbsolutePath = segments.length > 1 && !segments[0]; + // for (navigation) property segment or annotation term + if (!isAbsolutePath && completionSegmentIndex > 0) { + const contextToConsider = [contextPath, precedingPath].join("/"); - if (isAbsolutePath && completionSegmentIndex > 0) { - // absolute paths are not supported in metaPath + if (!isNavSegmentsAllowed) { return []; } - if (!isNavSegmentsAllowed && completionSegmentIndex > 0) { - return []; - } - if (expectedAnnotations.length + expectedTypes.length === 0) { + + ({ + target: base, + targetStructuredType: baseType, + isCollection: isCollection, + } = resolvePathTarget(metadata, contextToConsider, baseType)); + if (!base) { + // target not resolved e.g. for wrong nav segment - no further segments possible return []; } + } - // completion for (navigation) property segment or annotation term - const precedingPath = segments.slice(0, completionSegmentIndex).join("/"); - const { target, isCollection, targetStructuredType } = - completionSegmentIndex === 0 - ? { - target: base, - targetStructuredType: baseType, - isCollection: undefined, - } - : resolvePathTarget(service.convertedMetadata, precedingPath, baseType); - if (!target) { - // target not resolved - no further segments possible - return []; - } else if (isPropertiesAllowed && target._type === "Property") { - // no further segments possible after entity property - return []; - } else { - // Calculate completion range considering that value region includes quotes - const affectedRange: Range | undefined = getAffectedRange( - attribute.syntax.value, - completionSegmentOffset - ); + if (!base) { + ({ + target: base, + targetStructuredType: baseType, + isCollection: isCollection, + } = resolvePathTarget(metadata, normalizePath(contextPath))); + } - let possibleTargets: ( - | EntitySet - | Singleton - | NavigationProperty - | Property - )[] = []; - if (target._type === "Property" || target._type === "EntityContainer") { - return []; - } - // collect existing terms - const annotationList = collectAnnotationsForElement( - expectedAnnotations, - target - ); - if (["EntitySet", "Singleton"].includes(target._type)) { - // for first path segment completion, where current base can be entity set or singleton, - // we collect also terms applied on their structural entity type + if (!base) { + // target not resolved - no further segments possible + return []; + } - // targetStructuredType is never undefined in this context - annotationList.push( - ...collectAnnotationsForElement( - expectedAnnotations, - targetStructuredType - ) - ); - } - result.push( - ...annotationList.map((annotation) => { - const fullPath = `@${ - annotation.qualifier - ? `${annotation.term}#${annotation.qualifier}` - : annotation.term - }`; - return { - type: AnnotationPathInXMLAttributeValueTypeName, - node: { - kind: "Term", - name: fullPath, - text: fullPath, - affectedRange, - }, - } as AnnotationPathInXMLAttributeValueCompletion; - }) - ); + if (base._type === "Property") { + // no further segments possible after entity property + return []; + } + + // Calculate completion range considering that value region includes quotes + const affectedRange: Range | undefined = getAffectedRange( + attribute.syntax.value, + completionSegmentOffset + ); - // collect possible properties or navigation segments - possibleTargets = getNextPossiblePathTargets( - service.convertedMetadata, - target, - false, - { - allowedTerms: expectedAnnotations, - allowedTargets: expectedTypes, - isPropertyPath: isPropertiesAllowed, - isCollection: isCollection ? false : undefined, + let possibleTargets: ( + | EntitySet + | Singleton + | NavigationProperty + | Property + )[] = []; + + // collect existing terms + const annotationList = collectAnnotationsForElement( + expectedAnnotations, + base + ); + if (["EntitySet", "Singleton"].includes(base._type)) { + // for first path segment completion, where current base can be entity set or singleton, + // we collect also terms applied on their structural entity type + + // targetStructuredType is never undefined in this context + annotationList.push( + ...collectAnnotationsForElement(expectedAnnotations, baseType) + ); + } + result.push( + ...annotationList.map((annotation) => { + const fullPath = `@${ + annotation.qualifier + ? `${annotation.term}#${annotation.qualifier}` + : annotation.term + }`; + return { + type: AnnotationPathInXMLAttributeValueTypeName, + node: { + kind: "Term", + name: fullPath, + text: fullPath, + affectedRange, }, - [target.fullyQualifiedName] - ); + } as AnnotationPathInXMLAttributeValueCompletion; + }) + ); - result.push( - ...convertTargetsToCompletionItems( - possibleTargets, - isNavSegmentsAllowed, - isPropertiesAllowed, - affectedRange - ) - ); - } + // collect possible properties or navigation segments + possibleTargets = getNextPossiblePathTargets( + service.convertedMetadata, + base, + false, + { + allowedTerms: expectedAnnotations, + allowedTargets: expectedTypes, + isPropertyPath: isPropertiesAllowed, + isCollection: isCollection ? false : undefined, + }, + [base.fullyQualifiedName] + ); + if (!isNavSegmentsAllowed) { + // filter out Property + possibleTargets = possibleTargets.filter((i) => i._type === "Property"); + } + result.push( + ...convertTargetsToCompletionItems( + possibleTargets, + isPropertiesAllowed, + base._type === "EntityContainer", + affectedRange + ) + ); + + // only if contextPath is not defined in xml and CC is request for initial segment + if (!contextPathAttr && completionSegmentIndex === 0) { + const suggestions = getRootElementSuggestions( + metadata, + expectedAnnotations, + expectedTypes, + isPropertiesAllowed + ); + const targetSuggestion = suggestionToTargetCompletion( + attribute, + suggestions, + completionSegmentIndex, + completionSegmentOffset + ); + result.push(...targetSuggestion); } return result; } function convertTargetsToCompletionItems( targets: (EntitySet | Singleton | Property | NavigationProperty)[], - isNavSegmentsAllowed: boolean, isPropertyPath: boolean, + isEntityContainer: boolean, affectedRange: Range | undefined -): ( - | AnnotationPathInXMLAttributeValueCompletion - | PropertyPathInXMLAttributeValueCompletion -)[] { - const applicableTargets: (NavigationProperty | Property)[] = targets.reduce( - (acc, t) => { - if ( - t._type === "Property" || - (isNavSegmentsAllowed && t._type === "NavigationProperty") - ) { - acc.push(t); - } - return acc; - }, - [] as (NavigationProperty | Property)[] - ); - - return applicableTargets.map((t) => { - if (t._type === "Property") { - return { - type: PropertyPathInXMLAttributeValueTypeName, - node: { - kind: t._type, - name: t.name, - text: t.name, - affectedRange, - sortText: "A" + t.name, - }, - } as PropertyPathInXMLAttributeValueCompletion; - } else { - return { - type: isPropertyPath - ? PropertyPathInXMLAttributeValueTypeName - : AnnotationPathInXMLAttributeValueTypeName, - node: { - kind: t._type, - name: t.name, - text: t.name, - affectedRange, - commitCharacters: ["/"], - sortText: "B" + t.name, - }, - } as AnnotationPathInXMLAttributeValueCompletion; +): MetaPathSuggestion[] { + return targets.map((t) => { + let type = PropertyPathInXMLAttributeValueTypeName; + if (!isPropertyPath) { + type = AnnotationPathInXMLAttributeValueTypeName; + } + if (isEntityContainer) { + type = AnnotationTargetInXMLAttributeValueTypeName; } + return { + type, + node: { + kind: t._type, + name: t.name, + text: t.name, + affectedRange, + commitCharacters: ["/"], + sortText: sortMap[t._type] + t.name, + }, + } as MetaPathSuggestion; }); } diff --git a/packages/fe/src/services/completion/providers/utils.ts b/packages/fe/src/services/completion/providers/utils.ts new file mode 100644 index 000000000..0dcaa2423 --- /dev/null +++ b/packages/fe/src/services/completion/providers/utils.ts @@ -0,0 +1,139 @@ +import { XMLAttribute } from "@xml-tools/ast"; +import { + AnnotationTargetInXMLAttributeValueCompletion, + AnnotationTargetInXMLAttributeValueTypeName, + CompletionSuggestion, +} from "../../../types/completion"; +import { getAffectedRange } from "../utils"; +import { + getNextPossibleContextPathTargets, + getRootElements, + resolvePathTarget, + AllowedTargetType, + isNextSegmentPossible, +} from "../../../utils"; +import { ConvertedMetadata } from "@sap-ux/vocabularies-types"; +import { AnnotationTerm } from "src/types"; + +const getSuggestionText = ( + suggestion: CompletionSuggestion, + completionSegmentIndex: number +): string => { + const isFullyQualifiedName = [ + "EntityContainer", + "EntitySet", + "Singleton", + ].includes(suggestion.element._type); + return `${completionSegmentIndex === 0 ? "/" : ""}${ + isFullyQualifiedName && completionSegmentIndex < 2 + ? suggestion.element.fullyQualifiedName + : suggestion.element.name + }`; +}; + +export const sortMap: Record = { + Property: "A", + NavigationProperty: "B", + Term: "C", + EntityType: "D", + EntitySet: "E", + Singleton: "F", + EntityContainer: "Z", +}; + +export function suggestionToTargetCompletion( + attribute: XMLAttribute, + suggestions: CompletionSuggestion[], + completionIndex: number, + completionOffset: number +): AnnotationTargetInXMLAttributeValueCompletion[] { + return suggestions.map((suggestion) => { + const text = getSuggestionText(suggestion, completionIndex); + return { + type: AnnotationTargetInXMLAttributeValueTypeName, + node: { + kind: suggestion.element._type, + name: text, + text, + affectedRange: getAffectedRange( + attribute.syntax.value, + completionOffset + ), + commitCharacters: suggestion.isLastSegment ? [] : ["/"], + commitCharactersRequired: true, + sortText: sortMap[suggestion.element._type] + text, + }, + }; + }); +} + +export function getRootElementSuggestions( + metadata: ConvertedMetadata, + expectedAnnotations: AnnotationTerm[], + expectedTypes: AllowedTargetType[], + isPropertyPath: boolean +): CompletionSuggestion[] { + const roots = getRootElements( + metadata, + expectedAnnotations, + expectedTypes, + isPropertyPath + ); + return roots.map((root) => ({ + element: root, + isLastSegment: !isNextSegmentPossible(metadata, root, {}), + })); +} + +/** + * Any suggestion after root segment + */ +export function getNavigationSuggestion( + metadata: ConvertedMetadata, + precedingPath: string, + options: { + isPropertyPath?: boolean; + allowedTerms?: AnnotationTerm[]; + allowedTargets?: AllowedTargetType[]; + isCollection?: boolean; + } +): CompletionSuggestion[] { + const { target, isCollection, milestones } = resolvePathTarget( + metadata, + precedingPath + ); + if (!target) { + // target not resolved or path leads to collection - no further segments possible + return []; + } + if (target._type === "Property") { + // no further segments possible after entity property, container is not supported + return []; + } + + options = { ...options, isCollection: isCollection ? false : undefined }; + + const possibleTargets = getNextPossibleContextPathTargets( + metadata, + target, + options, + milestones + ); + const suggestions: CompletionSuggestion[] = []; + suggestions.push( + ...possibleTargets.map((t) => { + const entityType = + t._type === "NavigationProperty" ? t.targetType : t.entityType; + return { + element: t, + isLastSegment: !isNextSegmentPossible( + metadata, + entityType, + options, + milestones + ), + }; + }) + ); + return suggestions; +} diff --git a/packages/fe/src/types/completion.ts b/packages/fe/src/types/completion.ts index 950d158a2..f762d8002 100644 --- a/packages/fe/src/types/completion.ts +++ b/packages/fe/src/types/completion.ts @@ -1,4 +1,11 @@ import { MarkupContent, Range } from "vscode-languageserver-types"; +import { + EntityContainer, + EntitySet, + EntityType, + NavigationProperty, + Singleton, +} from "@sap-ux/vocabularies-types"; export type CompletionItemKind = | "Term" @@ -123,3 +130,20 @@ export interface FilterBarIdInXMLAttributeValueCompletion extends BaseXMLViewAnnotationCompletion { type: typeof FilterBarIdInXMLAttributeValueTypeName; } + +export type ApplicableMetadataElement = + | EntityContainer + | EntitySet + | EntityType + | Singleton + | NavigationProperty; + +export interface CompletionSuggestion { + element: ApplicableMetadataElement; + isLastSegment: boolean; +} + +export type MetaPathSuggestion = + | AnnotationPathInXMLAttributeValueCompletion + | PropertyPathInXMLAttributeValueCompletion + | AnnotationTargetInXMLAttributeValueCompletion; diff --git a/packages/fe/src/types/index.ts b/packages/fe/src/types/index.ts index 446a95acb..580354908 100644 --- a/packages/fe/src/types/index.ts +++ b/packages/fe/src/types/index.ts @@ -15,3 +15,15 @@ export type AnnotationBase = { term: string; qualifier: string; }; + +export enum ContextPathOrigin { + xmlAttributeInContextPath = "xml-attribute-in-context-path", + xmlAttributeInMetaPath = "xml-attribute-in-meta-path", + contextPathInManifest = "context-path-in-manifest", + entitySetInManifest = "entitySet-in-manifest", +} + +export interface ResolveContextPath { + contextPath: string; + origin: ContextPathOrigin; +} diff --git a/packages/fe/src/utils/metadata.ts b/packages/fe/src/utils/metadata.ts index c51844e6e..ada008b3a 100644 --- a/packages/fe/src/utils/metadata.ts +++ b/packages/fe/src/utils/metadata.ts @@ -101,7 +101,7 @@ export function getRootElements( export function collectAnnotationsForElement( allowedTerms: AnnotationTerm[], - element: EntityType | EntitySet | Singleton | undefined, + element: EntityContainer | EntityType | EntitySet | Singleton | undefined, property?: string, navigationProperty?: string ): AnnotationBase[] { @@ -110,20 +110,25 @@ export function collectAnnotationsForElement( if (!element) { return []; } - const type: EntityType = getEntityTypeForElement(element); let target: + | EntityContainer | EntityType | EntitySet | Singleton | Property | NavigationProperty | undefined; - if (property) { - target = type.entityProperties.find((p) => p.name === property); - } else if (navigationProperty) { - target = type.navigationProperties.find( - (p) => p.name === navigationProperty - ); + if (element._type !== "EntityContainer") { + const type: EntityType = getEntityTypeForElement(element); + if (property) { + target = type.entityProperties.find((p) => p.name === property); + } else if (navigationProperty) { + target = type.navigationProperties.find( + (p) => p.name === navigationProperty + ); + } else { + target = element; + } } else { target = element; } diff --git a/packages/fe/src/utils/misc.ts b/packages/fe/src/utils/misc.ts index 63d0c801b..5bb490949 100644 --- a/packages/fe/src/utils/misc.ts +++ b/packages/fe/src/utils/misc.ts @@ -2,7 +2,11 @@ import { UI5Prop } from "@ui5-language-assistant/semantic-model-types"; import { Context } from "@ui5-language-assistant/context"; import { XMLElement } from "@xml-tools/ast"; import i18next, { TFunction } from "i18next"; -import type { AnnotationTerm } from "../types"; +import { + ContextPathOrigin, + type AnnotationTerm, + type ResolveContextPath, +} from "../types"; import { AllowedTargetType } from "./metadata"; import { BuildingBlockPathConstraints, @@ -72,7 +76,9 @@ export const t: TFunction = (key: string, ...args) => { }; /** - * Returns context path for completion and diagnostics services + * Returns context path for completion and diagnostics services. Context path defined in xml attribute wins over + * context path defined in manifest.json file. + * * @param attributeValue - current element contextPath attribute value * @param context - global context object * @returns - context path @@ -82,9 +88,76 @@ export function getContextPath( context: Context ): AttributeValueType { const contextPathInManifest: string | undefined = - context.manifestDetails?.customViews?.[context.customViewId || ""] - ?.contextPath; + getManifestContextPath(context); return typeof attributeValue !== "undefined" ? attributeValue : contextPathInManifest || undefined; } + +/** + * Get context path defined in manifest.json file. + * + * @param context + * @returns + */ +export function getManifestContextPath(context: Context): string | undefined { + return context.manifestDetails?.customViews?.[context.customViewId || ""] + ?.contextPath; +} + +/** + * Resolve context path. Context path can be defined + * - as xml attribute + * - as contextPath in manifest.json file + * - in meta path if path is starting as absolute + * - as entity set of view defined in manifest.json file + * + * @param context context + * @param element xml element + * @param isAbsolutePath path started as absolute + * @param precedingPath proceeding path segment + * @returns context path with its origin + */ +export function resolveContextPath( + context: Context, + element: XMLElement, + isAbsolutePath: boolean, + precedingPath: string +): ResolveContextPath | undefined { + const contextPathAttr = getElementAttributeValue(element, "contextPath"); + if (contextPathAttr) { + return { + contextPath: contextPathAttr, + origin: ContextPathOrigin.xmlAttributeInContextPath, + }; + } + + // if not absolute path and context path is not defined in xml, take it from manifest.json + const contextPath = getManifestContextPath(context); + if (!isAbsolutePath && contextPath) { + return { + contextPath, + origin: ContextPathOrigin.contextPathInManifest, + }; + } + + // context path is preceding path if it started as absolute path + if (isAbsolutePath && precedingPath.length > 0) { + return { + contextPath: precedingPath, + origin: ContextPathOrigin.xmlAttributeInMetaPath, + }; + } + + const entitySet = + context.manifestDetails.customViews[context.customViewId || ""] + ?.entitySet ?? ""; + + if (entitySet) { + // context path is entity set + return { + contextPath: `/${entitySet}`, + origin: ContextPathOrigin.entitySetInManifest, + }; + } +} diff --git a/packages/fe/src/utils/path.ts b/packages/fe/src/utils/path.ts index 38afb4807..70ef8bfbd 100644 --- a/packages/fe/src/utils/path.ts +++ b/packages/fe/src/utils/path.ts @@ -13,6 +13,7 @@ import { getEntityTypeForElement, } from "./metadata"; import { AnnotationTerm } from "./spec"; +import { ApplicableMetadataElement } from "../types/completion"; export type ResolvedPathTargetType = | EntityContainer @@ -272,3 +273,24 @@ export function getNextPossiblePathTargets( return result; } + +export const isNextSegmentPossible = ( + convertedMetadata: ConvertedMetadata, + currentTarget: Exclude, + options: { + isPropertyPath?: boolean; + allowedTerms?: AnnotationTerm[]; + allowedTargets?: AllowedTargetType[]; + isCollection?: boolean; + }, + milestones: string[] = [] +): boolean => { + return ( + getNextPossibleContextPathTargets( + convertedMetadata, + currentTarget, + options, + [...milestones, currentTarget.fullyQualifiedName] + ).length > 0 + ); +}; diff --git a/packages/fe/test/unit/services/completion/providers/context-path.test.ts b/packages/fe/test/unit/services/completion/providers/context-path.test.ts index 599495ab4..690941c6c 100644 --- a/packages/fe/test/unit/services/completion/providers/context-path.test.ts +++ b/packages/fe/test/unit/services/completion/providers/context-path.test.ts @@ -113,54 +113,54 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:/; sort:Z", - "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/HighestTotal; text: /TravelService.EntityContainer/HighestTotal; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/Currencies; text: /TravelService.EntityContainer/Currencies; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/TravelStatus; text: /TravelService.EntityContainer/TravelStatus; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/TravelAgency; text: /TravelService.EntityContainer/TravelAgency; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Passenger; text: /TravelService.EntityContainer/Passenger; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Countries; text: /TravelService.EntityContainer/Countries; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookingStatus; text: /TravelService.EntityContainer/BookingStatus; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Airline; text: /TravelService.EntityContainer/Airline; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Flight; text: /TravelService.EntityContainer/Flight; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Supplement; text: /TravelService.EntityContainer/Supplement; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/FlightConnection; text: /TravelService.EntityContainer/FlightConnection; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/SupplementType; text: /TravelService.EntityContainer/SupplementType; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Airport; text: /TravelService.EntityContainer/Airport; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Currencies_texts; text: /TravelService.EntityContainer/Currencies_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/TravelStatus_texts; text: /TravelService.EntityContainer/TravelStatus_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/Countries_texts; text: /TravelService.EntityContainer/Countries_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/BookingStatus_texts; text: /TravelService.EntityContainer/BookingStatus_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/Supplement_texts; text: /TravelService.EntityContainer/Supplement_texts; kind:2; commit:; sort:B", - "label: /TravelService.EntityContainer/SupplementType_texts; text: /TravelService.EntityContainer/SupplementType_texts; kind:2; commit:; sort:B", - "label: /Travel; text: /Travel; kind:7; commit:/; sort:A", - "label: /HighestTotal; text: /HighestTotal; kind:7; commit:; sort:A", - "label: /Currencies; text: /Currencies; kind:7; commit:/; sort:A", - "label: /TravelStatus; text: /TravelStatus; kind:7; commit:/; sort:A", - "label: /TravelAgency; text: /TravelAgency; kind:7; commit:/; sort:A", - "label: /Passenger; text: /Passenger; kind:7; commit:/; sort:A", - "label: /Booking; text: /Booking; kind:7; commit:/; sort:A", - "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:A", - "label: /Countries; text: /Countries; kind:7; commit:/; sort:A", - "label: /BookingStatus; text: /BookingStatus; kind:7; commit:/; sort:A", - "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:A", - "label: /Airline; text: /Airline; kind:7; commit:/; sort:A", - "label: /Flight; text: /Flight; kind:7; commit:/; sort:A", - "label: /Supplement; text: /Supplement; kind:7; commit:/; sort:A", - "label: /FlightConnection; text: /FlightConnection; kind:7; commit:/; sort:A", - "label: /SupplementType; text: /SupplementType; kind:7; commit:/; sort:A", - "label: /Airport; text: /Airport; kind:7; commit:/; sort:A", - "label: /DraftAdministrativeData; text: /DraftAdministrativeData; kind:7; commit:; sort:A", - "label: /Currencies_texts; text: /Currencies_texts; kind:7; commit:; sort:A", - "label: /TravelStatus_texts; text: /TravelStatus_texts; kind:7; commit:; sort:A", - "label: /Countries_texts; text: /Countries_texts; kind:7; commit:; sort:A", - "label: /BookingStatus_texts; text: /BookingStatus_texts; kind:7; commit:; sort:A", - "label: /Supplement_texts; text: /Supplement_texts; kind:7; commit:; sort:A", - "label: /SupplementType_texts; text: /SupplementType_texts; kind:7; commit:; sort:A", + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/HighestTotal; text: /TravelService.EntityContainer/HighestTotal; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/Currencies; text: /TravelService.EntityContainer/Currencies; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/TravelStatus; text: /TravelService.EntityContainer/TravelStatus; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/TravelAgency; text: /TravelService.EntityContainer/TravelAgency; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Passenger; text: /TravelService.EntityContainer/Passenger; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Countries; text: /TravelService.EntityContainer/Countries; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingStatus; text: /TravelService.EntityContainer/BookingStatus; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Airline; text: /TravelService.EntityContainer/Airline; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Flight; text: /TravelService.EntityContainer/Flight; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Supplement; text: /TravelService.EntityContainer/Supplement; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/FlightConnection; text: /TravelService.EntityContainer/FlightConnection; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/SupplementType; text: /TravelService.EntityContainer/SupplementType; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Airport; text: /TravelService.EntityContainer/Airport; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Currencies_texts; text: /TravelService.EntityContainer/Currencies_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/TravelStatus_texts; text: /TravelService.EntityContainer/TravelStatus_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/Countries_texts; text: /TravelService.EntityContainer/Countries_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/BookingStatus_texts; text: /TravelService.EntityContainer/BookingStatus_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/Supplement_texts; text: /TravelService.EntityContainer/Supplement_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/SupplementType_texts; text: /TravelService.EntityContainer/SupplementType_texts; kind:2; commit:; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /HighestTotal; text: /HighestTotal; kind:7; commit:; sort:D", + "label: /Currencies; text: /Currencies; kind:7; commit:/; sort:D", + "label: /TravelStatus; text: /TravelStatus; kind:7; commit:/; sort:D", + "label: /TravelAgency; text: /TravelAgency; kind:7; commit:/; sort:D", + "label: /Passenger; text: /Passenger; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /Countries; text: /Countries; kind:7; commit:/; sort:D", + "label: /BookingStatus; text: /BookingStatus; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + "label: /Airline; text: /Airline; kind:7; commit:/; sort:D", + "label: /Flight; text: /Flight; kind:7; commit:/; sort:D", + "label: /Supplement; text: /Supplement; kind:7; commit:/; sort:D", + "label: /FlightConnection; text: /FlightConnection; kind:7; commit:/; sort:D", + "label: /SupplementType; text: /SupplementType; kind:7; commit:/; sort:D", + "label: /Airport; text: /Airport; kind:7; commit:/; sort:D", + "label: /DraftAdministrativeData; text: /DraftAdministrativeData; kind:7; commit:; sort:D", + "label: /Currencies_texts; text: /Currencies_texts; kind:7; commit:; sort:D", + "label: /TravelStatus_texts; text: /TravelStatus_texts; kind:7; commit:; sort:D", + "label: /Countries_texts; text: /Countries_texts; kind:7; commit:; sort:D", + "label: /BookingStatus_texts; text: /BookingStatus_texts; kind:7; commit:; sort:D", + "label: /Supplement_texts; text: /Supplement_texts; kind:7; commit:; sort:D", + "label: /SupplementType_texts; text: /SupplementType_texts; kind:7; commit:; sort:D", ]); }); @@ -227,22 +227,22 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: Travel; text: Travel; kind:2; commit:/; sort:B", - "label: Currencies; text: Currencies; kind:2; commit:/; sort:B", - "label: TravelStatus; text: TravelStatus; kind:2; commit:/; sort:B", - "label: TravelAgency; text: TravelAgency; kind:2; commit:/; sort:B", - "label: Passenger; text: Passenger; kind:2; commit:/; sort:B", - "label: Booking; text: Booking; kind:2; commit:/; sort:B", - "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:B", - "label: Countries; text: Countries; kind:2; commit:/; sort:B", - "label: BookingStatus; text: BookingStatus; kind:2; commit:/; sort:B", - "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:B", - "label: Airline; text: Airline; kind:2; commit:/; sort:B", - "label: Flight; text: Flight; kind:2; commit:/; sort:B", - "label: Supplement; text: Supplement; kind:2; commit:/; sort:B", - "label: FlightConnection; text: FlightConnection; kind:2; commit:/; sort:B", - "label: SupplementType; text: SupplementType; kind:2; commit:/; sort:B", - "label: Airport; text: Airport; kind:2; commit:/; sort:B", + "label: Travel; text: Travel; kind:2; commit:/; sort:E", + "label: Currencies; text: Currencies; kind:2; commit:/; sort:E", + "label: TravelStatus; text: TravelStatus; kind:2; commit:/; sort:E", + "label: TravelAgency; text: TravelAgency; kind:2; commit:/; sort:E", + "label: Passenger; text: Passenger; kind:2; commit:/; sort:E", + "label: Booking; text: Booking; kind:2; commit:/; sort:E", + "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:E", + "label: Countries; text: Countries; kind:2; commit:/; sort:E", + "label: BookingStatus; text: BookingStatus; kind:2; commit:/; sort:E", + "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:E", + "label: Airline; text: Airline; kind:2; commit:/; sort:E", + "label: Flight; text: Flight; kind:2; commit:/; sort:E", + "label: Supplement; text: Supplement; kind:2; commit:/; sort:E", + "label: FlightConnection; text: FlightConnection; kind:2; commit:/; sort:E", + "label: SupplementType; text: SupplementType; kind:2; commit:/; sort:E", + "label: Airport; text: Airport; kind:2; commit:/; sort:E", ]); }); @@ -253,14 +253,14 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:B", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:B", + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:B", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:B", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:B", + "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:B", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:B", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:B", ]); }); it("navigation segment completion (case 2)", async function () { @@ -270,14 +270,14 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:B", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:B", + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:B", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:B", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:B", + "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:B", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:B", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:B", ]); }); @@ -288,13 +288,13 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:B", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:B", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:B", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:B", + "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:B", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:B", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:B", ]); }); @@ -305,12 +305,12 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:N", - "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:N", - "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:N", - "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:N", - "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:N", - "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:N", + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:B", + "label: BookingStatus; text: BookingStatus; kind:18; commit:/; sort:B", + "label: to_Carrier; text: to_Carrier; kind:18; commit:/; sort:B", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:B", + "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:B", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:; sort:B", ]); }); @@ -341,15 +341,15 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:/; sort:Z", - "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:B", - "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:B", - "label: /Travel; text: /Travel; kind:7; commit:; sort:A", - "label: /Booking; text: /Booking; kind:7; commit:/; sort:A", - "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:A", - "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:A", + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", ]); }); @@ -377,10 +377,10 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: Travel; text: Travel; kind:2; commit:; sort:B", - "label: Booking; text: Booking; kind:2; commit:/; sort:B", - "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:B", - "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:B", + "label: Travel; text: Travel; kind:2; commit:; sort:E", + "label: Booking; text: Booking; kind:2; commit:/; sort:E", + "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:E", + "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:E", ]); }); @@ -391,8 +391,8 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:B", + "label: to_Travel; text: to_Travel; kind:18; commit:; sort:B", ]); }); @@ -403,8 +403,8 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:N", - "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", + "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:B", + "label: to_Travel; text: to_Travel; kind:18; commit:; sort:B", ]); }); @@ -415,7 +415,7 @@ describe("contextPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: to_Travel; text: to_Travel; kind:18; commit:; sort:N", + "label: to_Travel; text: to_Travel; kind:18; commit:; sort:B", ]); }); diff --git a/packages/fe/test/unit/services/completion/providers/meta-path.test.ts b/packages/fe/test/unit/services/completion/providers/meta-path.test.ts index 3d291a912..848b41519 100644 --- a/packages/fe/test/unit/services/completion/providers/meta-path.test.ts +++ b/packages/fe/test/unit/services/completion/providers/meta-path.test.ts @@ -134,13 +134,6 @@ describe("metaPath attribute value completion", () => { expect(result.length).toEqual(0); }); - it("path is absolute - not supported", async function () { - const result = await getCompletionResult( - `` - ); - expect(result.length).toEqual(0); - }); - it("existing navigation segments not allowed", async function () { const result = await getCompletionResult( `` @@ -198,6 +191,15 @@ describe("metaPath attribute value completion", () => { expect(result.length).toEqual(0); }); + it("contextPath is proved and CC is request after absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([]); + }); + it("service path is not provided in manifest", async function () { const result = await getCompletionResult( ``, @@ -211,34 +213,6 @@ describe("metaPath attribute value completion", () => { ); expect(result.length).toEqual(0); }); - - it("custom views are empty manifest", async function () { - const result = await getCompletionResult( - ``, - (c) => { - const newContext = { - ...c, - manifestDetails: { ...c.manifestDetails, customViews: {} }, - }; - return newContext; - } - ); - expect(result.length).toEqual(0); - }); - - it("custom view id not determined", async function () { - const result = await getCompletionResult( - ``, - (c) => { - const newContext: Context = { - ...c, - customViewId: "", - }; - return newContext; - } - ); - expect(result.length).toEqual(0); - }); }); describe("Annotation path", () => { @@ -254,6 +228,33 @@ describe("metaPath attribute value completion", () => { "label: @com.sap.vocabularies.UI.v1.Chart#sample1; text: @com.sap.vocabularies.UI.v1.Chart#sample1; kind:12; commit:undefined; sort:", "label: to_Booking; text: to_Booking; kind:18; commit:/; sort:B", "label: to_BookedFlights; text: to_BookedFlights; kind:18; commit:/; sort:B", + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + ]); + }); + it("first segment completion - starting with absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: TravelService.EntityContainer; text: TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: TravelService.EntityContainer/Travel; text: TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Booking; text: TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookedFlights; text: TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookingSupplement; text: TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: Travel; text: Travel; kind:7; commit:/; sort:D", + "label: Booking; text: Booking; kind:7; commit:/; sort:D", + "label: BookedFlights; text: BookedFlights; kind:7; commit:/; sort:D", + "label: BookingSupplement; text: BookingSupplement; kind:7; commit:/; sort:D", ]); }); it("second segment completion", async function () { @@ -266,6 +267,17 @@ describe("metaPath attribute value completion", () => { "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:B", ]); }); + it("second segment completion - with absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: @com.sap.vocabularies.UI.v1.Chart; text: @com.sap.vocabularies.UI.v1.Chart; kind:12; commit:undefined; sort:", + "label: @com.sap.vocabularies.UI.v1.Chart#sample1; text: @com.sap.vocabularies.UI.v1.Chart#sample1; kind:12; commit:undefined; sort:", + ]); + }); it("third segment completion", async function () { const result = await getCompletionResult( `` @@ -277,6 +289,30 @@ describe("metaPath attribute value completion", () => { "label: @com.sap.vocabularies.UI.v1.Chart#sample1; text: @com.sap.vocabularies.UI.v1.Chart#sample1; kind:12; commit:undefined; sort:", ]); }); + it("third segment completion - with absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: @com.sap.vocabularies.UI.v1.Chart; text: @com.sap.vocabularies.UI.v1.Chart; kind:12; commit:undefined; sort:", + "label: @com.sap.vocabularies.UI.v1.Chart#sample1; text: @com.sap.vocabularies.UI.v1.Chart#sample1; kind:12; commit:undefined; sort:", + ]); + }); + it("after entity container ", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: Travel; text: Travel; kind:2; commit:/; sort:E", + "label: Booking; text: Booking; kind:2; commit:/; sort:E", + "label: BookedFlights; text: BookedFlights; kind:2; commit:/; sort:E", + "label: BookingSupplement; text: BookingSupplement; kind:2; commit:/; sort:E", + ]); + }); }); describe("metaPath completion with contextPath provided in manifest", () => { @@ -290,6 +326,34 @@ describe("metaPath attribute value completion", () => { ).toStrictEqual([ "label: @com.sap.vocabularies.UI.v1.Chart; text: @com.sap.vocabularies.UI.v1.Chart; kind:12; commit:undefined; sort:", "label: @com.sap.vocabularies.UI.v1.Chart#sample1; text: @com.sap.vocabularies.UI.v1.Chart#sample1; kind:12; commit:undefined; sort:", + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + ]); + }); + it("term segment completion from entity type - starting with absolute path", async function () { + const result = await getCompletionResult( + ``, + prepareContextAdapter("/Travel") + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: TravelService.EntityContainer; text: TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: TravelService.EntityContainer/Travel; text: TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Booking; text: TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookedFlights; text: TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookingSupplement; text: TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: Travel; text: Travel; kind:7; commit:/; sort:D", + "label: Booking; text: Booking; kind:7; commit:/; sort:D", + "label: BookedFlights; text: BookedFlights; kind:7; commit:/; sort:D", + "label: BookingSupplement; text: BookingSupplement; kind:7; commit:/; sort:D", ]); }); it("segment completion from entity set (nav segments allowed)", async function () { @@ -302,6 +366,27 @@ describe("metaPath attribute value completion", () => { ).toStrictEqual([ "label: to_BookSupplement; text: to_BookSupplement; kind:18; commit:/; sort:B", "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:B", + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + ]); + }); + it("two segment completion with absolute path", async function () { + const result = await getCompletionResult( + ``, + prepareContextAdapter("/TravelService.EntityContainer/Booking") + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: @com.sap.vocabularies.UI.v1.Chart; text: @com.sap.vocabularies.UI.v1.Chart; kind:12; commit:undefined; sort:", + "label: @com.sap.vocabularies.UI.v1.Chart#sample1; text: @com.sap.vocabularies.UI.v1.Chart#sample1; kind:12; commit:undefined; sort:", ]); }); }); @@ -348,30 +433,135 @@ describe("metaPath attribute value completion", () => { "label: to_Booking; text: to_Booking; kind:18; commit:/; sort:B", "label: to_BookedFlights; text: to_BookedFlights; kind:18; commit:/; sort:B", "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:/; sort:B", - "label: createdAt; text: createdAt; kind:10; commit:undefined; sort:A", - "label: createdBy; text: createdBy; kind:10; commit:undefined; sort:A", - "label: LastChangedAt; text: LastChangedAt; kind:10; commit:undefined; sort:A", - "label: LastChangedBy; text: LastChangedBy; kind:10; commit:undefined; sort:A", - "label: TravelUUID; text: TravelUUID; kind:10; commit:undefined; sort:A", - "label: TravelID; text: TravelID; kind:10; commit:undefined; sort:A", - "label: BeginDate; text: BeginDate; kind:10; commit:undefined; sort:A", - "label: EndDate; text: EndDate; kind:10; commit:undefined; sort:A", - "label: BookingFee; text: BookingFee; kind:10; commit:undefined; sort:A", - "label: TotalPrice; text: TotalPrice; kind:10; commit:undefined; sort:A", - "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:undefined; sort:A", - "label: Description; text: Description; kind:10; commit:undefined; sort:A", - "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:undefined; sort:A", - "label: GoGreen; text: GoGreen; kind:10; commit:undefined; sort:A", - "label: GreenFee; text: GreenFee; kind:10; commit:undefined; sort:A", - "label: TreesPlanted; text: TreesPlanted; kind:10; commit:undefined; sort:A", - "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:undefined; sort:A", - "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:undefined; sort:A", - "label: acceptEnabled; text: acceptEnabled; kind:10; commit:undefined; sort:A", - "label: rejectEnabled; text: rejectEnabled; kind:10; commit:undefined; sort:A", - "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:undefined; sort:A", - "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:undefined; sort:A", + "label: createdAt; text: createdAt; kind:10; commit:/; sort:A", + "label: createdBy; text: createdBy; kind:10; commit:/; sort:A", + "label: LastChangedAt; text: LastChangedAt; kind:10; commit:/; sort:A", + "label: LastChangedBy; text: LastChangedBy; kind:10; commit:/; sort:A", + "label: TravelUUID; text: TravelUUID; kind:10; commit:/; sort:A", + "label: TravelID; text: TravelID; kind:10; commit:/; sort:A", + "label: BeginDate; text: BeginDate; kind:10; commit:/; sort:A", + "label: EndDate; text: EndDate; kind:10; commit:/; sort:A", + "label: BookingFee; text: BookingFee; kind:10; commit:/; sort:A", + "label: TotalPrice; text: TotalPrice; kind:10; commit:/; sort:A", + "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:/; sort:A", + "label: Description; text: Description; kind:10; commit:/; sort:A", + "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:/; sort:A", + "label: GoGreen; text: GoGreen; kind:10; commit:/; sort:A", + "label: GreenFee; text: GreenFee; kind:10; commit:/; sort:A", + "label: TreesPlanted; text: TreesPlanted; kind:10; commit:/; sort:A", + "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:/; sort:A", + "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:/; sort:A", + "label: acceptEnabled; text: acceptEnabled; kind:10; commit:/; sort:A", + "label: rejectEnabled; text: rejectEnabled; kind:10; commit:/; sort:A", + "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:/; sort:A", + "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:/; sort:A", + "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:/; sort:A", + "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:/; sort:A", + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/HighestTotal; text: /TravelService.EntityContainer/HighestTotal; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/Currencies; text: /TravelService.EntityContainer/Currencies; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/TravelStatus; text: /TravelService.EntityContainer/TravelStatus; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/TravelAgency; text: /TravelService.EntityContainer/TravelAgency; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Passenger; text: /TravelService.EntityContainer/Passenger; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Countries; text: /TravelService.EntityContainer/Countries; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingStatus; text: /TravelService.EntityContainer/BookingStatus; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Airline; text: /TravelService.EntityContainer/Airline; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Flight; text: /TravelService.EntityContainer/Flight; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Supplement; text: /TravelService.EntityContainer/Supplement; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/FlightConnection; text: /TravelService.EntityContainer/FlightConnection; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/SupplementType; text: /TravelService.EntityContainer/SupplementType; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Airport; text: /TravelService.EntityContainer/Airport; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Currencies_texts; text: /TravelService.EntityContainer/Currencies_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/TravelStatus_texts; text: /TravelService.EntityContainer/TravelStatus_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/Countries_texts; text: /TravelService.EntityContainer/Countries_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/BookingStatus_texts; text: /TravelService.EntityContainer/BookingStatus_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/Supplement_texts; text: /TravelService.EntityContainer/Supplement_texts; kind:2; commit:; sort:E", + "label: /TravelService.EntityContainer/SupplementType_texts; text: /TravelService.EntityContainer/SupplementType_texts; kind:2; commit:; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /HighestTotal; text: /HighestTotal; kind:7; commit:; sort:D", + "label: /Currencies; text: /Currencies; kind:7; commit:/; sort:D", + "label: /TravelStatus; text: /TravelStatus; kind:7; commit:/; sort:D", + "label: /TravelAgency; text: /TravelAgency; kind:7; commit:/; sort:D", + "label: /Passenger; text: /Passenger; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /Countries; text: /Countries; kind:7; commit:/; sort:D", + "label: /BookingStatus; text: /BookingStatus; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + "label: /Airline; text: /Airline; kind:7; commit:/; sort:D", + "label: /Flight; text: /Flight; kind:7; commit:/; sort:D", + "label: /Supplement; text: /Supplement; kind:7; commit:/; sort:D", + "label: /FlightConnection; text: /FlightConnection; kind:7; commit:/; sort:D", + "label: /SupplementType; text: /SupplementType; kind:7; commit:/; sort:D", + "label: /Airport; text: /Airport; kind:7; commit:/; sort:D", + "label: /DraftAdministrativeData; text: /DraftAdministrativeData; kind:7; commit:; sort:D", + "label: /Currencies_texts; text: /Currencies_texts; kind:7; commit:; sort:D", + "label: /TravelStatus_texts; text: /TravelStatus_texts; kind:7; commit:; sort:D", + "label: /Countries_texts; text: /Countries_texts; kind:7; commit:; sort:D", + "label: /BookingStatus_texts; text: /BookingStatus_texts; kind:7; commit:; sort:D", + "label: /Supplement_texts; text: /Supplement_texts; kind:7; commit:; sort:D", + "label: /SupplementType_texts; text: /SupplementType_texts; kind:7; commit:; sort:D", + ]); + }); + it("first segment completion - starting with absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: TravelService.EntityContainer; text: TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: TravelService.EntityContainer/Travel; text: TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/HighestTotal; text: TravelService.EntityContainer/HighestTotal; kind:2; commit:; sort:E", + "label: TravelService.EntityContainer/Currencies; text: TravelService.EntityContainer/Currencies; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/TravelStatus; text: TravelService.EntityContainer/TravelStatus; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/TravelAgency; text: TravelService.EntityContainer/TravelAgency; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Passenger; text: TravelService.EntityContainer/Passenger; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Booking; text: TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookedFlights; text: TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Countries; text: TravelService.EntityContainer/Countries; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookingStatus; text: TravelService.EntityContainer/BookingStatus; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/BookingSupplement; text: TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Airline; text: TravelService.EntityContainer/Airline; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Flight; text: TravelService.EntityContainer/Flight; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Supplement; text: TravelService.EntityContainer/Supplement; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/FlightConnection; text: TravelService.EntityContainer/FlightConnection; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/SupplementType; text: TravelService.EntityContainer/SupplementType; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Airport; text: TravelService.EntityContainer/Airport; kind:2; commit:/; sort:E", + "label: TravelService.EntityContainer/Currencies_texts; text: TravelService.EntityContainer/Currencies_texts; kind:2; commit:; sort:E", + "label: TravelService.EntityContainer/TravelStatus_texts; text: TravelService.EntityContainer/TravelStatus_texts; kind:2; commit:; sort:E", + "label: TravelService.EntityContainer/Countries_texts; text: TravelService.EntityContainer/Countries_texts; kind:2; commit:; sort:E", + "label: TravelService.EntityContainer/BookingStatus_texts; text: TravelService.EntityContainer/BookingStatus_texts; kind:2; commit:; sort:E", + "label: TravelService.EntityContainer/Supplement_texts; text: TravelService.EntityContainer/Supplement_texts; kind:2; commit:; sort:E", + "label: TravelService.EntityContainer/SupplementType_texts; text: TravelService.EntityContainer/SupplementType_texts; kind:2; commit:; sort:E", + "label: Travel; text: Travel; kind:7; commit:/; sort:D", + "label: HighestTotal; text: HighestTotal; kind:7; commit:; sort:D", + "label: Currencies; text: Currencies; kind:7; commit:/; sort:D", + "label: TravelStatus; text: TravelStatus; kind:7; commit:/; sort:D", + "label: TravelAgency; text: TravelAgency; kind:7; commit:/; sort:D", + "label: Passenger; text: Passenger; kind:7; commit:/; sort:D", + "label: Booking; text: Booking; kind:7; commit:/; sort:D", + "label: BookedFlights; text: BookedFlights; kind:7; commit:/; sort:D", + "label: Countries; text: Countries; kind:7; commit:/; sort:D", + "label: BookingStatus; text: BookingStatus; kind:7; commit:/; sort:D", + "label: BookingSupplement; text: BookingSupplement; kind:7; commit:/; sort:D", + "label: Airline; text: Airline; kind:7; commit:/; sort:D", + "label: Flight; text: Flight; kind:7; commit:/; sort:D", + "label: Supplement; text: Supplement; kind:7; commit:/; sort:D", + "label: FlightConnection; text: FlightConnection; kind:7; commit:/; sort:D", + "label: SupplementType; text: SupplementType; kind:7; commit:/; sort:D", + "label: Airport; text: Airport; kind:7; commit:/; sort:D", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:7; commit:; sort:D", + "label: Currencies_texts; text: Currencies_texts; kind:7; commit:; sort:D", + "label: TravelStatus_texts; text: TravelStatus_texts; kind:7; commit:; sort:D", + "label: Countries_texts; text: Countries_texts; kind:7; commit:; sort:D", + "label: BookingStatus_texts; text: BookingStatus_texts; kind:7; commit:; sort:D", + "label: Supplement_texts; text: Supplement_texts; kind:7; commit:; sort:D", + "label: SupplementType_texts; text: SupplementType_texts; kind:7; commit:; sort:D", ]); }); it("second segment completion", async function () { @@ -388,27 +578,67 @@ describe("metaPath attribute value completion", () => { "label: to_Travel; text: to_Travel; kind:18; commit:/; sort:B", "label: to_Flight; text: to_Flight; kind:18; commit:/; sort:B", "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:/; sort:B", - "label: createdAt; text: createdAt; kind:10; commit:undefined; sort:A", - "label: createdBy; text: createdBy; kind:10; commit:undefined; sort:A", - "label: LastChangedAt; text: LastChangedAt; kind:10; commit:undefined; sort:A", - "label: LastChangedBy; text: LastChangedBy; kind:10; commit:undefined; sort:A", - "label: BookingUUID; text: BookingUUID; kind:10; commit:undefined; sort:A", - "label: BookingID; text: BookingID; kind:10; commit:undefined; sort:A", - "label: BookingDate; text: BookingDate; kind:10; commit:undefined; sort:A", - "label: ConnectionID; text: ConnectionID; kind:10; commit:undefined; sort:A", - "label: FlightDate; text: FlightDate; kind:10; commit:undefined; sort:A", - "label: FlightPrice; text: FlightPrice; kind:10; commit:undefined; sort:A", - "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:undefined; sort:A", - "label: BookingStatus_code; text: BookingStatus_code; kind:10; commit:undefined; sort:A", - "label: to_Carrier_AirlineID; text: to_Carrier_AirlineID; kind:10; commit:undefined; sort:A", - "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:undefined; sort:A", - "label: to_Travel_TravelUUID; text: to_Travel_TravelUUID; kind:10; commit:undefined; sort:A", - "label: criticality; text: criticality; kind:10; commit:undefined; sort:A", - "label: BookedFlights; text: BookedFlights; kind:10; commit:undefined; sort:A", - "label: EligibleForPrime; text: EligibleForPrime; kind:10; commit:undefined; sort:A", - "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:undefined; sort:A", + "label: createdAt; text: createdAt; kind:10; commit:/; sort:A", + "label: createdBy; text: createdBy; kind:10; commit:/; sort:A", + "label: LastChangedAt; text: LastChangedAt; kind:10; commit:/; sort:A", + "label: LastChangedBy; text: LastChangedBy; kind:10; commit:/; sort:A", + "label: BookingUUID; text: BookingUUID; kind:10; commit:/; sort:A", + "label: BookingID; text: BookingID; kind:10; commit:/; sort:A", + "label: BookingDate; text: BookingDate; kind:10; commit:/; sort:A", + "label: ConnectionID; text: ConnectionID; kind:10; commit:/; sort:A", + "label: FlightDate; text: FlightDate; kind:10; commit:/; sort:A", + "label: FlightPrice; text: FlightPrice; kind:10; commit:/; sort:A", + "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:/; sort:A", + "label: BookingStatus_code; text: BookingStatus_code; kind:10; commit:/; sort:A", + "label: to_Carrier_AirlineID; text: to_Carrier_AirlineID; kind:10; commit:/; sort:A", + "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:/; sort:A", + "label: to_Travel_TravelUUID; text: to_Travel_TravelUUID; kind:10; commit:/; sort:A", + "label: criticality; text: criticality; kind:10; commit:/; sort:A", + "label: BookedFlights; text: BookedFlights; kind:10; commit:/; sort:A", + "label: EligibleForPrime; text: EligibleForPrime; kind:10; commit:/; sort:A", + "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:/; sort:A", + "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:/; sort:A", + "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:/; sort:A", + ]); + }); + it("second segment completion - with absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: CurrencyCode; text: CurrencyCode; kind:18; commit:/; sort:B", + "label: TravelStatus; text: TravelStatus; kind:18; commit:/; sort:B", + "label: to_Agency; text: to_Agency; kind:18; commit:/; sort:B", + "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:B", + "label: to_Booking; text: to_Booking; kind:18; commit:/; sort:B", + "label: to_BookedFlights; text: to_BookedFlights; kind:18; commit:/; sort:B", + "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:/; sort:B", + "label: createdAt; text: createdAt; kind:10; commit:/; sort:A", + "label: createdBy; text: createdBy; kind:10; commit:/; sort:A", + "label: LastChangedAt; text: LastChangedAt; kind:10; commit:/; sort:A", + "label: LastChangedBy; text: LastChangedBy; kind:10; commit:/; sort:A", + "label: TravelUUID; text: TravelUUID; kind:10; commit:/; sort:A", + "label: TravelID; text: TravelID; kind:10; commit:/; sort:A", + "label: BeginDate; text: BeginDate; kind:10; commit:/; sort:A", + "label: EndDate; text: EndDate; kind:10; commit:/; sort:A", + "label: BookingFee; text: BookingFee; kind:10; commit:/; sort:A", + "label: TotalPrice; text: TotalPrice; kind:10; commit:/; sort:A", + "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:/; sort:A", + "label: Description; text: Description; kind:10; commit:/; sort:A", + "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:/; sort:A", + "label: GoGreen; text: GoGreen; kind:10; commit:/; sort:A", + "label: GreenFee; text: GreenFee; kind:10; commit:/; sort:A", + "label: TreesPlanted; text: TreesPlanted; kind:10; commit:/; sort:A", + "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:/; sort:A", + "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:/; sort:A", + "label: acceptEnabled; text: acceptEnabled; kind:10; commit:/; sort:A", + "label: rejectEnabled; text: rejectEnabled; kind:10; commit:/; sort:A", + "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:/; sort:A", + "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:/; sort:A", + "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:/; sort:A", + "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:/; sort:A", ]); }); it("third segment completion", async function () { @@ -423,30 +653,49 @@ describe("metaPath attribute value completion", () => { "label: to_Agency; text: to_Agency; kind:18; commit:/; sort:B", "label: to_Customer; text: to_Customer; kind:18; commit:/; sort:B", "label: DraftAdministrativeData; text: DraftAdministrativeData; kind:18; commit:/; sort:B", - "label: createdAt; text: createdAt; kind:10; commit:undefined; sort:A", - "label: createdBy; text: createdBy; kind:10; commit:undefined; sort:A", - "label: LastChangedAt; text: LastChangedAt; kind:10; commit:undefined; sort:A", - "label: LastChangedBy; text: LastChangedBy; kind:10; commit:undefined; sort:A", - "label: TravelUUID; text: TravelUUID; kind:10; commit:undefined; sort:A", - "label: TravelID; text: TravelID; kind:10; commit:undefined; sort:A", - "label: BeginDate; text: BeginDate; kind:10; commit:undefined; sort:A", - "label: EndDate; text: EndDate; kind:10; commit:undefined; sort:A", - "label: BookingFee; text: BookingFee; kind:10; commit:undefined; sort:A", - "label: TotalPrice; text: TotalPrice; kind:10; commit:undefined; sort:A", - "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:undefined; sort:A", - "label: Description; text: Description; kind:10; commit:undefined; sort:A", - "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:undefined; sort:A", - "label: GoGreen; text: GoGreen; kind:10; commit:undefined; sort:A", - "label: GreenFee; text: GreenFee; kind:10; commit:undefined; sort:A", - "label: TreesPlanted; text: TreesPlanted; kind:10; commit:undefined; sort:A", - "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:undefined; sort:A", - "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:undefined; sort:A", - "label: acceptEnabled; text: acceptEnabled; kind:10; commit:undefined; sort:A", - "label: rejectEnabled; text: rejectEnabled; kind:10; commit:undefined; sort:A", - "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:undefined; sort:A", - "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:undefined; sort:A", + "label: createdAt; text: createdAt; kind:10; commit:/; sort:A", + "label: createdBy; text: createdBy; kind:10; commit:/; sort:A", + "label: LastChangedAt; text: LastChangedAt; kind:10; commit:/; sort:A", + "label: LastChangedBy; text: LastChangedBy; kind:10; commit:/; sort:A", + "label: TravelUUID; text: TravelUUID; kind:10; commit:/; sort:A", + "label: TravelID; text: TravelID; kind:10; commit:/; sort:A", + "label: BeginDate; text: BeginDate; kind:10; commit:/; sort:A", + "label: EndDate; text: EndDate; kind:10; commit:/; sort:A", + "label: BookingFee; text: BookingFee; kind:10; commit:/; sort:A", + "label: TotalPrice; text: TotalPrice; kind:10; commit:/; sort:A", + "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:/; sort:A", + "label: Description; text: Description; kind:10; commit:/; sort:A", + "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:/; sort:A", + "label: GoGreen; text: GoGreen; kind:10; commit:/; sort:A", + "label: GreenFee; text: GreenFee; kind:10; commit:/; sort:A", + "label: TreesPlanted; text: TreesPlanted; kind:10; commit:/; sort:A", + "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:/; sort:A", + "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:/; sort:A", + "label: acceptEnabled; text: acceptEnabled; kind:10; commit:/; sort:A", + "label: rejectEnabled; text: rejectEnabled; kind:10; commit:/; sort:A", + "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:/; sort:A", + "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:/; sort:A", + "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:/; sort:A", + "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:/; sort:A", + ]); + }); + it("third segment completion - with absolute path", async function () { + const result = await getCompletionResult( + `` + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: CountryCode; text: CountryCode; kind:18; commit:/; sort:B", + "label: AgencyID; text: AgencyID; kind:10; commit:/; sort:A", + "label: Name; text: Name; kind:10; commit:/; sort:A", + "label: Street; text: Street; kind:10; commit:/; sort:A", + "label: PostalCode; text: PostalCode; kind:10; commit:/; sort:A", + "label: City; text: City; kind:10; commit:/; sort:A", + "label: CountryCode_code; text: CountryCode_code; kind:10; commit:/; sort:A", + "label: PhoneNumber; text: PhoneNumber; kind:10; commit:/; sort:A", + "label: EMailAddress; text: EMailAddress; kind:10; commit:/; sort:A", + "label: WebAddress; text: WebAddress; kind:10; commit:/; sort:A", ]); }); }); @@ -459,30 +708,30 @@ describe("metaPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: createdAt; text: createdAt; kind:10; commit:undefined; sort:A", - "label: createdBy; text: createdBy; kind:10; commit:undefined; sort:A", - "label: LastChangedAt; text: LastChangedAt; kind:10; commit:undefined; sort:A", - "label: LastChangedBy; text: LastChangedBy; kind:10; commit:undefined; sort:A", - "label: TravelUUID; text: TravelUUID; kind:10; commit:undefined; sort:A", - "label: TravelID; text: TravelID; kind:10; commit:undefined; sort:A", - "label: BeginDate; text: BeginDate; kind:10; commit:undefined; sort:A", - "label: EndDate; text: EndDate; kind:10; commit:undefined; sort:A", - "label: BookingFee; text: BookingFee; kind:10; commit:undefined; sort:A", - "label: TotalPrice; text: TotalPrice; kind:10; commit:undefined; sort:A", - "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:undefined; sort:A", - "label: Description; text: Description; kind:10; commit:undefined; sort:A", - "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:undefined; sort:A", - "label: GoGreen; text: GoGreen; kind:10; commit:undefined; sort:A", - "label: GreenFee; text: GreenFee; kind:10; commit:undefined; sort:A", - "label: TreesPlanted; text: TreesPlanted; kind:10; commit:undefined; sort:A", - "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:undefined; sort:A", - "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:undefined; sort:A", - "label: acceptEnabled; text: acceptEnabled; kind:10; commit:undefined; sort:A", - "label: rejectEnabled; text: rejectEnabled; kind:10; commit:undefined; sort:A", - "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:undefined; sort:A", - "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:undefined; sort:A", + "label: createdAt; text: createdAt; kind:10; commit:/; sort:A", + "label: createdBy; text: createdBy; kind:10; commit:/; sort:A", + "label: LastChangedAt; text: LastChangedAt; kind:10; commit:/; sort:A", + "label: LastChangedBy; text: LastChangedBy; kind:10; commit:/; sort:A", + "label: TravelUUID; text: TravelUUID; kind:10; commit:/; sort:A", + "label: TravelID; text: TravelID; kind:10; commit:/; sort:A", + "label: BeginDate; text: BeginDate; kind:10; commit:/; sort:A", + "label: EndDate; text: EndDate; kind:10; commit:/; sort:A", + "label: BookingFee; text: BookingFee; kind:10; commit:/; sort:A", + "label: TotalPrice; text: TotalPrice; kind:10; commit:/; sort:A", + "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:/; sort:A", + "label: Description; text: Description; kind:10; commit:/; sort:A", + "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:/; sort:A", + "label: GoGreen; text: GoGreen; kind:10; commit:/; sort:A", + "label: GreenFee; text: GreenFee; kind:10; commit:/; sort:A", + "label: TreesPlanted; text: TreesPlanted; kind:10; commit:/; sort:A", + "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:/; sort:A", + "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:/; sort:A", + "label: acceptEnabled; text: acceptEnabled; kind:10; commit:/; sort:A", + "label: rejectEnabled; text: rejectEnabled; kind:10; commit:/; sort:A", + "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:/; sort:A", + "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:/; sort:A", + "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:/; sort:A", + "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:/; sort:A", ]); }); it("property segment completion from entity set", async function () { @@ -492,32 +741,86 @@ describe("metaPath attribute value completion", () => { expect( result.map((item) => completionItemToSnapshot(item)) ).toStrictEqual([ - "label: createdAt; text: createdAt; kind:10; commit:undefined; sort:A", - "label: createdBy; text: createdBy; kind:10; commit:undefined; sort:A", - "label: LastChangedAt; text: LastChangedAt; kind:10; commit:undefined; sort:A", - "label: LastChangedBy; text: LastChangedBy; kind:10; commit:undefined; sort:A", - "label: TravelUUID; text: TravelUUID; kind:10; commit:undefined; sort:A", - "label: TravelID; text: TravelID; kind:10; commit:undefined; sort:A", - "label: BeginDate; text: BeginDate; kind:10; commit:undefined; sort:A", - "label: EndDate; text: EndDate; kind:10; commit:undefined; sort:A", - "label: BookingFee; text: BookingFee; kind:10; commit:undefined; sort:A", - "label: TotalPrice; text: TotalPrice; kind:10; commit:undefined; sort:A", - "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:undefined; sort:A", - "label: Description; text: Description; kind:10; commit:undefined; sort:A", - "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:undefined; sort:A", - "label: GoGreen; text: GoGreen; kind:10; commit:undefined; sort:A", - "label: GreenFee; text: GreenFee; kind:10; commit:undefined; sort:A", - "label: TreesPlanted; text: TreesPlanted; kind:10; commit:undefined; sort:A", - "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:undefined; sort:A", - "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:undefined; sort:A", - "label: acceptEnabled; text: acceptEnabled; kind:10; commit:undefined; sort:A", - "label: rejectEnabled; text: rejectEnabled; kind:10; commit:undefined; sort:A", - "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:undefined; sort:A", - "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:undefined; sort:A", - "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:undefined; sort:A", + "label: createdAt; text: createdAt; kind:10; commit:/; sort:A", + "label: createdBy; text: createdBy; kind:10; commit:/; sort:A", + "label: LastChangedAt; text: LastChangedAt; kind:10; commit:/; sort:A", + "label: LastChangedBy; text: LastChangedBy; kind:10; commit:/; sort:A", + "label: TravelUUID; text: TravelUUID; kind:10; commit:/; sort:A", + "label: TravelID; text: TravelID; kind:10; commit:/; sort:A", + "label: BeginDate; text: BeginDate; kind:10; commit:/; sort:A", + "label: EndDate; text: EndDate; kind:10; commit:/; sort:A", + "label: BookingFee; text: BookingFee; kind:10; commit:/; sort:A", + "label: TotalPrice; text: TotalPrice; kind:10; commit:/; sort:A", + "label: CurrencyCode_code; text: CurrencyCode_code; kind:10; commit:/; sort:A", + "label: Description; text: Description; kind:10; commit:/; sort:A", + "label: TravelStatus_code; text: TravelStatus_code; kind:10; commit:/; sort:A", + "label: GoGreen; text: GoGreen; kind:10; commit:/; sort:A", + "label: GreenFee; text: GreenFee; kind:10; commit:/; sort:A", + "label: TreesPlanted; text: TreesPlanted; kind:10; commit:/; sort:A", + "label: to_Agency_AgencyID; text: to_Agency_AgencyID; kind:10; commit:/; sort:A", + "label: to_Customer_CustomerID; text: to_Customer_CustomerID; kind:10; commit:/; sort:A", + "label: acceptEnabled; text: acceptEnabled; kind:10; commit:/; sort:A", + "label: rejectEnabled; text: rejectEnabled; kind:10; commit:/; sort:A", + "label: deductDiscountEnabled; text: deductDiscountEnabled; kind:10; commit:/; sort:A", + "label: IsActiveEntity; text: IsActiveEntity; kind:10; commit:/; sort:A", + "label: HasActiveEntity; text: HasActiveEntity; kind:10; commit:/; sort:A", + "label: HasDraftEntity; text: HasDraftEntity; kind:10; commit:/; sort:A", ]); }); }); }); + + describe("custom views", () => { + it("custom views are empty manifest", async function () { + const result = await getCompletionResult( + ``, + (c) => { + const newContext = { + ...c, + manifestDetails: { ...c.manifestDetails, customViews: {} }, + }; + return newContext; + } + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + ]); + }); + + it("custom view id not determined", async function () { + const result = await getCompletionResult( + ``, + (c) => { + const newContext: Context = { + ...c, + customViewId: "", + }; + return newContext; + } + ); + expect( + result.map((item) => completionItemToSnapshot(item)) + ).toStrictEqual([ + "label: /TravelService.EntityContainer; text: /TravelService.EntityContainer; kind:19; commit:; sort:Z", + "label: /TravelService.EntityContainer/Travel; text: /TravelService.EntityContainer/Travel; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/Booking; text: /TravelService.EntityContainer/Booking; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookedFlights; text: /TravelService.EntityContainer/BookedFlights; kind:2; commit:/; sort:E", + "label: /TravelService.EntityContainer/BookingSupplement; text: /TravelService.EntityContainer/BookingSupplement; kind:2; commit:/; sort:E", + "label: /Travel; text: /Travel; kind:7; commit:/; sort:D", + "label: /Booking; text: /Booking; kind:7; commit:/; sort:D", + "label: /BookedFlights; text: /BookedFlights; kind:7; commit:/; sort:D", + "label: /BookingSupplement; text: /BookingSupplement; kind:7; commit:/; sort:D", + ]); + }); + }); }); diff --git a/packages/fe/test/unit/services/diagnostics/validators/wrong-filterbar-id.test.ts b/packages/fe/test/unit/services/diagnostics/validators/wrong-filterbar-id.test.ts index 2753a9783..e27c55db5 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/wrong-filterbar-id.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/wrong-filterbar-id.test.ts @@ -101,7 +101,7 @@ describe("filterBar attribute value validation", () => { }); }); - it("shows warning when is is not empty and no filterBar macros elements in the document", async function () { + it("shows warning when it is not empty and no filterBar macros elements in the document", async function () { const result = await validateView( `` ); From 0221327d081e3976fceb02df80a307ef2f4e9075 Mon Sep 17 00:00:00 2001 From: Maruf Rasully Date: Thu, 25 Apr 2024 13:56:05 +0200 Subject: [PATCH 2/7] fix: validation - draft 02 --- .../diagnostics/validators/absolute-path.ts | 188 +++++++++++ .../diagnostics/validators/meta-path.ts | 202 +++++++++++ .../validators/missing-entity-set.ts | 4 + .../validators/unknown-annotation-path.ts | 96 ++++-- .../validators/unknown-annotation-target.ts | 317 +++++++++--------- .../validators/unknown-property-path.ts | 2 +- .../unknown-annotation-path.test.ts | 19 +- .../unknown-annotation-target.test.ts | 1 + 8 files changed, 638 insertions(+), 191 deletions(-) create mode 100644 packages/fe/src/services/diagnostics/validators/absolute-path.ts create mode 100644 packages/fe/src/services/diagnostics/validators/meta-path.ts diff --git a/packages/fe/src/services/diagnostics/validators/absolute-path.ts b/packages/fe/src/services/diagnostics/validators/absolute-path.ts new file mode 100644 index 000000000..3f36722ae --- /dev/null +++ b/packages/fe/src/services/diagnostics/validators/absolute-path.ts @@ -0,0 +1,188 @@ +import { XMLAttribute } from "@xml-tools/ast"; +import { Context } from "@ui5-language-assistant/context"; +import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; +import { isPossibleBindingAttributeValue } from "@ui5-language-assistant/xml-views-validation"; +import { + AnnotationIssue, + ANNOTATION_ISSUE_TYPE, + SAP_FE_MACROS, +} from "../../../types"; +import { + getPathConstraintsForControl, + isPropertyPathAllowed, + resolvePathTarget, + getAnnotationAppliedOnElement, + normalizePath, + TypeNameMap, + t, + AnnotationTerm, + AllowedTargetType, + ResolvedPathTargetType, +} from "../../../utils"; +import { + EntityContainer, + EntitySet, + EntityType, + Singleton, +} from "@sap-ux/vocabularies-types"; +import { ServiceDetails } from "@ui5-language-assistant/context/src/types"; + +interface ValidateAbsolutePathResult { + issues: AnnotationIssue[]; + resolvedTarget: ResolvedPathTargetType | undefined; +} +export function validateAbsolutePath( + attribute: XMLAttribute, + absolutePath: string, + expectedTypes: AllowedTargetType[], + expectedTypesMetaPath: AllowedTargetType[], + isPropertyPath: boolean, + service: ServiceDetails, + isNotRecommended = false +): ValidateAbsolutePathResult { + // always must be defined + // const actualAttributeValue = attribute.value!; + // always must be defined + const actualAttributeValueToken = attribute.syntax.value!; + + const result: ValidateAbsolutePathResult = { + issues: [], + resolvedTarget: undefined, + }; + const pushToResult = (item: AnnotationIssue) => { + result.issues.push(item); + return result; + }; + + if (!absolutePath.startsWith("/")) { + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("INVALID_CONTEXT_PATH_VALUE", { + value: absolutePath, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } + + const normalizedValue = normalizePath(absolutePath); + const expectedTypesList = ( + isNotRecommended ? expectedTypesMetaPath : expectedTypes + ) + .map((item) => TypeNameMap[item]) + .join(", "); + + // Check by segments + const { + target, + targetStructuredType: targetEntity, + isCardinalityIssue, + lastValidSegmentIndex, + } = resolvePathTarget(service.convertedMetadata, normalizedValue); + const originalSegments = absolutePath.split("/"); + + result.resolvedTarget = target; + + if (target?._type === "Property") { + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + actualType: "Edm.Property", + expectedTypes: expectedTypesList, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } + + if (target?._type === "EntityContainer") { + const message = t( + isNotRecommended + ? "INCOMPLETE_CONTEXT_PATH_LEADS_TO_ENTITY_CONTAINER" + : "INCOMPLETE_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" + ); + return pushToResult({ + kind: "IncompletePath", + issueType: ANNOTATION_ISSUE_TYPE, + message, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } + + if (!target || !targetEntity) { + if (!isCardinalityIssue) { + // Path does not exist + originalSegments.splice(lastValidSegmentIndex + 1); + const correctPart = originalSegments.length + ? "/" + originalSegments.join("/") + : ""; + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("UNKNOWN_CONTEXT_PATH", { value: absolutePath }), // todo -> check value and affected range + offsetRange: { + start: actualAttributeValueToken.startOffset + correctPart.length + 1, + end: actualAttributeValueToken.endOffset - 1, + }, + severity: "warn", + }); + } else { + // segment found but preceding path leads to collection + originalSegments.splice(lastValidSegmentIndex + 1); + const correctPart = originalSegments.join("/"); + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("INVALID_CONTEXT_PATH_MULTIPLE_1_TO_MANY"), + offsetRange: { + start: actualAttributeValueToken.startOffset + correctPart.length + 1, + end: actualAttributeValueToken.endOffset - 1, + }, + severity: "warn", + }); + } + } else { + if ( + (!isNotRecommended && !expectedTypes.includes(target._type)) || + (isNotRecommended && !expectedTypesMetaPath.includes(target._type)) + ) { + return pushToResult({ + kind: "InvalidAnnotationTarget", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + actualType: TypeNameMap[target._type], + expectedTypes: expectedTypesList, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } + + if (isPropertyPath) { + return { + issues: [], + resolvedTarget: target, + }; + } + + // TODO: required and actual cardinality mismatch check + // return pushToResult(issue); + // } + } + return result; +} diff --git a/packages/fe/src/services/diagnostics/validators/meta-path.ts b/packages/fe/src/services/diagnostics/validators/meta-path.ts new file mode 100644 index 000000000..5bb8c7073 --- /dev/null +++ b/packages/fe/src/services/diagnostics/validators/meta-path.ts @@ -0,0 +1,202 @@ +import { XMLAttribute } from "@xml-tools/ast"; +import { Context } from "@ui5-language-assistant/context"; +import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; +import { isPossibleBindingAttributeValue } from "@ui5-language-assistant/xml-views-validation"; +import { + AnnotationIssue, + ANNOTATION_ISSUE_TYPE, + SAP_FE_MACROS, + ContextPathOrigin, +} from "../../../types"; +import { + getContextPath, + getElementAttributeValue, + getPathConstraintsForControl, + isPropertyPathAllowed, + normalizePath, + resolveContextPath, + resolvePathTarget, + t, + TypeNameMap, +} from "../../../utils"; + +import { + EntityContainer, + EntitySet, + EntityType, + Singleton, + Property, +} from "@sap-ux/vocabularies-types"; + +/** + * absolute path + * + * non-absolute + * + * UnknownEnumValueIssue - any invalid path segment or property does not exits in path + * UnknownAnnotationPathIssue - not used + * AnnotationTargetRequiredIssue - target is mandatory + * AnnotationPathRequiredIssue - probabally when metpath is empty or in complete + * PathDoesNotExistIssue - any invalid segment in path + * + */ +export function validateMetaPath( + attribute: XMLAttribute, + context: Context +): AnnotationIssue[] { + let isNavSegmentsAllowed = true; + let base: + | EntityContainer + | EntitySet + | EntityType + | Singleton + | Property + | undefined; + let baseType: EntityType | undefined; + let normalizedContextPath: string; + + const actualAttributeValue = attribute.value; + const actualAttributeValueToken = attribute.syntax.value; + if ( + actualAttributeValue === null || + actualAttributeValueToken === undefined || + isPossibleBindingAttributeValue(actualAttributeValue) + ) { + return []; + } + + const ui5Property = getUI5PropertyByXMLAttributeKey( + attribute, + context.ui5Model + ); + + if ( + ui5Property?.library !== SAP_FE_MACROS || + ui5Property.name !== "metaPath" + ) { + return []; + } + + const mainServicePath = context.manifestDetails.mainServicePath; + const service = mainServicePath + ? context.services[mainServicePath] + : undefined; + if (!service) { + return []; + } + const element = attribute.parent; + let value = attribute.value; + const control = element.name; + if (!value) { + // todo + return []; + } + const isAbsolutePath = value.startsWith("/"); + + if (!isAbsolutePath) { + return []; + } + + let proceedingPath = value; + let lastSegment = ""; + + if (value.includes("@")) { + // it must be annotation path + const parts = value.split("/"); + lastSegment = parts.pop() ?? ""; + proceedingPath = parts.join("/"); + } + + const resolvedContext = resolveContextPath( + context, + element, + isAbsolutePath, + proceedingPath + ); + + if (!resolvedContext) { + return []; + } + // /travel/to_nave/ + const { contextPath, origin } = resolvedContext; + + const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( + control, + ui5Property + ); + const isPropertiesAllowed = expectedTypes.includes("Property"); + const metadata = service.convertedMetadata; + let isCollection: boolean | undefined; + let lastValidSegmentIndex: number; + let isCardinalityIssue: boolean; + let milestones: string[]; + normalizedContextPath = normalizePath(contextPath); + ({ + target: base, + targetStructuredType: baseType, + isCardinalityIssue, + isCollection, + lastValidSegmentIndex, + milestones, + } = resolvePathTarget(metadata, normalizedContextPath)); + + if (!base) { + // not resolved. issue can be either in contextPath in manifest.json, or entity set. contePath in xml may be handled somewhere else. + // todo + if (isPropertiesAllowed) { + return [ + { + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: `Wrong path ${value}`, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }, + ]; + } + return []; + } + + if (isPropertiesAllowed && base._type !== "Property") { + return [ + { + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: `Path must ends with property path ${value}`, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }, + ]; + } + + if (!isPropertiesAllowed && expectedAnnotations.length > 0) { + // check last segment + const termName = lastSegment.slice(0, lastSegment.indexOf("#")); + const term = expectedAnnotations.find( + (i) => termName === `@${i.fullyQualifiedName}` + ); + if (!term) { + // missing annotation term + return [ + { + kind: "AnnotationPathRequired", + issueType: ANNOTATION_ISSUE_TYPE, + message: `Term is either missing or wrong ${value}`, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }, + ]; + } + } + + return []; +} diff --git a/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts b/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts index 58d6ac889..11350b644 100644 --- a/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts +++ b/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts @@ -30,6 +30,10 @@ export function validateMissingViewEntitySet( ) { return []; } + const isAbsolutePath = actualAttributeValue.startsWith("/"); + if (isAbsolutePath) { + return []; + } const element = attribute.parent; const contextPathAttr = getElementAttributeValue(element, "contextPath"); const contextPath = getContextPath(contextPathAttr, context); diff --git a/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts b/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts index 93f56bf79..9640fe2c2 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts @@ -19,6 +19,7 @@ import { normalizePath, t, getContextPath, + TypeNameMap, } from "../../../utils"; import { getAnnotationAppliedOnElement } from "../../../utils"; @@ -65,6 +66,8 @@ export function validateUnknownAnnotationPath( context.manifestDetails.customViews[context.customViewId || ""] ?.entitySet ?? ""; + const isAbsolutePath = actualAttributeValue.startsWith("/"); + let isNavSegmentsAllowed = true; let base: ResolvedPathTargetType | undefined; let baseType: EntityType | undefined; @@ -81,7 +84,7 @@ export function validateUnknownAnnotationPath( normalizedContextPath )); isNavSegmentsAllowed = typeof contextPathAttr === "undefined"; - } else { + } else if (!isAbsolutePath) { if (!entitySet) { return []; } @@ -91,7 +94,7 @@ export function validateUnknownAnnotationPath( (e) => e.name === entitySet ); baseType = base?.entityType; - if (entitySet && !base) { + if (entitySet && !base && !isAbsolutePath) { return [ { kind: "InvalidAnnotationTarget", @@ -106,12 +109,12 @@ export function validateUnknownAnnotationPath( ]; } } - const { expectedAnnotations } = getPathConstraintsForControl( + const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( control, ui5Property ); - if (!base || base._type === "Property") { + if (!isAbsolutePath && (!base || base._type === "Property")) { return []; } @@ -158,21 +161,22 @@ export function validateUnknownAnnotationPath( segment.includes("@") ); segments.splice(termSegmentIndex); - if (segments.length > 1 && !segments[0]) { - // absolute path not allowed - return [ - { - kind: "InvalidAnnotationTerm", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("ABSOLUTE_ANNOTATION_PATH_NOT_ALLOWED"), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - } as AnnotationIssue, - ]; - } else if (segments.length > 0 && !isNavSegmentsAllowed) { + // if (segments.length > 1 && !segments[0]) { + // // absolute path not allowed + // return [ + // { + // kind: "InvalidAnnotationTerm", + // issueType: ANNOTATION_ISSUE_TYPE, + // message: t("ABSOLUTE_ANNOTATION_PATH_NOT_ALLOWED"), + // offsetRange: { + // start: actualAttributeValueToken.startOffset, + // end: actualAttributeValueToken.endOffset, + // }, + // severity: "warn", + // } as AnnotationIssue, + // ]; + // } else + if (segments.length > 0 && !isNavSegmentsAllowed) { return [ { kind: "InvalidAnnotationTerm", @@ -188,22 +192,31 @@ export function validateUnknownAnnotationPath( } as AnnotationIssue, ]; } - - let targetEntity: EntityType | undefined = baseType; + let targetEntity: ResolvedPathTargetType | undefined = baseType; let lastValidSegmentIndex = -1; - for (const segment of segments) { - if (!targetEntity) { - break; - } - const navProperty = targetEntity.navigationProperties.find( - (p) => p.name === segment + if (isAbsolutePath) { + const resolvedPathTarget = resolvePathTarget( + service.convertedMetadata, + segments.join("/"), + baseType ); - targetEntity = navProperty?.targetType; - if (targetEntity) { - lastValidSegmentIndex++; + targetEntity = resolvedPathTarget.target; + lastValidSegmentIndex = resolvedPathTarget.lastValidSegmentIndex; + } else { + for (const segment of segments) { + if (!targetEntity) { + break; + } + const navProperty = ( + targetEntity as EntityType + ).navigationProperties.find((p) => p.name === segment); + + targetEntity = navProperty?.targetType; + if (targetEntity) { + lastValidSegmentIndex++; + } } } - if (!targetEntity) { originalSegments.splice(lastValidSegmentIndex + 1); const correctPart = originalSegments.join("/"); @@ -222,13 +235,32 @@ export function validateUnknownAnnotationPath( severity: "warn", }, ]; + } else if (targetEntity._type === "Property") { + const expectedTypesList = expectedTypes + .map((item) => TypeNameMap[item]) + .join(", "); + return [ + { + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + actualType: "Edm.Property", + expectedTypes: expectedTypesList, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }, + ]; } else { const termSegment = originalSegments[termSegmentIndex]; const parts = termSegment.split("@"); let annotations: AnnotationBase[] | undefined; annotations = getAnnotationAppliedOnElement( expectedAnnotations, - segments.length === 0 ? base : targetEntity, + segments.length === 0 ? base! : targetEntity!, parts[0] ); diff --git a/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts b/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts index 0da5a8537..2a5ef5e6f 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts @@ -22,6 +22,7 @@ import { EntityType, Singleton, } from "@sap-ux/vocabularies-types"; +import { validateAbsolutePath } from "./absolute-path"; export function validateUnknownAnnotationTarget( attribute: XMLAttribute, @@ -110,169 +111,185 @@ export function validateUnknownAnnotationTarget( }); } - if (!actualAttributeValue.startsWith("/")) { - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("INVALID_CONTEXT_PATH_VALUE", { - value: actualAttributeValue, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } + // if (!actualAttributeValue.startsWith("/")) { + // return pushToResult({ + // kind: "UnknownEnumValue", + // issueType: ANNOTATION_ISSUE_TYPE, + // message: t("INVALID_CONTEXT_PATH_VALUE", { + // value: actualAttributeValue, + // }), + // offsetRange: { + // start: actualAttributeValueToken.startOffset, + // end: actualAttributeValueToken.endOffset, + // }, + // severity: "warn", + // }); + // } + // const segments = attribute.value.split('/'); + // const proceedingPath = segments.slice(0, segments.length - 1).join('/'); + // const normalizedValue = normalizePath(actualAttributeValue); + // const expectedTypesList = ( + // isNotRecommended ? expectedTypesMetaPath : expectedTypes + // ) + // .map((item) => TypeNameMap[item]) + // .join(", "); - const normalizedValue = normalizePath(actualAttributeValue); - const expectedTypesList = ( - isNotRecommended ? expectedTypesMetaPath : expectedTypes - ) - .map((item) => TypeNameMap[item]) - .join(", "); + // // Check by segments + // const { + // target, + // targetStructuredType: targetEntity, + // isCardinalityIssue, + // lastValidSegmentIndex, + // } = resolvePathTarget(service.convertedMetadata, normalizedValue); + // const originalSegments = actualAttributeValue.split("/"); - // Check by segments - const { - target, - targetStructuredType: targetEntity, - isCardinalityIssue, - lastValidSegmentIndex, - } = resolvePathTarget(service.convertedMetadata, normalizedValue); - const originalSegments = actualAttributeValue.split("/"); + // if (target?._type === "Property") { + // return pushToResult({ + // kind: "UnknownEnumValue", + // issueType: ANNOTATION_ISSUE_TYPE, + // message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + // actualType: "Edm.Property", + // expectedTypes: expectedTypesList, + // }), + // offsetRange: { + // start: actualAttributeValueToken.startOffset, + // end: actualAttributeValueToken.endOffset, + // }, + // severity: "warn", + // }); + // } - if (target?._type === "Property") { - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { - actualType: "Edm.Property", - expectedTypes: expectedTypesList, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } - - if (target?._type === "EntityContainer") { - const message = t( - isNotRecommended - ? "INCOMPLETE_CONTEXT_PATH_LEADS_TO_ENTITY_CONTAINER" - : "INCOMPLETE_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" - ); - return pushToResult({ - kind: "IncompletePath", - issueType: ANNOTATION_ISSUE_TYPE, - message, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } + // if (target?._type === "EntityContainer") { + // const message = t( + // isNotRecommended + // ? "INCOMPLETE_CONTEXT_PATH_LEADS_TO_ENTITY_CONTAINER" + // : "INCOMPLETE_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" + // ); + // return pushToResult({ + // kind: "IncompletePath", + // issueType: ANNOTATION_ISSUE_TYPE, + // message, + // offsetRange: { + // start: actualAttributeValueToken.startOffset, + // end: actualAttributeValueToken.endOffset, + // }, + // severity: "warn", + // }); + // } - if (!target || !targetEntity) { - if (!isCardinalityIssue) { - // Path does not exist - originalSegments.splice(lastValidSegmentIndex + 1); - const correctPart = originalSegments.length - ? "/" + originalSegments.join("/") - : ""; - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("UNKNOWN_CONTEXT_PATH", { value: actualAttributeValue }), - offsetRange: { - start: - actualAttributeValueToken.startOffset + correctPart.length + 1, - end: actualAttributeValueToken.endOffset - 1, - }, - severity: "warn", - }); - } else { - // segment found but preceding path leads to collection - originalSegments.splice(lastValidSegmentIndex + 1); - const correctPart = originalSegments.join("/"); - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("INVALID_CONTEXT_PATH_MULTIPLE_1_TO_MANY"), - offsetRange: { - start: - actualAttributeValueToken.startOffset + correctPart.length + 1, - end: actualAttributeValueToken.endOffset - 1, - }, - severity: "warn", - }); - } - } else { - if ( - (!isNotRecommended && !expectedTypes.includes(target._type)) || - (isNotRecommended && !expectedTypesMetaPath.includes(target._type)) - ) { - return pushToResult({ - kind: "InvalidAnnotationTarget", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { - actualType: TypeNameMap[target._type], - expectedTypes: expectedTypesList, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } + // if (!target || !targetEntity) { + // if (!isCardinalityIssue) { + // // Path does not exist + // originalSegments.splice(lastValidSegmentIndex + 1); + // const correctPart = originalSegments.length + // ? "/" + originalSegments.join("/") + // : ""; + // return pushToResult({ + // kind: "UnknownEnumValue", + // issueType: ANNOTATION_ISSUE_TYPE, + // message: t("UNKNOWN_CONTEXT_PATH", { value: actualAttributeValue }), + // offsetRange: { + // start: + // actualAttributeValueToken.startOffset + correctPart.length + 1, + // end: actualAttributeValueToken.endOffset - 1, + // }, + // severity: "warn", + // }); + // } else { + // // segment found but preceding path leads to collection + // originalSegments.splice(lastValidSegmentIndex + 1); + // const correctPart = originalSegments.join("/"); + // return pushToResult({ + // kind: "UnknownEnumValue", + // issueType: ANNOTATION_ISSUE_TYPE, + // message: t("INVALID_CONTEXT_PATH_MULTIPLE_1_TO_MANY"), + // offsetRange: { + // start: + // actualAttributeValueToken.startOffset + correctPart.length + 1, + // end: actualAttributeValueToken.endOffset - 1, + // }, + // severity: "warn", + // }); + // } + // } else { + // if ( + // (!isNotRecommended && !expectedTypes.includes(target._type)) || + // (isNotRecommended && !expectedTypesMetaPath.includes(target._type)) + // ) { + // return pushToResult({ + // kind: "InvalidAnnotationTarget", + // issueType: ANNOTATION_ISSUE_TYPE, + // message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + // actualType: TypeNameMap[target._type], + // expectedTypes: expectedTypesList, + // }), + // offsetRange: { + // start: actualAttributeValueToken.startOffset, + // end: actualAttributeValueToken.endOffset, + // }, + // severity: "warn", + // }); + // } - if (isPropertyPathAllowed(control)) { - return result; - } + // if (isPropertyPathAllowed(control)) { + // return result; + // } - let annotationList = getAnnotationAppliedOnElement( - expectedAnnotations, - target as EntityContainer | EntityType | EntitySet | Singleton + const { resolvedTarget: target, issues: absolutePathIssues } = + validateAbsolutePath( + attribute, + attribute.value, + expectedTypes, + expectedTypesMetaPath, + isPropertyPathAllowed(control), + service, + isNotRecommended ); - if (annotationList.length > 0) { - // path is correct - return result; - } + if (absolutePathIssues.length > 0) { + return [...result, ...absolutePathIssues]; + } - annotationList = getAnnotationAppliedOnElement( - expectedAnnotationsMetaPath, - target as EntityContainer | EntityType | EntitySet | Singleton - ); + let annotationList = getAnnotationAppliedOnElement( + expectedAnnotations, + target as EntityContainer | EntityType | EntitySet | Singleton + ); - if (annotationList.length > 0) { - // path is correct - return result; - } + if (annotationList.length > 0) { + // path is correct + return result; + } - const message = t( - expectedAnnotations.length === 0 - ? "CONTEXT_PATH_DOES_NOT_LEAD_TO_ANNOTATIONS" - : "INVALID_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" - ); - // Path itself is found but it doesn't suit current context - const issue: AnnotationIssue = { - kind: "InvalidAnnotationTarget", - issueType: ANNOTATION_ISSUE_TYPE, - message, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }; + annotationList = getAnnotationAppliedOnElement( + expectedAnnotationsMetaPath, + target as EntityContainer | EntityType | EntitySet | Singleton + ); - // TODO: required and actual cardinality mismatch check - return pushToResult(issue); + if (annotationList.length > 0) { + // path is correct + return result; } + + const message = t( + expectedAnnotations.length === 0 + ? "CONTEXT_PATH_DOES_NOT_LEAD_TO_ANNOTATIONS" + : "INVALID_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" + ); + // Path itself is found but it doesn't suit current context + const issue: AnnotationIssue = { + kind: "InvalidAnnotationTarget", + issueType: ANNOTATION_ISSUE_TYPE, + message, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }; + + // TODO: required and actual cardinality mismatch check + return pushToResult(issue); } + // } return []; } diff --git a/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts b/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts index ee12a8471..3e1462a1c 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts @@ -49,7 +49,7 @@ export function validateUnknownPropertyPath( ) { const element = attribute.parent; const control = element.name; - + const isAbsolutePath = actualAttributeValue.startsWith("/"); const mainServicePath = context.manifestDetails.mainServicePath; const service = mainServicePath ? context.services[mainServicePath] diff --git a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts index d866790c9..345d2def0 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts @@ -235,14 +235,14 @@ describe("metaPath attribute value validation (annotation path)", () => { ]); }); - it("is absolute path", async function () { - const result = await validateView( - `` - ); - expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ - "kind: InvalidAnnotationTerm; text: Absolute annotation paths not allowed in metaPath. Use contextPath attribute to change path context; severity:warn; offset:344-405", - ]); - }); + // it("is absolute path", async function () { + // const result = await validateView( + // `` + // ); + // expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ + // "kind: InvalidAnnotationTerm; text: Absolute annotation paths not allowed in metaPath. Use contextPath attribute to change path context; severity:warn; offset:344-405", + // ]); + // }); it("is incomplete", async function () { const result = await validateView( @@ -326,3 +326,6 @@ describe("metaPath attribute value validation (annotation path)", () => { }); }); }); + +// todo test EntitySet or contextPath for the current view are not defined in application manifest. +// when entity set or context path is undefined in mainfest.json file and metapath starts with absolute path - no diagnostics diff --git a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts index b624b8310..53156d493 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts @@ -157,6 +157,7 @@ describe("contextPath attribute value validation", () => { ); expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ "kind: ContextPathBindingNotRecommended; text: Context path for Field is usually defined if binding for the object is different than that of the page; severity:info; offset:347-356", + "kind: InvalidAnnotationTarget; text: Invalid contextPath value. It does not lead to any annotations of the expected type; severity:warn; offset:347-356", ]); }); }); From 6faa936f0276f3c9687d7c8f1294aaa9831f1e1c Mon Sep 17 00:00:00 2001 From: Maruf Rasully Date: Fri, 26 Apr 2024 06:50:47 +0200 Subject: [PATCH 3/7] fix: add diagnostic support for absolute path --- .../diagnostics/validators/absolute-path.ts | 188 ----------- .../diagnostics/validators/meta-path.ts | 202 ----------- .../validators/unknown-annotation-path.ts | 159 +++++---- .../validators/unknown-annotation-target.ts | 317 +++++++++--------- .../validators/unknown-property-path.ts | 1 - .../validators/missing-entity-set.test.ts | 7 + .../unknown-annotation-path.test.ts | 79 ++++- .../unknown-annotation-target.test.ts | 1 - 8 files changed, 311 insertions(+), 643 deletions(-) delete mode 100644 packages/fe/src/services/diagnostics/validators/absolute-path.ts delete mode 100644 packages/fe/src/services/diagnostics/validators/meta-path.ts diff --git a/packages/fe/src/services/diagnostics/validators/absolute-path.ts b/packages/fe/src/services/diagnostics/validators/absolute-path.ts deleted file mode 100644 index 3f36722ae..000000000 --- a/packages/fe/src/services/diagnostics/validators/absolute-path.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { XMLAttribute } from "@xml-tools/ast"; -import { Context } from "@ui5-language-assistant/context"; -import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; -import { isPossibleBindingAttributeValue } from "@ui5-language-assistant/xml-views-validation"; -import { - AnnotationIssue, - ANNOTATION_ISSUE_TYPE, - SAP_FE_MACROS, -} from "../../../types"; -import { - getPathConstraintsForControl, - isPropertyPathAllowed, - resolvePathTarget, - getAnnotationAppliedOnElement, - normalizePath, - TypeNameMap, - t, - AnnotationTerm, - AllowedTargetType, - ResolvedPathTargetType, -} from "../../../utils"; -import { - EntityContainer, - EntitySet, - EntityType, - Singleton, -} from "@sap-ux/vocabularies-types"; -import { ServiceDetails } from "@ui5-language-assistant/context/src/types"; - -interface ValidateAbsolutePathResult { - issues: AnnotationIssue[]; - resolvedTarget: ResolvedPathTargetType | undefined; -} -export function validateAbsolutePath( - attribute: XMLAttribute, - absolutePath: string, - expectedTypes: AllowedTargetType[], - expectedTypesMetaPath: AllowedTargetType[], - isPropertyPath: boolean, - service: ServiceDetails, - isNotRecommended = false -): ValidateAbsolutePathResult { - // always must be defined - // const actualAttributeValue = attribute.value!; - // always must be defined - const actualAttributeValueToken = attribute.syntax.value!; - - const result: ValidateAbsolutePathResult = { - issues: [], - resolvedTarget: undefined, - }; - const pushToResult = (item: AnnotationIssue) => { - result.issues.push(item); - return result; - }; - - if (!absolutePath.startsWith("/")) { - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("INVALID_CONTEXT_PATH_VALUE", { - value: absolutePath, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } - - const normalizedValue = normalizePath(absolutePath); - const expectedTypesList = ( - isNotRecommended ? expectedTypesMetaPath : expectedTypes - ) - .map((item) => TypeNameMap[item]) - .join(", "); - - // Check by segments - const { - target, - targetStructuredType: targetEntity, - isCardinalityIssue, - lastValidSegmentIndex, - } = resolvePathTarget(service.convertedMetadata, normalizedValue); - const originalSegments = absolutePath.split("/"); - - result.resolvedTarget = target; - - if (target?._type === "Property") { - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { - actualType: "Edm.Property", - expectedTypes: expectedTypesList, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } - - if (target?._type === "EntityContainer") { - const message = t( - isNotRecommended - ? "INCOMPLETE_CONTEXT_PATH_LEADS_TO_ENTITY_CONTAINER" - : "INCOMPLETE_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" - ); - return pushToResult({ - kind: "IncompletePath", - issueType: ANNOTATION_ISSUE_TYPE, - message, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } - - if (!target || !targetEntity) { - if (!isCardinalityIssue) { - // Path does not exist - originalSegments.splice(lastValidSegmentIndex + 1); - const correctPart = originalSegments.length - ? "/" + originalSegments.join("/") - : ""; - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("UNKNOWN_CONTEXT_PATH", { value: absolutePath }), // todo -> check value and affected range - offsetRange: { - start: actualAttributeValueToken.startOffset + correctPart.length + 1, - end: actualAttributeValueToken.endOffset - 1, - }, - severity: "warn", - }); - } else { - // segment found but preceding path leads to collection - originalSegments.splice(lastValidSegmentIndex + 1); - const correctPart = originalSegments.join("/"); - return pushToResult({ - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("INVALID_CONTEXT_PATH_MULTIPLE_1_TO_MANY"), - offsetRange: { - start: actualAttributeValueToken.startOffset + correctPart.length + 1, - end: actualAttributeValueToken.endOffset - 1, - }, - severity: "warn", - }); - } - } else { - if ( - (!isNotRecommended && !expectedTypes.includes(target._type)) || - (isNotRecommended && !expectedTypesMetaPath.includes(target._type)) - ) { - return pushToResult({ - kind: "InvalidAnnotationTarget", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { - actualType: TypeNameMap[target._type], - expectedTypes: expectedTypesList, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }); - } - - if (isPropertyPath) { - return { - issues: [], - resolvedTarget: target, - }; - } - - // TODO: required and actual cardinality mismatch check - // return pushToResult(issue); - // } - } - return result; -} diff --git a/packages/fe/src/services/diagnostics/validators/meta-path.ts b/packages/fe/src/services/diagnostics/validators/meta-path.ts deleted file mode 100644 index 5bb8c7073..000000000 --- a/packages/fe/src/services/diagnostics/validators/meta-path.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { XMLAttribute } from "@xml-tools/ast"; -import { Context } from "@ui5-language-assistant/context"; -import { getUI5PropertyByXMLAttributeKey } from "@ui5-language-assistant/logic-utils"; -import { isPossibleBindingAttributeValue } from "@ui5-language-assistant/xml-views-validation"; -import { - AnnotationIssue, - ANNOTATION_ISSUE_TYPE, - SAP_FE_MACROS, - ContextPathOrigin, -} from "../../../types"; -import { - getContextPath, - getElementAttributeValue, - getPathConstraintsForControl, - isPropertyPathAllowed, - normalizePath, - resolveContextPath, - resolvePathTarget, - t, - TypeNameMap, -} from "../../../utils"; - -import { - EntityContainer, - EntitySet, - EntityType, - Singleton, - Property, -} from "@sap-ux/vocabularies-types"; - -/** - * absolute path - * - * non-absolute - * - * UnknownEnumValueIssue - any invalid path segment or property does not exits in path - * UnknownAnnotationPathIssue - not used - * AnnotationTargetRequiredIssue - target is mandatory - * AnnotationPathRequiredIssue - probabally when metpath is empty or in complete - * PathDoesNotExistIssue - any invalid segment in path - * - */ -export function validateMetaPath( - attribute: XMLAttribute, - context: Context -): AnnotationIssue[] { - let isNavSegmentsAllowed = true; - let base: - | EntityContainer - | EntitySet - | EntityType - | Singleton - | Property - | undefined; - let baseType: EntityType | undefined; - let normalizedContextPath: string; - - const actualAttributeValue = attribute.value; - const actualAttributeValueToken = attribute.syntax.value; - if ( - actualAttributeValue === null || - actualAttributeValueToken === undefined || - isPossibleBindingAttributeValue(actualAttributeValue) - ) { - return []; - } - - const ui5Property = getUI5PropertyByXMLAttributeKey( - attribute, - context.ui5Model - ); - - if ( - ui5Property?.library !== SAP_FE_MACROS || - ui5Property.name !== "metaPath" - ) { - return []; - } - - const mainServicePath = context.manifestDetails.mainServicePath; - const service = mainServicePath - ? context.services[mainServicePath] - : undefined; - if (!service) { - return []; - } - const element = attribute.parent; - let value = attribute.value; - const control = element.name; - if (!value) { - // todo - return []; - } - const isAbsolutePath = value.startsWith("/"); - - if (!isAbsolutePath) { - return []; - } - - let proceedingPath = value; - let lastSegment = ""; - - if (value.includes("@")) { - // it must be annotation path - const parts = value.split("/"); - lastSegment = parts.pop() ?? ""; - proceedingPath = parts.join("/"); - } - - const resolvedContext = resolveContextPath( - context, - element, - isAbsolutePath, - proceedingPath - ); - - if (!resolvedContext) { - return []; - } - // /travel/to_nave/ - const { contextPath, origin } = resolvedContext; - - const { expectedAnnotations, expectedTypes } = getPathConstraintsForControl( - control, - ui5Property - ); - const isPropertiesAllowed = expectedTypes.includes("Property"); - const metadata = service.convertedMetadata; - let isCollection: boolean | undefined; - let lastValidSegmentIndex: number; - let isCardinalityIssue: boolean; - let milestones: string[]; - normalizedContextPath = normalizePath(contextPath); - ({ - target: base, - targetStructuredType: baseType, - isCardinalityIssue, - isCollection, - lastValidSegmentIndex, - milestones, - } = resolvePathTarget(metadata, normalizedContextPath)); - - if (!base) { - // not resolved. issue can be either in contextPath in manifest.json, or entity set. contePath in xml may be handled somewhere else. - // todo - if (isPropertiesAllowed) { - return [ - { - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: `Wrong path ${value}`, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }, - ]; - } - return []; - } - - if (isPropertiesAllowed && base._type !== "Property") { - return [ - { - kind: "UnknownEnumValue", - issueType: ANNOTATION_ISSUE_TYPE, - message: `Path must ends with property path ${value}`, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }, - ]; - } - - if (!isPropertiesAllowed && expectedAnnotations.length > 0) { - // check last segment - const termName = lastSegment.slice(0, lastSegment.indexOf("#")); - const term = expectedAnnotations.find( - (i) => termName === `@${i.fullyQualifiedName}` - ); - if (!term) { - // missing annotation term - return [ - { - kind: "AnnotationPathRequired", - issueType: ANNOTATION_ISSUE_TYPE, - message: `Term is either missing or wrong ${value}`, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }, - ]; - } - } - - return []; -} diff --git a/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts b/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts index 9640fe2c2..c9688f49e 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts @@ -23,7 +23,24 @@ import { } from "../../../utils"; import { getAnnotationAppliedOnElement } from "../../../utils"; -import { EntityType } from "@sap-ux/vocabularies-types"; +import { EntityType, Property } from "@sap-ux/vocabularies-types"; + +const getMessageValue = ( + isAbsolutePath: boolean, + value: string | null, + normalizedContext: string | undefined +): string => { + if (!value) { + return ""; + } + if (isAbsolutePath) { + return value; + } + if (normalizedContext) { + return `${normalizedContext}/${value}`; + } + return value; +}; export function validateUnknownAnnotationPath( attribute: XMLAttribute, @@ -71,7 +88,7 @@ export function validateUnknownAnnotationPath( let isNavSegmentsAllowed = true; let base: ResolvedPathTargetType | undefined; let baseType: EntityType | undefined; - let normalizedContextPath: string; + let normalizedContextPath: string | undefined; // resolve context and get annotations for it if (typeof contextPath === "string") { @@ -94,7 +111,7 @@ export function validateUnknownAnnotationPath( (e) => e.name === entitySet ); baseType = base?.entityType; - if (entitySet && !base && !isAbsolutePath) { + if (entitySet && !base) { return [ { kind: "InvalidAnnotationTarget", @@ -161,21 +178,6 @@ export function validateUnknownAnnotationPath( segment.includes("@") ); segments.splice(termSegmentIndex); - // if (segments.length > 1 && !segments[0]) { - // // absolute path not allowed - // return [ - // { - // kind: "InvalidAnnotationTerm", - // issueType: ANNOTATION_ISSUE_TYPE, - // message: t("ABSOLUTE_ANNOTATION_PATH_NOT_ALLOWED"), - // offsetRange: { - // start: actualAttributeValueToken.startOffset, - // end: actualAttributeValueToken.endOffset, - // }, - // severity: "warn", - // } as AnnotationIssue, - // ]; - // } else if (segments.length > 0 && !isNavSegmentsAllowed) { return [ { @@ -225,7 +227,11 @@ export function validateUnknownAnnotationPath( kind: "PathDoesNotExist", issueType: ANNOTATION_ISSUE_TYPE, message: t("UNKNOWN_ANNOTATION_PATH", { - value: `${normalizedContextPath}/${attribute.value}`, + value: getMessageValue( + isAbsolutePath, + attribute.value, + normalizedContextPath + ), }), offsetRange: { start: @@ -235,7 +241,8 @@ export function validateUnknownAnnotationPath( severity: "warn", }, ]; - } else if (targetEntity._type === "Property") { + } + if (targetEntity._type === "Property") { const expectedTypesList = expectedTypes .map((item) => TypeNameMap[item]) .join(", "); @@ -254,74 +261,80 @@ export function validateUnknownAnnotationPath( severity: "warn", }, ]; + } + + base = base as Exclude; + const termSegment = originalSegments[termSegmentIndex]; + const parts = termSegment.split("@"); + let annotations: AnnotationBase[] | undefined; + + annotations = getAnnotationAppliedOnElement( + expectedAnnotations, + segments.length === 0 ? base : targetEntity, + parts[0] + ); + + const match = annotations.find( + (anno) => composeAnnotationPath(anno) === "@" + parts[1] + ); + if (match) { + return []; } else { - const termSegment = originalSegments[termSegmentIndex]; - const parts = termSegment.split("@"); - let annotations: AnnotationBase[] | undefined; + // check whether the provided term exists on target + const term: AnnotationTerm = fullyQualifiedNameToTerm(parts[1]); annotations = getAnnotationAppliedOnElement( - expectedAnnotations, - segments.length === 0 ? base! : targetEntity!, + [term], + segments.length === 0 ? base : targetEntity, parts[0] ); - const match = annotations.find( (anno) => composeAnnotationPath(anno) === "@" + parts[1] ); if (match) { - return []; - } else { - // check whether the provided term exists on target - const term: AnnotationTerm = fullyQualifiedNameToTerm(parts[1]); + // determine whether any allowed term exists in the project suitable for the current context annotations = getAnnotationAppliedOnElement( - [term], - segments.length === 0 ? base : targetEntity, - parts[0] - ); - const match = annotations.find( - (anno) => composeAnnotationPath(anno) === "@" + parts[1] + expectedAnnotations, + segments.length === 0 ? base : targetEntity ); - if (match) { - // determine whether any allowed term exists in the project suitable for the current context - annotations = getAnnotationAppliedOnElement( - expectedAnnotations, - base - ); - return [ - { - kind: "InvalidAnnotationTerm", - issueType: ANNOTATION_ISSUE_TYPE, - message: t( - annotations.length - ? "INVALID_ANNOTATION_TERM_TRIGGER_CODE_COMPLETION" - : "INVALID_ANNOTATION_TERM_THERE_ARE_NO_SUITABLE_ANNOTATIONS", - { value: attribute.value } - ), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", + return [ + { + kind: "InvalidAnnotationTerm", + issueType: ANNOTATION_ISSUE_TYPE, + message: t( + annotations.length + ? "INVALID_ANNOTATION_TERM_TRIGGER_CODE_COMPLETION" + : "INVALID_ANNOTATION_TERM_THERE_ARE_NO_SUITABLE_ANNOTATIONS", + { value: attribute.value } + ), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, }, - ]; - } + severity: "warn", + }, + ]; } + } - return [ - { - kind: "PathDoesNotExist", - issueType: ANNOTATION_ISSUE_TYPE, - message: t("UNKNOWN_ANNOTATION_PATH", { - value: `${normalizedContextPath}/${attribute.value}`, - }), - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", + return [ + { + kind: "PathDoesNotExist", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("UNKNOWN_ANNOTATION_PATH", { + value: getMessageValue( + isAbsolutePath, + attribute.value, + normalizedContextPath + ), + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, }, - ]; - } + severity: "warn", + }, + ]; } return []; diff --git a/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts b/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts index 2a5ef5e6f..0da5a8537 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-annotation-target.ts @@ -22,7 +22,6 @@ import { EntityType, Singleton, } from "@sap-ux/vocabularies-types"; -import { validateAbsolutePath } from "./absolute-path"; export function validateUnknownAnnotationTarget( attribute: XMLAttribute, @@ -111,185 +110,169 @@ export function validateUnknownAnnotationTarget( }); } - // if (!actualAttributeValue.startsWith("/")) { - // return pushToResult({ - // kind: "UnknownEnumValue", - // issueType: ANNOTATION_ISSUE_TYPE, - // message: t("INVALID_CONTEXT_PATH_VALUE", { - // value: actualAttributeValue, - // }), - // offsetRange: { - // start: actualAttributeValueToken.startOffset, - // end: actualAttributeValueToken.endOffset, - // }, - // severity: "warn", - // }); - // } - // const segments = attribute.value.split('/'); - // const proceedingPath = segments.slice(0, segments.length - 1).join('/'); - // const normalizedValue = normalizePath(actualAttributeValue); - // const expectedTypesList = ( - // isNotRecommended ? expectedTypesMetaPath : expectedTypes - // ) - // .map((item) => TypeNameMap[item]) - // .join(", "); - - // // Check by segments - // const { - // target, - // targetStructuredType: targetEntity, - // isCardinalityIssue, - // lastValidSegmentIndex, - // } = resolvePathTarget(service.convertedMetadata, normalizedValue); - // const originalSegments = actualAttributeValue.split("/"); - - // if (target?._type === "Property") { - // return pushToResult({ - // kind: "UnknownEnumValue", - // issueType: ANNOTATION_ISSUE_TYPE, - // message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { - // actualType: "Edm.Property", - // expectedTypes: expectedTypesList, - // }), - // offsetRange: { - // start: actualAttributeValueToken.startOffset, - // end: actualAttributeValueToken.endOffset, - // }, - // severity: "warn", - // }); - // } + if (!actualAttributeValue.startsWith("/")) { + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("INVALID_CONTEXT_PATH_VALUE", { + value: actualAttributeValue, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } - // if (target?._type === "EntityContainer") { - // const message = t( - // isNotRecommended - // ? "INCOMPLETE_CONTEXT_PATH_LEADS_TO_ENTITY_CONTAINER" - // : "INCOMPLETE_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" - // ); - // return pushToResult({ - // kind: "IncompletePath", - // issueType: ANNOTATION_ISSUE_TYPE, - // message, - // offsetRange: { - // start: actualAttributeValueToken.startOffset, - // end: actualAttributeValueToken.endOffset, - // }, - // severity: "warn", - // }); - // } + const normalizedValue = normalizePath(actualAttributeValue); + const expectedTypesList = ( + isNotRecommended ? expectedTypesMetaPath : expectedTypes + ) + .map((item) => TypeNameMap[item]) + .join(", "); - // if (!target || !targetEntity) { - // if (!isCardinalityIssue) { - // // Path does not exist - // originalSegments.splice(lastValidSegmentIndex + 1); - // const correctPart = originalSegments.length - // ? "/" + originalSegments.join("/") - // : ""; - // return pushToResult({ - // kind: "UnknownEnumValue", - // issueType: ANNOTATION_ISSUE_TYPE, - // message: t("UNKNOWN_CONTEXT_PATH", { value: actualAttributeValue }), - // offsetRange: { - // start: - // actualAttributeValueToken.startOffset + correctPart.length + 1, - // end: actualAttributeValueToken.endOffset - 1, - // }, - // severity: "warn", - // }); - // } else { - // // segment found but preceding path leads to collection - // originalSegments.splice(lastValidSegmentIndex + 1); - // const correctPart = originalSegments.join("/"); - // return pushToResult({ - // kind: "UnknownEnumValue", - // issueType: ANNOTATION_ISSUE_TYPE, - // message: t("INVALID_CONTEXT_PATH_MULTIPLE_1_TO_MANY"), - // offsetRange: { - // start: - // actualAttributeValueToken.startOffset + correctPart.length + 1, - // end: actualAttributeValueToken.endOffset - 1, - // }, - // severity: "warn", - // }); - // } - // } else { - // if ( - // (!isNotRecommended && !expectedTypes.includes(target._type)) || - // (isNotRecommended && !expectedTypesMetaPath.includes(target._type)) - // ) { - // return pushToResult({ - // kind: "InvalidAnnotationTarget", - // issueType: ANNOTATION_ISSUE_TYPE, - // message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { - // actualType: TypeNameMap[target._type], - // expectedTypes: expectedTypesList, - // }), - // offsetRange: { - // start: actualAttributeValueToken.startOffset, - // end: actualAttributeValueToken.endOffset, - // }, - // severity: "warn", - // }); - // } + // Check by segments + const { + target, + targetStructuredType: targetEntity, + isCardinalityIssue, + lastValidSegmentIndex, + } = resolvePathTarget(service.convertedMetadata, normalizedValue); + const originalSegments = actualAttributeValue.split("/"); - // if (isPropertyPathAllowed(control)) { - // return result; - // } + if (target?._type === "Property") { + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + actualType: "Edm.Property", + expectedTypes: expectedTypesList, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } - const { resolvedTarget: target, issues: absolutePathIssues } = - validateAbsolutePath( - attribute, - attribute.value, - expectedTypes, - expectedTypesMetaPath, - isPropertyPathAllowed(control), - service, + if (target?._type === "EntityContainer") { + const message = t( isNotRecommended + ? "INCOMPLETE_CONTEXT_PATH_LEADS_TO_ENTITY_CONTAINER" + : "INCOMPLETE_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" ); - - if (absolutePathIssues.length > 0) { - return [...result, ...absolutePathIssues]; + return pushToResult({ + kind: "IncompletePath", + issueType: ANNOTATION_ISSUE_TYPE, + message, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); } - let annotationList = getAnnotationAppliedOnElement( - expectedAnnotations, - target as EntityContainer | EntityType | EntitySet | Singleton - ); + if (!target || !targetEntity) { + if (!isCardinalityIssue) { + // Path does not exist + originalSegments.splice(lastValidSegmentIndex + 1); + const correctPart = originalSegments.length + ? "/" + originalSegments.join("/") + : ""; + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("UNKNOWN_CONTEXT_PATH", { value: actualAttributeValue }), + offsetRange: { + start: + actualAttributeValueToken.startOffset + correctPart.length + 1, + end: actualAttributeValueToken.endOffset - 1, + }, + severity: "warn", + }); + } else { + // segment found but preceding path leads to collection + originalSegments.splice(lastValidSegmentIndex + 1); + const correctPart = originalSegments.join("/"); + return pushToResult({ + kind: "UnknownEnumValue", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("INVALID_CONTEXT_PATH_MULTIPLE_1_TO_MANY"), + offsetRange: { + start: + actualAttributeValueToken.startOffset + correctPart.length + 1, + end: actualAttributeValueToken.endOffset - 1, + }, + severity: "warn", + }); + } + } else { + if ( + (!isNotRecommended && !expectedTypes.includes(target._type)) || + (isNotRecommended && !expectedTypesMetaPath.includes(target._type)) + ) { + return pushToResult({ + kind: "InvalidAnnotationTarget", + issueType: ANNOTATION_ISSUE_TYPE, + message: t("CONTEXT_PATH_LEADS_TO_WRONG_TARGET", { + actualType: TypeNameMap[target._type], + expectedTypes: expectedTypesList, + }), + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }); + } - if (annotationList.length > 0) { - // path is correct - return result; - } + if (isPropertyPathAllowed(control)) { + return result; + } - annotationList = getAnnotationAppliedOnElement( - expectedAnnotationsMetaPath, - target as EntityContainer | EntityType | EntitySet | Singleton - ); + let annotationList = getAnnotationAppliedOnElement( + expectedAnnotations, + target as EntityContainer | EntityType | EntitySet | Singleton + ); - if (annotationList.length > 0) { - // path is correct - return result; - } + if (annotationList.length > 0) { + // path is correct + return result; + } - const message = t( - expectedAnnotations.length === 0 - ? "CONTEXT_PATH_DOES_NOT_LEAD_TO_ANNOTATIONS" - : "INVALID_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" - ); - // Path itself is found but it doesn't suit current context - const issue: AnnotationIssue = { - kind: "InvalidAnnotationTarget", - issueType: ANNOTATION_ISSUE_TYPE, - message, - offsetRange: { - start: actualAttributeValueToken.startOffset, - end: actualAttributeValueToken.endOffset, - }, - severity: "warn", - }; + annotationList = getAnnotationAppliedOnElement( + expectedAnnotationsMetaPath, + target as EntityContainer | EntityType | EntitySet | Singleton + ); + + if (annotationList.length > 0) { + // path is correct + return result; + } + + const message = t( + expectedAnnotations.length === 0 + ? "CONTEXT_PATH_DOES_NOT_LEAD_TO_ANNOTATIONS" + : "INVALID_CONTEXT_PATH_TRIGGER_CODE_COMPLETION" + ); + // Path itself is found but it doesn't suit current context + const issue: AnnotationIssue = { + kind: "InvalidAnnotationTarget", + issueType: ANNOTATION_ISSUE_TYPE, + message, + offsetRange: { + start: actualAttributeValueToken.startOffset, + end: actualAttributeValueToken.endOffset, + }, + severity: "warn", + }; - // TODO: required and actual cardinality mismatch check - return pushToResult(issue); + // TODO: required and actual cardinality mismatch check + return pushToResult(issue); + } } - // } return []; } diff --git a/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts b/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts index 3e1462a1c..cc808ee70 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-property-path.ts @@ -49,7 +49,6 @@ export function validateUnknownPropertyPath( ) { const element = attribute.parent; const control = element.name; - const isAbsolutePath = actualAttributeValue.startsWith("/"); const mainServicePath = context.manifestDetails.mainServicePath; const service = mainServicePath ? context.services[mainServicePath] diff --git a/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts b/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts index bcfde4a31..e63587dce 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts @@ -123,6 +123,13 @@ describe("missing entitySet validation", () => { expect(result.length).toEqual(0); }); + it("attribute value is absolute", async function () { + const result = await validateView( + `` + ); + expect(result.length).toEqual(0); + }); + it("contextPath exists", async function () { const result = await validateView( `` diff --git a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts index 345d2def0..0172f1675 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts @@ -136,6 +136,20 @@ describe("metaPath attribute value validation (annotation path)", () => { expect(result.length).toEqual(0); }); + it("custom views are empty manifest - absolute path", async function () { + const result = await validateView( + ``, + (c) => { + const newContext = { + ...c, + manifestDetails: { ...c.manifestDetails, customViews: {} }, + }; + return newContext; + } + ); + expect(result.length).toEqual(0); + }); + it("custom view id not determined", async function () { const result = await validateView( ``, @@ -149,6 +163,19 @@ describe("metaPath attribute value validation (annotation path)", () => { ); expect(result.length).toEqual(0); }); + it("custom view id not determined - absolute path", async function () { + const result = await validateView( + `>`, + (c) => { + const newContext: Context = { + ...c, + customViewId: "", + }; + return newContext; + } + ); + expect(result.length).toEqual(0); + }); it("contextPath is not absolute", async function () { const result = await validateView( @@ -185,6 +212,12 @@ describe("metaPath attribute value validation (annotation path)", () => { ); expect(result.length).toEqual(0); }); + it("is absolute path", async function () { + const result = await validateView( + `` + ); + expect(result.length).toEqual(0); + }); }); describe("shows info message when...", () => { @@ -235,14 +268,14 @@ describe("metaPath attribute value validation (annotation path)", () => { ]); }); - // it("is absolute path", async function () { - // const result = await validateView( - // `` - // ); - // expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ - // "kind: InvalidAnnotationTerm; text: Absolute annotation paths not allowed in metaPath. Use contextPath attribute to change path context; severity:warn; offset:344-405", - // ]); - // }); + it("contains wrong segments - absolute path", async function () { + const result = await validateView( + `` + ); + expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ + 'kind: PathDoesNotExist; text: Unknown annotation path: "/Booking_01/to_Travel/@com.sap.vocabularies.UI.v1.Chart#sample1"; severity:warn; offset:345-407', + ]); + }); it("is incomplete", async function () { const result = await validateView( @@ -253,6 +286,15 @@ describe("metaPath attribute value validation (annotation path)", () => { ]); }); + it("is incomplete - absolute path", async function () { + const result = await validateView( + `` + ); + expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ + "kind: PropertyPathNotAllowed; text: Path value must end with annotation term. Use code completion to select annotation path; severity:warn; offset:344-363", + ]); + }); + it("is property path", async function () { const result = await validateView( `` @@ -262,6 +304,15 @@ describe("metaPath attribute value validation (annotation path)", () => { ]); }); + it("is property path - absolute path", async function () { + const result = await validateView( + `` + ); + expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ + "kind: PropertyPathNotAllowed; text: Path value must end with annotation term. Use code completion to select annotation path; severity:warn; offset:344-373", + ]); + }); + it("contains navigation segments when contextPath is specified", async function () { const result = await validateView( `` @@ -280,6 +331,15 @@ describe("metaPath attribute value validation (annotation path)", () => { ]); }); + it("is pointing to not existing term - absolute path", async function () { + const result = await validateView( + `` + ); + expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ + 'kind: PathDoesNotExist; text: Unknown annotation path: "/Booking/to_Travel/@com.sap.vocabularies.UI.v1.Chart#NotExisting"; severity:warn; offset:344-409', + ]); + }); + it("is pointing to not existing term (with contextPath)", async function () { const result = await validateView( `` @@ -326,6 +386,3 @@ describe("metaPath attribute value validation (annotation path)", () => { }); }); }); - -// todo test EntitySet or contextPath for the current view are not defined in application manifest. -// when entity set or context path is undefined in mainfest.json file and metapath starts with absolute path - no diagnostics diff --git a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts index 53156d493..b624b8310 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-target.test.ts @@ -157,7 +157,6 @@ describe("contextPath attribute value validation", () => { ); expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ "kind: ContextPathBindingNotRecommended; text: Context path for Field is usually defined if binding for the object is different than that of the page; severity:info; offset:347-356", - "kind: InvalidAnnotationTarget; text: Invalid contextPath value. It does not lead to any annotations of the expected type; severity:warn; offset:347-356", ]); }); }); From 77ab88059a777600179d5d44b7d54aea4f77099f Mon Sep 17 00:00:00 2001 From: Maruf Rasully Date: Fri, 26 Apr 2024 11:24:41 +0200 Subject: [PATCH 4/7] fix: adapt test case and adress sonar cloud issue --- .../completion/providers/meta-path.ts | 8 ++++---- .../validators/unknown-annotation-path.ts | 16 ++++++++-------- .../unknown-annotation-path.test.ts | 1 + .../annotation_metaPath_attribute_handling.md | 19 +++++++++---------- .../property_metaPath_attribute_handling.md | 11 ++++++++++- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/fe/src/services/completion/providers/meta-path.ts b/packages/fe/src/services/completion/providers/meta-path.ts index ebc27f36d..3e6c482fe 100644 --- a/packages/fe/src/services/completion/providers/meta-path.ts +++ b/packages/fe/src/services/completion/providers/meta-path.ts @@ -134,7 +134,7 @@ export function metaPathSuggestions({ ); baseType = base?.entityType; } - let isCollection; + let isCollection: boolean | undefined; // for (navigation) property segment or annotation term if (!isAbsolutePath && completionSegmentIndex > 0) { @@ -147,7 +147,7 @@ export function metaPathSuggestions({ ({ target: base, targetStructuredType: baseType, - isCollection: isCollection, + isCollection, } = resolvePathTarget(metadata, contextToConsider, baseType)); if (!base) { // target not resolved e.g. for wrong nav segment - no further segments possible @@ -159,7 +159,7 @@ export function metaPathSuggestions({ ({ target: base, targetStructuredType: baseType, - isCollection: isCollection, + isCollection, } = resolvePathTarget(metadata, normalizePath(contextPath))); } @@ -195,7 +195,7 @@ export function metaPathSuggestions({ // for first path segment completion, where current base can be entity set or singleton, // we collect also terms applied on their structural entity type - // targetStructuredType is never undefined in this context + // baseType is never undefined in this context annotationList.push( ...collectAnnotationsForElement(expectedAnnotations, baseType) ); diff --git a/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts b/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts index c9688f49e..0f78fdd5b 100644 --- a/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts +++ b/packages/fe/src/services/diagnostics/validators/unknown-annotation-path.ts @@ -263,14 +263,18 @@ export function validateUnknownAnnotationPath( ]; } - base = base as Exclude; + const baseEntity = + segments.length === 0 + ? (base as Exclude) + : targetEntity; + const termSegment = originalSegments[termSegmentIndex]; const parts = termSegment.split("@"); let annotations: AnnotationBase[] | undefined; annotations = getAnnotationAppliedOnElement( expectedAnnotations, - segments.length === 0 ? base : targetEntity, + baseEntity, parts[0] ); @@ -282,11 +286,7 @@ export function validateUnknownAnnotationPath( } else { // check whether the provided term exists on target const term: AnnotationTerm = fullyQualifiedNameToTerm(parts[1]); - annotations = getAnnotationAppliedOnElement( - [term], - segments.length === 0 ? base : targetEntity, - parts[0] - ); + annotations = getAnnotationAppliedOnElement([term], baseEntity, parts[0]); const match = annotations.find( (anno) => composeAnnotationPath(anno) === "@" + parts[1] ); @@ -294,7 +294,7 @@ export function validateUnknownAnnotationPath( // determine whether any allowed term exists in the project suitable for the current context annotations = getAnnotationAppliedOnElement( expectedAnnotations, - segments.length === 0 ? base : targetEntity + baseEntity ); return [ diff --git a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts index 0172f1675..18628de21 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/unknown-annotation-path.test.ts @@ -212,6 +212,7 @@ describe("metaPath attribute value validation (annotation path)", () => { ); expect(result.length).toEqual(0); }); + it("is absolute path", async function () { const result = await validateView( `` diff --git a/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md b/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md index 542ee597a..e67c393c9 100644 --- a/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md +++ b/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md @@ -55,18 +55,17 @@ annotate service.Travel with @( 2. Observe diagnostics warning: `Annotation path value cannot be empty`. 3. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample`. Observe warning message `Unknown annotation path: "/Travel/@com.sap.vocabularies.UI.v1.Chart#sample"`. 4. Set the metaPath attribute value as `to_Booking`. Observe warning message `Path value must end with annotation term. Use code completion to select annotation path`. -5. Set the metaPath attribute value as `/Travel/@com.sap.vocabularies.UI.v1.Chart#sample1`. Observe warning message `Absolute annotation paths not allowed in metaPath. Use contextPath attribute to change path context` -6. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.LineItem`. Observe warning message `Invalid annotation term: "@com.sap.vocabularies.UI.v1.LineItem". Trigger code completion to choose one of allowed annotations`. -7. Go to app manifest file, find `routing\targets\TravelMain` settings entry and rename `entitySet` property in the nested object structure to `entitySet_`. Save the file. -8. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample1`. Observe info message `EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers`. -9. Revert manifest change that is done at previous step 7. Change property `entitySet` value to `Travel_`. Save the file. -10. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample`. Observe info message `Entity Set "Travel_" specified in manifest for the current view is not found. Attribute value completion and diagnostics are disabled`. -11. Reset property `entitySet` value to `Travel` in app manifest. Save the file. -12. Replace current macros element with the snippet: +5. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.LineItem`. Observe warning message `Invalid annotation term: "@com.sap.vocabularies.UI.v1.LineItem". Trigger code completion to choose one of allowed annotations`. +6. Go to app manifest file, find `routing\targets\TravelMain` settings entry and rename `entitySet` property in the nested object structure to `entitySet_`. Save the file. +7. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample1`. Observe info message `EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers`. +8. Revert manifest change that is done at previous step 7. Change property `entitySet` value to `Travel_`. Save the file. +9. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample`. Observe info message `Entity Set "Travel_" specified in manifest for the current view is not found. Attribute value completion and diagnostics are disabled`. +10. Reset property `entitySet` value to `Travel` in app manifest. Save the file. +11. Replace current macros element with the snippet: ```XML ``` -13. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample`. Observe warning message `Unknown annotation path: "/TravelService.EntityContainer/Travel/@com.sap.vocabularies.UI.v1.Chart#sample`. -14. Set the metaPath attribute value as `to_Booking/@com.sap.vocabularies.UI.v1.Chart#sample`. Observe warning message `Navigation segments not allowed when contextPath is provided`. +13. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#notDefined`. Observe warning message `Unknown annotation path: "/TravelService.EntityContainer/Travel/@com.sap.vocabularies.UI.v1.Chart#notDefined`. +14. Set the metaPath attribute value as `to_Booking/@com.sap.vocabularies.UI.v1.Chart#sample1`. Observe warning message `Navigation segments not allowed when contextPath is provided`. diff --git a/packages/vscode-ui5-language-assistant/test/manual-tests/property_metaPath_attribute_handling.md b/packages/vscode-ui5-language-assistant/test/manual-tests/property_metaPath_attribute_handling.md index 3ba3d1ba2..8f9e98e79 100644 --- a/packages/vscode-ui5-language-assistant/test/manual-tests/property_metaPath_attribute_handling.md +++ b/packages/vscode-ui5-language-assistant/test/manual-tests/property_metaPath_attribute_handling.md @@ -28,7 +28,16 @@ Associated user stories: ``` 4. Place the cursor at the position of the `metaPath` attribute value and trigger code completion. -5. Observe the list of suggestions for the first path segment. Make sure that properties of the current default entity set `Travel` specified in manifest go first in the list and followed by possible navigation segments. Choose first property and press `Enter`. Observe no error messages are shown for the attribute value. +5. Observe the list of suggestions for the first path segment. Make sure that list is sort as: + + - properties of the current default entity set `Travel` specified in manifest + - navigation segments + - entity types with absolute path + - entity sets + - entity container + + Choose first property and press `Enter`. Observe no error messages are shown for the attribute value. + 6. Place cursor at value's first position and trigger code completion. Choose option `to_Booking` and press `/` to confirm. Observe the segment is added, and completion for next segment is triggered. Choose navigation property `to_Travel` and press `/` to confirm. Observe `Travel` properties are listed and further navigation segment `to_Booking` is not available to avoid cyclic routes. Choose first property and press `Enter`. Observe no error messages are shown for the attribute value. 7. Remove entire current element and place following snippet instead: From ec4fd9582bbff6328bcfa148de325949a07fa172 Mon Sep 17 00:00:00 2001 From: Maruf Rasully Date: Fri, 26 Apr 2024 11:28:04 +0200 Subject: [PATCH 5/7] fix: add change set --- .changeset/eight-pots-protect.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/eight-pots-protect.md diff --git a/.changeset/eight-pots-protect.md b/.changeset/eight-pots-protect.md new file mode 100644 index 000000000..7aa557d03 --- /dev/null +++ b/.changeset/eight-pots-protect.md @@ -0,0 +1,7 @@ +--- +"@ui5-language-assistant/vscode-ui5-language-assistant-bas-ext": patch +"vscode-ui5-language-assistant": patch +"@ui5-language-assistant/fe": patch +--- + +feat: add absolute path support for meta path From 40e9f028feb643031d1b73fa09b22d7f466f747a Mon Sep 17 00:00:00 2001 From: Maruf Rasully Date: Fri, 26 Apr 2024 14:24:23 +0200 Subject: [PATCH 6/7] fix: diagnostic message --- packages/fe/src/i18n/i18n.json | 2 +- .../services/diagnostics/validators/missing-entity-set.ts | 2 +- .../diagnostics/validators/missing-entity-set.test.ts | 6 +++--- .../manual-tests/annotation_metaPath_attribute_handling.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/fe/src/i18n/i18n.json b/packages/fe/src/i18n/i18n.json index 535df1450..af82d08e2 100644 --- a/packages/fe/src/i18n/i18n.json +++ b/packages/fe/src/i18n/i18n.json @@ -24,7 +24,7 @@ "UNKNOWN_PATH": "Unknown path: \"{{value}}\"", "INVALID_PROPERTY_PATH_MULTIPLE_1_TO_MANY": "Invalid property path value. Multiple 1:many association segments not allowed", - "ENTITY_SET_OR_CONTEXT_PATH_IS_MISSING_IN_MANIFEST": "EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers", + "ENTITY_SET_OR_CONTEXT_PATH_IS_MISSING_IN_MANIFEST": "Path cannot be identified: use absolute path or define contextPath", "EMPTY_CONTEXT_PATH_IN_MANIFEST": "ContextPath in manifest is empty. Attribute value completion and diagnostics are disabled", "RELATIVE_CONTEXT_PATH_IN_MANIFEST": "ContextPath in manifest \"{{value}}\" must be absolute. Attribute value completion and diagnostics are disabled", "UNKNOWN_CONTEXT_PATH_IN_MANIFEST": "Unknown contextPath in manifest \"{{value}}\". Attribute value completion and diagnostics are disabled", diff --git a/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts b/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts index 11350b644..5c53bc6fb 100644 --- a/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts +++ b/packages/fe/src/services/diagnostics/validators/missing-entity-set.ts @@ -55,7 +55,7 @@ export function validateMissingViewEntitySet( start: actualAttributeValueToken.startOffset, end: actualAttributeValueToken.endOffset, }, - severity: "info", + severity: "warn", }, ]; } diff --git a/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts b/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts index e63587dce..b1a804c9c 100644 --- a/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts +++ b/packages/fe/test/unit/services/diagnostics/validators/missing-entity-set.test.ts @@ -76,7 +76,7 @@ describe("missing entitySet validation", () => { } ); expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ - "kind: MissingEntitySet; text: EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers; severity:info; offset:344-354", + "kind: MissingEntitySet; text: Path cannot be identified: use absolute path or define contextPath; severity:warn; offset:344-354", ]); }); @@ -87,7 +87,7 @@ describe("missing entitySet validation", () => { (c) => ({ ...c, manifestDetails: undefined } as any) ); expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ - "kind: MissingEntitySet; text: EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers; severity:info; offset:344-349", + "kind: MissingEntitySet; text: Path cannot be identified: use absolute path or define contextPath; severity:warn; offset:344-349", ]); }); @@ -103,7 +103,7 @@ describe("missing entitySet validation", () => { } ); expect(result.map((item) => issueToSnapshot(item))).toStrictEqual([ - "kind: MissingEntitySet; text: EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers; severity:info; offset:344-349", + "kind: MissingEntitySet; text: Path cannot be identified: use absolute path or define contextPath; severity:warn; offset:344-349", ]); }); }); diff --git a/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md b/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md index e67c393c9..e52b873e4 100644 --- a/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md +++ b/packages/vscode-ui5-language-assistant/test/manual-tests/annotation_metaPath_attribute_handling.md @@ -57,7 +57,7 @@ annotate service.Travel with @( 4. Set the metaPath attribute value as `to_Booking`. Observe warning message `Path value must end with annotation term. Use code completion to select annotation path`. 5. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.LineItem`. Observe warning message `Invalid annotation term: "@com.sap.vocabularies.UI.v1.LineItem". Trigger code completion to choose one of allowed annotations`. 6. Go to app manifest file, find `routing\targets\TravelMain` settings entry and rename `entitySet` property in the nested object structure to `entitySet_`. Save the file. -7. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample1`. Observe info message `EntitySet or contextPath for the current view are not defined in application manifest. Attribute value completion and diagnostics is not possible if EntitySet or contextPath are not defined or defined dynamically in controllers`. +7. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample1`. Observe warn message `Path cannot be identified: use absolute path or define contextPath`. 8. Revert manifest change that is done at previous step 7. Change property `entitySet` value to `Travel_`. Save the file. 9. Set the metaPath attribute value as `@com.sap.vocabularies.UI.v1.Chart#sample`. Observe info message `Entity Set "Travel_" specified in manifest for the current view is not found. Attribute value completion and diagnostics are disabled`. 10. Reset property `entitySet` value to `Travel` in app manifest. Save the file. From baa9489c6e53eefaffa5751f91a813ee5fb33088 Mon Sep 17 00:00:00 2001 From: vadson71 Date: Tue, 30 Apr 2024 09:34:46 +0300 Subject: [PATCH 7/7] fix: readme update --- packages/vscode-ui5-language-assistant/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vscode-ui5-language-assistant/README.md b/packages/vscode-ui5-language-assistant/README.md index b469747a9..5bf31d5dd 100644 --- a/packages/vscode-ui5-language-assistant/README.md +++ b/packages/vscode-ui5-language-assistant/README.md @@ -202,6 +202,8 @@ For SAPUI5 XML views, this means:`*.view.xml` or `*.fragment.xml` files. Note that the extension **lazily** downloads the SAPUI5 metadata needed for its features. This means that there may be a delay between starting VS Code and having the relevant features available. +When working with CAP projects, make sure you have @sap/cds module installed. For this, run npm install on your project. This command will download and install all necessary modules from the npm package repository. + ### Enabling offline work You can set up a local web server to host one or more supported versions of SAP UI5 SDK and register it in the user/workspace setting `"UI5LanguageAssistant.SAPUI5WebServer"`. This overrides the public CDN of SAP UI5 SDK in the extension and enables offline work with the apps having the matching hosted `"minUI5Version"` in `manifest.json`.