diff --git a/packages/language-server/src/completion-items.ts b/packages/language-server/src/completion-items.ts
index c8ec593ab..b46e2fe89 100644
--- a/packages/language-server/src/completion-items.ts
+++ b/packages/language-server/src/completion-items.ts
@@ -27,6 +27,7 @@ import {
} from "@ui5-language-assistant/xml-views-completion";
import { ui5NodeToFQN } from "@ui5-language-assistant/logic-utils";
import { getNodeDocumentation, getNodeDetail } from "./documentation";
+import { assertNever } from "assert-never";
export function getCompletionItems(
model: UI5SemanticModel,
@@ -103,8 +104,7 @@ export function computeLSPKind(
case "BooleanValueInXMLAttributeValue":
return CompletionItemKind.Constant;
default:
- // TODO: we probably need a logging solution to highlight edge cases we
- // do not handle...
+ assertNever(suggestion, true);
return CompletionItemKind.Text;
}
}
@@ -179,11 +179,20 @@ function createTextEdits(
// Tag name
case "UI5AggregationsInXMLTagName": {
range = getXMLTagNameRange(suggestion.astNode) ?? range;
- const tagName = suggestion.ui5Node.name;
+ let parentNS: string | undefined = undefined;
+ /* istanbul ignore else - defensive programming (aggregation suggestions wil not be returned in the root tag) */
+ if (suggestion.astNode.parent.type === "XMLElement") {
+ parentNS = suggestion.astNode.parent.ns;
+ if (parentNS !== undefined && parentNS !== "") {
+ parentNS += ":";
+ }
+ }
+ const tagName = `${parentNS ?? ""}${suggestion.ui5Node.name}`;
+ filterText = `${parentNS ?? ""}${suggestion.ui5Node.name}`;
// Auto-close tag
/* istanbul ignore else */
if (shouldCloseXMLElement(suggestion.astNode)) {
- newText += `>\${0}${tagName}>`;
+ newText = `${tagName}>\${0}${tagName}>`;
} else {
additionalTextEdits.push(
...getClosingTagTextEdits(suggestion.astNode, tagName)
diff --git a/packages/language-server/test/completion-items-classes-spec.ts b/packages/language-server/test/completion-items-classes-spec.ts
index bedd7f66a..246c850f4 100644
--- a/packages/language-server/test/completion-items-classes-spec.ts
+++ b/packages/language-server/test/completion-items-classes-spec.ts
@@ -263,9 +263,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
it("will not insert the namespace when selecting completion for class in inner tag and namespace is already defined", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
+
`,
expected: [
{
@@ -282,9 +282,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
it("will insert the namespace when selecting completion for class in inner tag and namespace is not defined", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
+
`,
expected: [
{
@@ -383,7 +383,7 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
xmlSnippet: `
-
it("will replace the class closing tag name when the tag is closed and has the same name as the opening tag", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
⭲MenuButton⭰>
-
+
`,
expected: [
{
@@ -435,9 +435,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
it("will not replace the class closing tag name when the tag is closed and has a different name from the opening tag", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
-
+
`,
expected: [
{
@@ -459,9 +459,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
it("will not replace the class closing tag name when the tag is closed and the opening tag doesn't have a name", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
<⇶>
-
+
`,
expected: [
{
@@ -477,9 +477,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
it("will replace the class closing tag name when the tag is closed and does not have a name", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
⭲>⭰
-
+
`,
expected: [
{
@@ -511,9 +511,9 @@ describe("the UI5 language assistant Code Completion Services - classes", () =>
it("will replace the class closing tag name when also inserting the namespace", () => {
assertClassesCompletions({
xmlSnippet: `
-
+
⭲MenuButton⭰>
-
+
`,
expected: [
{
diff --git a/packages/language-server/test/completion-items-spec.ts b/packages/language-server/test/completion-items-spec.ts
index 6ccc5e70b..472215dbe 100644
--- a/packages/language-server/test/completion-items-spec.ts
+++ b/packages/language-server/test/completion-items-spec.ts
@@ -224,7 +224,7 @@ describe("the UI5 language assistant Code Completion Services", () => {
expect(suggestionKinds).to.deep.equal([CompletionItemKind.Reference]);
});
- it("will get completion values for UI5 aggregation", () => {
+ it("will get completion values for UI5 aggregation in the default namespace", () => {
const xmlSnippet = `
@@ -232,6 +232,31 @@ describe("the UI5 language assistant Code Completion Services", () => {
const suggestions = getSuggestions(xmlSnippet, ui5SemanticModel);
const suggestionsDetails = map(suggestions, (suggestion) => ({
label: suggestion.label,
+ tagName: getTagName(suggestion.textEdit),
+ replacedText: getTextInRange(xmlSnippet, suggestion.textEdit?.range),
+ }));
+ const suggestionKinds = uniq(
+ map(suggestions, (suggestion) => suggestion.kind)
+ );
+
+ expect(suggestionsDetails).to.deep.equalInAnyOrder([
+ { label: "contextMenu", tagName: "contextMenu", replacedText: "te" },
+ { label: "items", tagName: "items", replacedText: "te" },
+ { label: "swipeContent", tagName: "swipeContent", replacedText: "te" },
+ ]);
+
+ expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
+ });
+
+ it("will get completion values for UI5 aggregation in a non-default namespace", () => {
+ const xmlSnippet = `
+ ({
+ label: suggestion.label,
+ tagName: getTagName(suggestion.textEdit),
replacedText: getTextInRange(xmlSnippet, suggestion.textEdit?.range),
}));
const suggestionKinds = uniq(
@@ -239,15 +264,15 @@ describe("the UI5 language assistant Code Completion Services", () => {
);
expect(suggestionsDetails).to.deep.equalInAnyOrder([
- { label: "contextMenu", replacedText: "te" },
- { label: "items", replacedText: "te" },
- { label: "swipeContent", replacedText: "te" },
+ { label: "contextMenu", tagName: "m:contextMenu", replacedText: "te" },
+ { label: "items", tagName: "m:items", replacedText: "te" },
+ { label: "swipeContent", tagName: "m:swipeContent", replacedText: "te" },
]);
expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
});
- it("will get completion values for UI5 aggregation when the cursor is in the middle of a name", () => {
+ it("will get completion values for UI5 aggregation when the cursor is in the middle of a name in the default namespace", () => {
const xmlSnippet = `
@@ -275,7 +300,39 @@ describe("the UI5 language assistant Code Completion Services", () => {
expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
});
- it("will replace the aggregation closing tag name when the tag is closed and has the same name as the opening tag", () => {
+ it("will get completion values for UI5 aggregation when the cursor is in the middle of a name in a non-default namespace", () => {
+ const xmlSnippet = `
+ ({
+ label: suggestion.label,
+ replacedText: getTextInRange(xmlSnippet, suggestion.textEdit?.range),
+ tagName: getTagName(suggestion.textEdit),
+ }));
+ const suggestionKinds = uniq(
+ map(suggestions, (suggestion) => suggestion.kind)
+ );
+
+ expect(suggestionsDetails).to.deep.equalInAnyOrder([
+ {
+ label: "contextMenu",
+ replacedText: "teMenu",
+ tagName: "m:contextMenu",
+ },
+ { label: "items", replacedText: "teMenu", tagName: "m:items" },
+ {
+ label: "swipeContent",
+ replacedText: "teMenu",
+ tagName: "m:swipeContent",
+ },
+ ]);
+
+ expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
+ });
+
+ it("will replace the aggregation closing tag name when the tag is closed and has the same name as the opening tag in the default namespace", () => {
const xmlSnippet = `
@@ -336,7 +393,68 @@ describe("the UI5 language assistant Code Completion Services", () => {
expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
});
- it("will not replace the class closing tag name when the tag is closed and has a different name from the opening tag", () => {
+ it("will replace the aggregation closing tag name when the tag is closed and has the same name as the opening tag in a non-default namespace", () => {
+ const xmlSnippet = `
+
+ ⭲m:te⭰>
+
+ `;
+ const suggestions = getSuggestions(xmlSnippet, ui5SemanticModel);
+ const suggestionsDetails = map(suggestions, (suggestion) => ({
+ label: suggestion.label,
+ tagName: getTagName(suggestion.textEdit),
+ additionalTextEdits: suggestion.additionalTextEdits,
+ replacedText: getTextInRange(xmlSnippet, suggestion.textEdit?.range),
+ }));
+ const suggestionKinds = uniq(
+ map(suggestions, (suggestion) => suggestion.kind)
+ );
+
+ const ranges = getRanges(xmlSnippet);
+ expect(ranges, "additional text edits ranges").to.have.lengthOf(1);
+
+ expect(suggestionsDetails).to.deep.equalInAnyOrder([
+ {
+ label: "contextMenu",
+ tagName: "contextMenu",
+ additionalTextEdits: [
+ {
+ range: ranges[0],
+ newText: `m:contextMenu`,
+ },
+ ],
+ replacedText: "m:te",
+ },
+ {
+ label: "items",
+ tagName: "items",
+ additionalTextEdits: [
+ {
+ range: ranges[0],
+ newText: `m:items`,
+ },
+ ],
+ replacedText: "m:te",
+ },
+ {
+ label: "swipeContent",
+ tagName: "swipeContent",
+ additionalTextEdits: [
+ {
+ range: ranges[0],
+ newText: `m:swipeContent`,
+ },
+ ],
+ replacedText: "m:te",
+ },
+ ]);
+
+ expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
+ });
+
+ it("will not replace the aggregation closing tag name when the tag is closed and has a different name from the opening tag", () => {
const xmlSnippet = `
@@ -382,7 +500,7 @@ describe("the UI5 language assistant Code Completion Services", () => {
expect(suggestionKinds).to.deep.equal([CompletionItemKind.Field]);
});
- it("will replace the class closing tag name when the tag is closed and does not have a name", () => {
+ it("will replace the aggregation closing tag name when the tag is closed and does not have a name", () => {
const xmlSnippet = `
diff --git a/packages/language-server/test/hover-spec.ts b/packages/language-server/test/hover-spec.ts
index 3ce8fa775..25ec53810 100644
--- a/packages/language-server/test/hover-spec.ts
+++ b/packages/language-server/test/hover-spec.ts
@@ -27,9 +27,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -70,9 +70,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expect(response).to.not.exist;
@@ -84,9 +84,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -100,9 +100,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -118,9 +118,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -134,9 +134,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -150,9 +150,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -166,9 +166,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -181,7 +181,7 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expectExists(response, "Hover item");
@@ -195,9 +195,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const response = getHoverItem(xmlSnippet, ui5SemanticModel);
expect(response).to.not.exist;
diff --git a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/input.xml b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/input.xml
index d17fe1d47..55311988b 100644
--- a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/input.xml
+++ b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/input.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/output-lsp-response.json b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/output-lsp-response.json
index 356e66f47..28cdacee9 100644
--- a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/output-lsp-response.json
+++ b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-attribute-key/output-lsp-response.json
@@ -10,8 +10,8 @@
},
{
"range": {
- "start": { "line": 1, "character": 13 },
- "end": { "line": 1, "character": 23 }
+ "start": { "line": 1, "character": 17 },
+ "end": { "line": 1, "character": 27 }
},
"severity": 1,
"source": "UI5 Language Assistant",
diff --git a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/input.xml b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/input.xml
index 553b1ecaa..8d0c26393 100644
--- a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/input.xml
+++ b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/input.xml
@@ -2,7 +2,7 @@
-
+
<🢂Button_TYPO🢀 />
<🢂content_TYPO🢀>
@@ -10,7 +10,7 @@
<🢂mvc:TYPO🢀>
-
+
<🢂Button2_TYPO🢀 />
\ No newline at end of file
diff --git a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/output-lsp-response.json b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/output-lsp-response.json
index 8f5d82860..ad6c2d6f8 100644
--- a/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/output-lsp-response.json
+++ b/packages/language-server/test/snapshots/xml-view-diagnostics/unknown-tag-name/output-lsp-response.json
@@ -42,6 +42,6 @@
},
"severity": 1,
"source": "UI5 Language Assistant",
- "message": "The \"Button2_TYPO\" name is neither a class name nor an aggregation in the \"sap.ui.core.mvc.View\" class"
+ "message": "The \"Button2_TYPO\" name is neither a class name in the \"sap.m\" namespace nor an aggregation of the \"sap.ui.core.mvc.View\" class"
}
]
diff --git a/packages/logic-utils/api.d.ts b/packages/logic-utils/api.d.ts
index ef2ea739f..78abb6a1d 100644
--- a/packages/logic-utils/api.d.ts
+++ b/packages/logic-utils/api.d.ts
@@ -172,3 +172,54 @@ export function convertJSDocToMarkdown(
* @param link
*/
export function getLink(model: UI5SemanticModel, link: string): string;
+
+/**
+ * Split possibly qualified XML Tag or XML Attribute name to prefix and local name.
+ * If there is no prefix in the qualified name, the returned prefix will be undefined.
+ * @param qName
+ */
+export function splitQNameByNamespace(
+ qName: string
+): { prefix: string | undefined; localName: string };
+
+/**
+ * Return the xml namespace defined for the xml element prefix (ns), or undefined if not found
+ * @param xmlElement
+ */
+export function resolveXMLNS(xmlElement: XMLElement): string | undefined;
+
+/**
+ * Return the xml namespace defined for this prefix, or undefined if not found.
+ * The defined namespaces are taken from the xml element.
+ * @param prefix
+ * @param xmlElement
+ */
+export function resolveXMLNSFromPrefix(
+ prefix: string | undefined,
+ xmlElement: XMLElement
+): string | undefined;
+
+/**
+ * Check if the xml element namespace prefixes (ns) reference the same namespace
+ * @param xmlElement1
+ * @param xmlElement2
+ */
+export function isSameXMLNS(
+ xmlElement1: XMLElement,
+ xmlElement2: XMLElement
+): boolean;
+
+/**
+ * Check if the xml namespace prefixes reference the same namespace.
+ * The defined namespaces are taken from the respective xml elements.
+ * @param prefix1
+ * @param xmlElement1
+ * @param prefix2
+ * @param xmlElement2
+ */
+export function isSameXMLNSFromPrefix(
+ prefix1: string | undefined,
+ xmlElement1: XMLElement,
+ prefix2: string | undefined,
+ xmlElement2: XMLElement
+): boolean;
diff --git a/packages/logic-utils/src/api.ts b/packages/logic-utils/src/api.ts
index d7bc7fe17..1564026b5 100644
--- a/packages/logic-utils/src/api.ts
+++ b/packages/logic-utils/src/api.ts
@@ -23,3 +23,10 @@ export {
convertJSDocToMarkdown,
getLink,
} from "./utils/documentation";
+export { splitQNameByNamespace } from "./utils/split-qname";
+export {
+ resolveXMLNS,
+ resolveXMLNSFromPrefix,
+ isSameXMLNS,
+ isSameXMLNSFromPrefix,
+} from "./utils/xml-namespaces";
diff --git a/packages/logic-utils/src/utils/split-qname.ts b/packages/logic-utils/src/utils/split-qname.ts
new file mode 100644
index 000000000..b82f3a471
--- /dev/null
+++ b/packages/logic-utils/src/utils/split-qname.ts
@@ -0,0 +1,17 @@
+import { includes } from "lodash";
+
+export function splitQNameByNamespace(
+ qName: string
+): { prefix: string | undefined; localName: string } {
+ if (!includes(qName, ":")) {
+ return { prefix: undefined, localName: qName };
+ }
+ const match = qName.match(/(?[^:]*)(:(?.*))?/);
+ // There will always be a match because qName always contains a colon at this point
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const matchGroups = match!.groups!;
+ return {
+ prefix: matchGroups.ns,
+ localName: matchGroups.name,
+ };
+}
diff --git a/packages/logic-utils/src/utils/xml-namespaces.ts b/packages/logic-utils/src/utils/xml-namespaces.ts
new file mode 100644
index 000000000..d6da8f873
--- /dev/null
+++ b/packages/logic-utils/src/utils/xml-namespaces.ts
@@ -0,0 +1,51 @@
+import { XMLElement, DEFAULT_NS } from "@xml-tools/ast";
+
+export function resolveXMLNS(xmlElement: XMLElement): string | undefined {
+ return resolveXMLNSFromPrefix(xmlElement.ns, xmlElement);
+}
+
+export function resolveXMLNSFromPrefix(
+ prefix: string | undefined,
+ xmlElement: XMLElement
+): string | undefined {
+ // If no NS is explicitly defined try the default one
+ const prefixXmlns = prefix ?? DEFAULT_NS;
+ const resolvedXmlns = xmlElement.namespaces[prefixXmlns];
+ return resolvedXmlns;
+}
+
+export function isSameXMLNS(
+ xmlElement1: XMLElement,
+ xmlElement2: XMLElement
+): boolean {
+ return isSameXMLNSFromPrefix(
+ xmlElement1.ns,
+ xmlElement1,
+ xmlElement2.ns,
+ xmlElement2
+ );
+}
+
+export function isSameXMLNSFromPrefix(
+ prefix1: string | undefined,
+ xmlElement1: XMLElement,
+ prefix2: string | undefined,
+ xmlElement2: XMLElement
+): boolean {
+ // It's possible to re-define namespaces, so we can't rely on the namespace prefix to check this.
+ // It's also possible to define several prefixes for the same namespace.
+
+ // If the prefixes are resolved to the same namespace, they are the same
+ const ns1 = resolveXMLNSFromPrefix(prefix1, xmlElement1);
+ const ns2 = resolveXMLNSFromPrefix(prefix2, xmlElement2);
+ if (ns1 === ns2 && ns1 !== undefined) {
+ return true;
+ }
+
+ // If both prefixes are not defined but they are the same string we also consider them the same
+ if (ns1 === undefined && ns2 === undefined && prefix1 === prefix2) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/packages/logic-utils/src/utils/xml-node-to-ui5-node.ts b/packages/logic-utils/src/utils/xml-node-to-ui5-node.ts
index 7c947ac76..acfbe9d5f 100644
--- a/packages/logic-utils/src/utils/xml-node-to-ui5-node.ts
+++ b/packages/logic-utils/src/utils/xml-node-to-ui5-node.ts
@@ -2,8 +2,10 @@ import {
xmlToFQN,
flattenProperties,
flattenAggregations,
+ isSameXMLNS,
+ resolveXMLNS,
} from "@ui5-language-assistant/logic-utils";
-import { XMLElement, XMLAttribute, DEFAULT_NS } from "@xml-tools/ast";
+import { XMLElement, XMLAttribute } from "@xml-tools/ast";
import {
UI5Class,
UI5SemanticModel,
@@ -30,8 +32,9 @@ export function getUI5AggregationByXMLElement(
if (element.parent.type === "XMLDocument") {
return undefined;
}
- // Aggregations don't have a namesapce
- if (element.ns !== undefined) {
+ // Aggregations must be in the same namespace as their parent
+ // https://sapui5.hana.ondemand.com/#/topic/19eabf5b13214f27b929b9473df3195b
+ if (!isSameXMLNS(element, element.parent)) {
return undefined;
}
const ui5Class = getUI5ClassByXMLElement(element.parent, model);
@@ -66,7 +69,7 @@ export function getUI5NodeFromXMLElementNamespace(
isXmlnsDefined: boolean;
} {
const isDefault = xmlElement.ns === undefined;
- const xmlNamespace = xmlElement.namespaces[xmlElement.ns ?? DEFAULT_NS];
+ const xmlNamespace = resolveXMLNS(xmlElement);
if (xmlNamespace === undefined) {
return {
namespace: undefined,
diff --git a/packages/logic-utils/src/utils/xml-to-fqn.ts b/packages/logic-utils/src/utils/xml-to-fqn.ts
index 12effef91..83b789f45 100644
--- a/packages/logic-utils/src/utils/xml-to-fqn.ts
+++ b/packages/logic-utils/src/utils/xml-to-fqn.ts
@@ -1,11 +1,10 @@
-import { DEFAULT_NS, XMLElement } from "@xml-tools/ast";
+import { XMLElement } from "@xml-tools/ast";
+import { resolveXMLNS } from "../api";
export function xmlToFQN(astElement: XMLElement): string {
// TODO: is this the optimal way to handle nameless elements?
- const baseName = astElement.name ? astElement.name : "";
- // if no NS is explicitly defined try the default one
- const prefixXmlns = astElement.ns ? astElement.ns : DEFAULT_NS;
- const resolvedXmlns = astElement.namespaces[prefixXmlns];
+ const baseName = astElement.name ?? "";
+ const resolvedXmlns = resolveXMLNS(astElement);
if (resolvedXmlns !== undefined) {
// Note that adding the 'dot' seems to be a UI5 semantic, not xmlns semantics
diff --git a/packages/logic-utils/test/utils/split-qname-spec.ts b/packages/logic-utils/test/utils/split-qname-spec.ts
new file mode 100644
index 000000000..6092b7f6e
--- /dev/null
+++ b/packages/logic-utils/test/utils/split-qname-spec.ts
@@ -0,0 +1,50 @@
+import { expect } from "chai";
+import { splitQNameByNamespace } from "../../src/api";
+
+describe("The @ui5-language-assistant/logic-utils function", () => {
+ it("returns the local name with undefined prefix if it's not qualified", () => {
+ const { prefix, localName } = splitQNameByNamespace("some.name");
+ expect(prefix).to.be.undefined;
+ expect(localName).to.equal("some.name");
+ });
+
+ it("returns the prefix and local name for qualified name", () => {
+ const { prefix, localName } = splitQNameByNamespace(
+ "thenamespace:some.name"
+ );
+ expect(prefix).to.equal("thenamespace");
+ expect(localName).to.equal("some.name");
+ });
+
+ it("returns empty local name if qname ends with :", () => {
+ const { prefix, localName } = splitQNameByNamespace("thenamespace:");
+ expect(prefix).to.equal("thenamespace");
+ expect(localName).to.equal("");
+ });
+
+ it("returns empty prefix if qname starts with :", () => {
+ const { prefix, localName } = splitQNameByNamespace(":some.name");
+ expect(prefix).to.equal("");
+ expect(localName).to.equal("some.name");
+ });
+
+ it("returns empty local name and prefix if qname is :", () => {
+ const { prefix, localName } = splitQNameByNamespace(":");
+ expect(prefix).to.equal("");
+ expect(localName).to.equal("");
+ });
+
+ it("returns empty local name and undefined prefix if qname is an empty string", () => {
+ const { prefix, localName } = splitQNameByNamespace("");
+ expect(prefix).to.be.undefined;
+ expect(localName).to.equal("");
+ });
+
+ it("splits on the first :", () => {
+ const { prefix, localName } = splitQNameByNamespace(
+ "thenamespace:some:name"
+ );
+ expect(prefix).to.equal("thenamespace");
+ expect(localName).to.equal("some:name");
+ });
+});
diff --git a/packages/logic-utils/test/utils/xml-namespaces-spec.ts b/packages/logic-utils/test/utils/xml-namespaces-spec.ts
new file mode 100644
index 000000000..b3b4f264d
--- /dev/null
+++ b/packages/logic-utils/test/utils/xml-namespaces-spec.ts
@@ -0,0 +1,504 @@
+import { expect } from "chai";
+import {
+ resolveXMLNS,
+ resolveXMLNSFromPrefix,
+ isSameXMLNS,
+ isSameXMLNSFromPrefix,
+} from "../../src/api";
+import { XMLElement, buildAst } from "@xml-tools/ast";
+import { parse, DocumentCstNode } from "@xml-tools/parser";
+import { expectExists } from "@ui5-language-assistant/test-utils";
+
+describe("The @ui5-language-assistant/logic-utils function", () => {
+ context("prefix exists", () => {
+ it("returns the namespace for defined prefix", () => {
+ const rootElement = getRootElement(
+ ``
+ );
+ expect(resolveXMLNSFromPrefix("a", rootElement)).to.equal("a.ns");
+ });
+
+ it("returns the correct namespace when it's redefined", () => {
+ const rootElement = getRootElement(`
+
+
+ `);
+ expect(rootElement.subElements[0]).to.exist;
+ expect(resolveXMLNSFromPrefix("a", rootElement.subElements[0])).to.equal(
+ "redefined.a"
+ );
+ });
+ });
+
+ context("prefix is not sent", () => {
+ it("returns the default namespace when it's defined", () => {
+ const rootElement = getRootElement(
+ ``
+ );
+ expect(resolveXMLNSFromPrefix(undefined, rootElement)).to.equal(
+ "default.ns"
+ );
+ });
+
+ it("returns undefined when the default namespace is not defined", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNSFromPrefix(undefined, rootElement)).to.be.undefined;
+ });
+ });
+
+ context("prefix is not defined", () => {
+ it("returns undefined", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNSFromPrefix("x", rootElement)).to.be.undefined;
+ });
+
+ it("returns undefined when there is a default namespace", () => {
+ const rootElement = getRootElement(
+ ``
+ );
+ expect(resolveXMLNSFromPrefix("x", rootElement)).to.be.undefined;
+ });
+ });
+});
+
+describe("The @ui5-language-assistant/logic-utils function", () => {
+ context("element prefix exists", () => {
+ it("returns the namespace for defined prefix", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNS(rootElement)).to.equal("a.ns");
+ });
+
+ it("returns the correct namespace when it's redefined", () => {
+ const rootElement = getRootElement(`
+
+
+ `);
+ expect(rootElement.subElements[0]).to.exist;
+ expect(resolveXMLNS(rootElement.subElements[0])).to.equal("redefined.a");
+ });
+ });
+
+ context("element doesn't have a prefix", () => {
+ it("returns the default namespace when it's defined", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNS(rootElement)).to.equal("default.ns");
+ });
+
+ it("returns undefined when the default namespace is not defined", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNS(rootElement)).to.be.undefined;
+ });
+ });
+
+ context("element prefix is not defined", () => {
+ it("returns undefined", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNS(rootElement)).to.be.undefined;
+ });
+
+ it("returns undefined when there is a default namespace", () => {
+ const rootElement = getRootElement(``);
+ expect(resolveXMLNS(rootElement)).to.be.undefined;
+ });
+ });
+});
+
+describe("The @ui5-language-assistant/logic-utils function", () => {
+ context("bothe prefixes are defined", () => {
+ it("returns true when it's the same prefix", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "a",
+ rootElement.subElements[0],
+ "a",
+ rootElement.subElements[1]
+ )
+ ).to.be.true;
+ });
+
+ it("returns true when the prefixes are different but they are resolved to the same namespace", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "y",
+ rootElement.subElements[1]
+ )
+ ).to.be.true;
+ });
+
+ it("returns false when the prefixes are resolved to different namespaces", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "y",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+
+ it("returns false when the prefixes are the same but one of them is redefined to a different namespace", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "a",
+ rootElement.subElements[0],
+ "a",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+ });
+
+ context("only one of the prefixes is defined", () => {
+ it("returns false when default namespace is defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "a",
+ rootElement.subElements[0],
+ "x",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "a",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+
+ it("returns false when default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "a",
+ rootElement.subElements[0],
+ "x",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "a",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+
+ it("returns false when the other is not sent and the default namespace is defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "a",
+ rootElement.subElements[0],
+ undefined,
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ expect(
+ isSameXMLNSFromPrefix(
+ undefined,
+ rootElement.subElements[0],
+ "a",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+
+ it("returns false when the other is not sent and the default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "a",
+ rootElement.subElements[0],
+ undefined,
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ expect(
+ isSameXMLNSFromPrefix(
+ undefined,
+ rootElement.subElements[0],
+ "a",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+ });
+
+ context("both prefixes are not defined", () => {
+ it("returns true when it's the same prefix", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "x",
+ rootElement.subElements[1]
+ )
+ ).to.be.true;
+ });
+
+ it("returns false when it's not the same prefix and the default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "y",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+
+ it("returns false when it's not the same prefix and the default namespace is defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNSFromPrefix(
+ "x",
+ rootElement.subElements[0],
+ "y",
+ rootElement.subElements[1]
+ )
+ ).to.be.false;
+ });
+ });
+});
+
+describe("The @ui5-language-assistant/logic-utils function", () => {
+ context("bothe prefixes are defined", () => {
+ it("returns true when it's the same prefix", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.true;
+ });
+
+ it("returns true when the prefixes are different but they are resolved to the same namespace", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.true;
+ });
+
+ it("returns false when the prefixes are resolved to different namespaces", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ });
+
+ it("returns false when the prefixes are the same but one of them is redefined to a different namespace", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ });
+ });
+
+ context("only one of the prefixes is defined", () => {
+ it("returns false when default namespace is defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ expect(
+ isSameXMLNS(rootElement.subElements[1], rootElement.subElements[0])
+ ).to.be.false;
+ });
+
+ it("returns false when default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ expect(
+ isSameXMLNS(rootElement.subElements[1], rootElement.subElements[0])
+ ).to.be.false;
+ });
+
+ it("returns false when the other doesn't have a prefix and the default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ expect(
+ isSameXMLNS(rootElement.subElements[1], rootElement.subElements[0])
+ ).to.be.false;
+ });
+
+ it("returns false when the other doesn't have a prefix and the default namespace is defined to a different namespace", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ expect(
+ isSameXMLNS(rootElement.subElements[1], rootElement.subElements[0])
+ ).to.be.false;
+ });
+
+ it("returns true when the other doesn't have a prefix and the default namespace is defined to the same namespace", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.true;
+ expect(
+ isSameXMLNS(rootElement.subElements[1], rootElement.subElements[0])
+ ).to.be.true;
+ });
+ });
+
+ context("both prefixes are not defined", () => {
+ it("returns true when it's the same prefix", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.true;
+ });
+
+ it("returns false when it's not the same prefix and the default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ });
+
+ it("returns false when it's not the same prefix and the default namespace is defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.false;
+ });
+
+ it("returns true when there is no prefix and the default namespace is not defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.true;
+ });
+
+ it("returns true when there is no prefix and the default namespace is defined", () => {
+ const rootElement = getRootElement(`
+
+
+
+ `);
+ expect(
+ isSameXMLNS(rootElement.subElements[0], rootElement.subElements[1])
+ ).to.be.true;
+ });
+ });
+});
+
+function getRootElement(xmlText: string): XMLElement {
+ const { cst, tokenVector } = parse(xmlText);
+ const ast = buildAst(cst as DocumentCstNode, tokenVector);
+ expectExists(ast.rootElement, "ast root element");
+ return ast.rootElement;
+}
diff --git a/packages/logic-utils/test/utils/xml-node-to-ui5-node-spec.ts b/packages/logic-utils/test/utils/xml-node-to-ui5-node-spec.ts
index 5e92c917b..c96ccab42 100644
--- a/packages/logic-utils/test/utils/xml-node-to-ui5-node-spec.ts
+++ b/packages/logic-utils/test/utils/xml-node-to-ui5-node-spec.ts
@@ -94,6 +94,34 @@ describe("The @ui5-language-assistant/logic-utils {
+ const xmlText = `
+
+
+ `;
+ const element = getRootElementChild(xmlText);
+
+ const ui5Aggregation = getUI5AggregationByXMLElement(element, ui5Model);
+ expectExists(ui5Aggregation, "ui5 aggregation");
+ expect(ui5NodeToFQN(ui5Aggregation)).to.equal(
+ "sap.ui.core.mvc.View.content"
+ );
+ });
+
+ it("returns the aggregation for known aggregation under a class tag with a different prefix that references the same namespace", () => {
+ const xmlText = `
+
+
+ `;
+ const element = getRootElementChild(xmlText);
+
+ const ui5Aggregation = getUI5AggregationByXMLElement(element, ui5Model);
+ expectExists(ui5Aggregation, "ui5 aggregation");
+ expect(ui5NodeToFQN(ui5Aggregation)).to.equal(
+ "sap.ui.core.mvc.View.content"
+ );
+ });
+
it("returns undefined for unknown aggregation under a class tag", () => {
const xmlText = `
@@ -127,7 +155,7 @@ describe("The @ui5-language-assistant/logic-utils {
+ it("returns undefined for tag with known namespace under a class tag without namespace", () => {
const xmlText = `
@@ -138,6 +166,39 @@ describe("The @ui5-language-assistant/logic-utils {
+ const xmlText = `
+
+
+ `;
+ const element = getRootElementChild(xmlText);
+
+ const ui5Aggregation = getUI5AggregationByXMLElement(element, ui5Model);
+ expect(ui5Aggregation, "ui5 aggregation").to.be.undefined;
+ });
+
+ it("returns undefined for tag with unknown namespace under a class tag with a different namespace", () => {
+ const xmlText = `
+
+
+ `;
+ const element = getRootElementChild(xmlText);
+
+ const ui5Aggregation = getUI5AggregationByXMLElement(element, ui5Model);
+ expect(ui5Aggregation, "ui5 aggregation").to.be.undefined;
+ });
+
+ it("returns undefined for tag with empty namespace under a class tag without a namespace", () => {
+ const xmlText = `
+
+ <:content>
+ `;
+ const element = getRootElementChild(xmlText);
+
+ const ui5Aggregation = getUI5AggregationByXMLElement(element, ui5Model);
+ expect(ui5Aggregation, "ui5 aggregation").to.be.undefined;
+ });
+
it("returns undefined for non-aggregation node under a class tag", () => {
const xmlText = `
@@ -149,7 +210,7 @@ describe("The @ui5-language-assistant/logic-utils {
+ it("returns undefined for root tag", () => {
const xmlText = `
`;
const element = getRootElement(xmlText);
diff --git a/packages/vscode-ui5-language-assistant/test/suite/extension-spec.ts b/packages/vscode-ui5-language-assistant/test/suite/extension-spec.ts
index 47d25b5a3..b941f8059 100644
--- a/packages/vscode-ui5-language-assistant/test/suite/extension-spec.ts
+++ b/packages/vscode-ui5-language-assistant/test/suite/extension-spec.ts
@@ -64,7 +64,7 @@ describe("the Language Server Client Integration Tests", () => {
const xmlSnippet = `
-
+
_.name))
);
+ const existingAggregationsWithoutCurrent =
+ xmlElement.name === null
+ ? existingAggregations
+ : reject(existingAggregations, (name) => name === xmlElement.name);
const uniquePrefixMatchingAggregations = filterMembersForSuggestion(
flattenAggregations(parentUI5Class),
- prefix,
- existingAggregations
+ prefixParts.localName,
+ existingAggregationsWithoutCurrent
);
return map(uniquePrefixMatchingAggregations, (_) => ({
diff --git a/packages/xml-views-completion/test/providers/elementName/aggregation-spec.ts b/packages/xml-views-completion/test/providers/elementName/aggregation-spec.ts
index 12a56779e..888997ddd 100644
--- a/packages/xml-views-completion/test/providers/elementName/aggregation-spec.ts
+++ b/packages/xml-views-completion/test/providers/elementName/aggregation-spec.ts
@@ -112,6 +112,32 @@ describe("The ui5-language-assistant xml-views-completion", () => {
});
});
+ it("will suggest the current aggregation", () => {
+ const xmlSnippet = `
+
+
+
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ const suggestedNames = map(suggestions, (_) => _.ui5Node.name);
+ expect(suggestedNames).to.include.members(["footer"]);
+ expectAggregationsSuggestions(suggestions, "Page");
+ },
+ });
+ });
+
it("will filter suggestions on prefix (true prefix)", () => {
const xmlSnippet = `
{
},
});
});
+
+ it("will return suggestions when namespace is the same as parent", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ const suggestedNames = map(suggestions, (_) => _.ui5Node.name);
+ expect(suggestedNames).to.include.members([
+ "customData",
+ "customHeader",
+ ]);
+ expect(suggestedNames).to.not.include.members([
+ "content",
+ "dependents",
+ "dragDropConfig",
+ "footer",
+ "headerContent",
+ "landmarkInfo",
+ "layoutData",
+ "subHeader",
+ "tooltip",
+ ]);
+ expectAggregationsSuggestions(suggestions, "Page");
+ },
+ });
+ });
+
+ it("will return suggestions when namespace prefix is different but referenced namespace is the same as parent", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ const suggestedNames = map(suggestions, (_) => _.ui5Node.name);
+ expect(suggestedNames).to.include.members([
+ "customData",
+ "customHeader",
+ ]);
+ expect(suggestedNames).to.not.include.members([
+ "content",
+ "dependents",
+ "dragDropConfig",
+ "footer",
+ "headerContent",
+ "landmarkInfo",
+ "layoutData",
+ "subHeader",
+ "tooltip",
+ ]);
+ expectAggregationsSuggestions(suggestions, "Page");
+ },
+ });
+ });
+
+ it("will return suggestions when parent has namespace and prefix doesn't", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ const suggestedNames = map(suggestions, (_) => _.ui5Node.name);
+ expect(suggestedNames).to.include.members([
+ "customData",
+ "customHeader",
+ ]);
+ expect(suggestedNames).to.not.include.members([
+ "content",
+ "dependents",
+ "dragDropConfig",
+ "footer",
+ "headerContent",
+ "landmarkInfo",
+ "layoutData",
+ "subHeader",
+ "tooltip",
+ ]);
+ expectAggregationsSuggestions(suggestions, "Page");
+ },
+ });
+ });
+
+ it("will return suggestions when prefix only contains the namespace and it is the same as parent", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ const suggestedNames = map(suggestions, (_) => _.ui5Node.name);
+ expect(suggestedNames).to.include.members([
+ "customData",
+ "dependents",
+ "dragDropConfig",
+ "layoutData",
+ "tooltip",
+ "content",
+ "customHeader",
+ "footer",
+ "headerContent",
+ "landmarkInfo",
+ "subHeader",
+ ]);
+ expectAggregationsSuggestions(suggestions, "Page");
+ },
+ });
+ });
});
context("none applicable scenarios", () => {
@@ -263,6 +440,50 @@ describe("The ui5-language-assistant xml-views-completion", () => {
},
});
});
+
+ it("will not suggest when namespace is not the same as parent", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ expect(suggestions).to.be.empty;
+ },
+ });
+ });
+
+ it("will not suggest when prefix has namespace and parent doesn't", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+
+ testSuggestionsScenario({
+ model: REAL_UI5_MODEL,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [aggregationSuggestions],
+ },
+ assertion: (suggestions) => {
+ expect(suggestions).to.be.empty;
+ },
+ });
+ });
});
});
});
diff --git a/packages/xml-views-completion/test/providers/elementName/classes-spec.ts b/packages/xml-views-completion/test/providers/elementName/classes-spec.ts
index 1b882fd20..74935390a 100644
--- a/packages/xml-views-completion/test/providers/elementName/classes-spec.ts
+++ b/packages/xml-views-completion/test/providers/elementName/classes-spec.ts
@@ -211,9 +211,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
-
+
<⇶
-
+
`;
testSuggestionsScenario({
@@ -262,9 +262,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
-
+
+
`;
testSuggestionsScenario({
@@ -299,9 +299,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
xmlns="sap.m"
xmlns:forms="sap.ui.commons.form"
>
-
+
+
`;
testSuggestionsScenario({
@@ -335,9 +335,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
xmlns="sap.m"
xmlns:forms="sap.ui.commons.form"
>
-
+
+
`;
testSuggestionsScenario({
@@ -372,9 +372,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
-
+
+
`;
testSuggestionsScenario({
@@ -400,11 +400,11 @@ describe("The ui5-language-assistant xml-views-completion", () => {
-
+
<⇶
-
+
`;
testSuggestionsScenario({
@@ -441,7 +441,29 @@ describe("The ui5-language-assistant xml-views-completion", () => {
});
});
- it("will offer no suggestion when an aggregation's parent tag does not start with upper case character", () => {
+ it("will offer no suggestions when under a tag with only namespace", () => {
+ const xmlSnippet = `
+
+
+ <⇶
+
+ `;
+
+ testSuggestionsScenario({
+ model: ui5Model,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [classesSuggestions],
+ },
+ assertion: (suggestions) => {
+ expect(suggestions).to.be.empty;
+ },
+ });
+ });
+
+ it("will offer no suggestion when the parent tag is not a recognized class or aggreation", () => {
const xmlSnippet = `
{
-
+
<⇶
-
+
`;
testSuggestionsScenario({
@@ -490,9 +512,31 @@ describe("The ui5-language-assistant xml-views-completion", () => {
-
+
+ <⇶
+
+ `;
+
+ testSuggestionsScenario({
+ model: ui5Model,
+ xmlText: xmlSnippet,
+ providers: {
+ elementName: [classesSuggestions],
+ },
+ assertion: (suggestions) => {
+ expect(suggestions).to.be.empty;
+ },
+ });
+ });
+
+ it("will offer no suggestions inside an explicit aggregation when the aggregation namespace is not recognized", () => {
+ const xmlSnippet = `
+
+
<⇶
-
+
`;
testSuggestionsScenario({
@@ -511,9 +555,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
const xmlSnippet = `
-
+
+
`;
testSuggestionsScenario({
@@ -544,9 +588,9 @@ describe("The ui5-language-assistant xml-views-completion", () => {
-
+
<⇶
-
+
`;
testSuggestionsScenario({
diff --git a/packages/xml-views-tooltip/src/tooltip.ts b/packages/xml-views-tooltip/src/tooltip.ts
index 58a487198..1a976916d 100644
--- a/packages/xml-views-tooltip/src/tooltip.ts
+++ b/packages/xml-views-tooltip/src/tooltip.ts
@@ -1,6 +1,6 @@
-import { find, includes } from "lodash";
+import { find } from "lodash";
import { assertNever } from "assert-never";
-import { XMLAttribute, XMLElement, DEFAULT_NS } from "@xml-tools/ast";
+import { XMLAttribute, XMLElement } from "@xml-tools/ast";
import { isXMLNamespaceKey } from "@xml-tools/common";
import {
XMLElementOpenName,
@@ -16,6 +16,9 @@ import {
flattenProperties,
flattenEvents,
flattenAssociations,
+ splitQNameByNamespace,
+ isSameXMLNSFromPrefix,
+ resolveXMLNSFromPrefix,
} from "@ui5-language-assistant/logic-utils";
import {
UI5Class,
@@ -73,16 +76,26 @@ function findUI5NodeByElement(
}
// openName or closeName cannot be undefined here because otherwise the ast position visitor wouldn't return their types
- const nameByKind = isOpenName
+ const tagQName = isOpenName
? /* istanbul ignore next */
astNode.syntax.openName?.image
: /* istanbul ignore next */
astNode.syntax.closeName?.image;
+ /* istanbul ignore if */
+ if (tagQName === undefined) {
+ return undefined;
+ }
- return nameByKind !== undefined
- ? findAggragationByName(parentElementClass, nameByKind)
- : /* istanbul ignore next */
- undefined;
+ // Aggregations must be in the same namespace as their parent
+ // https://sapui5.hana.ondemand.com/#/topic/19eabf5b13214f27b929b9473df3195b
+ const { prefix, localName } = splitQNameByNamespace(tagQName);
+ if (
+ !isSameXMLNSFromPrefix(prefix, astNode, astNode.parent.ns, astNode.parent)
+ ) {
+ return undefined;
+ }
+
+ return findAggragationByName(parentElementClass, localName);
}
function findAggragationByName(
@@ -98,38 +111,18 @@ function findAggragationByName(
return ui5Aggregation;
}
-function splitQNameByNamespace(
- qName: string
-): { ns: string | undefined; name: string } {
- if (!includes(qName, ":")) {
- return { name: qName, ns: undefined };
- }
- const match = qName.match(/(?[^:]*)(:(?.*))?/);
- // There will always be a match because qName always contains a colon at this point
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const matchGroups = match!.groups!;
- return {
- ns: matchGroups.ns,
- name:
- matchGroups.name ??
- /* istanbul ignore next */
- "",
- };
-}
-
function elementClosingTagToFQN(xmlElement: XMLElement): string {
//the closeName can't be undefined here because otherwise the ast position visitor wouldn't return its type
/* istanbul ignore next */
const qName = xmlElement.syntax.closeName?.image ?? "";
- const { ns, name } = splitQNameByNamespace(qName);
- const prefixXmlns = ns ?? DEFAULT_NS;
- const resolvedXmlns = xmlElement.namespaces[prefixXmlns];
+ const { prefix, localName } = splitQNameByNamespace(qName);
+ const resolvedXmlns = resolveXMLNSFromPrefix(prefix, xmlElement);
if (resolvedXmlns !== undefined) {
- return resolvedXmlns + "." + name;
+ return resolvedXmlns + "." + localName;
}
- return name;
+ return localName;
}
function findUI5NodeByXMLAttributeKey(
diff --git a/packages/xml-views-tooltip/test/tooltip-spec.ts b/packages/xml-views-tooltip/test/tooltip-spec.ts
index 72803252d..bdb9f82e9 100644
--- a/packages/xml-views-tooltip/test/tooltip-spec.ts
+++ b/packages/xml-views-tooltip/test/tooltip-spec.ts
@@ -24,9 +24,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -38,24 +38,24 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
it("will get hover content UI5 property in unknown tag", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
});
@@ -64,9 +64,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -78,33 +78,33 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
it("will get hover content UI5 property - incorrect enum", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
it("will get hover content UI5 namespace", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -118,9 +118,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -132,9 +132,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -142,27 +142,79 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
expect(ui5Node.kind).equal("UI5Aggregation");
});
- it("will get hover content of unknown tag with unknown parent tag", () => {
+ it("will get hover content UI5 Aggregation with a different namespace prefix that references the same namespace", () => {
const xmlSnippet = `
-
-
-
+
+
+
+ `;
+ const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
+ expectExists(ui5Node, "UI5Node");
+ expect(ui5Node.name).to.equal("content");
+ expect(ui5Node.kind).equal("UI5Aggregation");
+ });
+
+ it("will get hover content UI5 Aggregation in the default namespace", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+ const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
+ expectExists(ui5Node, "UI5Node");
+ expect(ui5Node.name).to.equal("content");
+ expect(ui5Node.kind).equal("UI5Aggregation");
+ });
+
+ it("will not get hover content for UI5 Aggregation in the wrong namespace", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+ const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
+ expect(ui5Node).to.be.undefined;
+ });
+
+ it("will not get hover content UI5 Aggregation when only the aggregation doesn't have a namespace", () => {
+ const xmlSnippet = `
+
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
});
context("hover on element close tag name", () => {
+ it("will get hover content of unknown tag with unknown parent tag", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+ const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
+ expect(ui5Node).to.be.undefined;
+ });
+
it("will get hover content UI5 class", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -174,9 +226,9 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -184,6 +236,35 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
expect(ui5Node.kind).equal("UI5Aggregation");
});
+ it("will get hover content UI5 Aggregation with a different namespace prefix that references the same namespace", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+ const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
+ expectExists(ui5Node, "UI5Node");
+ expect(ui5Node.name).to.equal("content");
+ expect(ui5Node.kind).equal("UI5Aggregation");
+ });
+
+ it("will get hover content UI5 Aggregation in the default namespace", () => {
+ const xmlSnippet = `
+
+
+
+ `;
+ const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
+ expectExists(ui5Node, "UI5Node");
+ expect(ui5Node.name).to.equal("content");
+ expect(ui5Node.kind).equal("UI5Aggregation");
+ });
+
it("will get hover content UI5 class with default namespace", () => {
const xmlSnippet = ` {
xmlns="sap.m">
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
it("will get hover content UI5 class when open and close tag are different", () => {
const xmlSnippet = `
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
expectExists(ui5Node, "UI5Node");
@@ -221,12 +302,12 @@ describe("the UI5 language assistant Hover Tooltip Service", () => {
const xmlSnippet = `
-
+
-
+
`;
const ui5Node = getUI5Node(xmlSnippet, ui5SemanticModel);
- expect(ui5Node).to.not.exist;
+ expect(ui5Node).to.be.undefined;
});
});
diff --git a/packages/xml-views-validation/src/utils/messages.ts b/packages/xml-views-validation/src/utils/messages.ts
index 0547063ce..c1204cc5f 100644
--- a/packages/xml-views-validation/src/utils/messages.ts
+++ b/packages/xml-views-validation/src/utils/messages.ts
@@ -4,7 +4,11 @@
export const UNKNOWN_CLASS_IN_NS = `The "{0}" class does not exist in the "{1}" namespace`;
export const UNKNOWN_CLASS_WITHOUT_NS = `The "{0}" class does not exist, please specify a namespace`;
export const UNKNOWN_AGGREGATION_IN_CLASS = `The "{0}" aggregation does not exist in the "{1}" class`;
-export const UNKNOWN_TAG_NAME_IN_CLASS = `The "{0}" name is neither a class name nor an aggregation in the "{1}" class`;
+export const UNKNOWN_AGGREGATION_IN_CLASS_DIFF_NAMESPACE = `The "{0}" aggregation must have the same namespace as the "{1}" class`;
+export const UNKNOWN_TAG_NAME_IN_CLASS = `The "{0}" name is neither a class name nor an aggregation of the "{1}" class`;
+export const UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS = `The "{0}" name is neither a class name in the "{1}" namespace nor an aggregation of the "{2}" class`;
+export const UNKNOWN_TAG_NAME_IN_NS = `The "{0}" name is neither a class name in the "{1}" namespace nor an aggregation of its parent tag`;
+export const UNKNOWN_TAG_NAME_NO_NS = `The "{0}" name is neither a class name nor an aggregation of its parent tag, please specify a namespace`;
export function getMessage(message: string, ...params: string[]): string {
let result = message;
diff --git a/packages/xml-views-validation/src/validators/attributes/unknown-attribute-key.ts b/packages/xml-views-validation/src/validators/attributes/unknown-attribute-key.ts
index 8bf890a2a..4f3aa04c7 100644
--- a/packages/xml-views-validation/src/validators/attributes/unknown-attribute-key.ts
+++ b/packages/xml-views-validation/src/validators/attributes/unknown-attribute-key.ts
@@ -13,6 +13,7 @@ import {
flattenAssociations,
flattenAggregations,
ui5NodeToFQN,
+ splitQNameByNamespace,
} from "@ui5-language-assistant/logic-utils";
import { UnknownAttributeKeyIssue } from "../../../api";
import { TEMPLATING_NS, CUSTOM_DATA_NS } from "../../utils/special-namespaces";
@@ -183,17 +184,15 @@ function isValidUI5ClassAttribute(
function splitAttributeByNamespace(
attribute: XMLAttribute & { key: string }
-): { ns: string | undefined; name: string | undefined } {
- if (!includes(attribute.key, ":")) {
- return { name: attribute.key, ns: undefined };
+): { ns: string | undefined; name: string } {
+ const { prefix, localName } = splitQNameByNamespace(attribute.key);
+ if (prefix === undefined) {
+ return { ns: prefix, name: localName };
}
- const match = attribute.key.match(/(?[^:]*)(:(?.*))?/);
- // There will always be a match because the attribute key always contains a colon at this point
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const matchGroups = match!.groups!;
- const resolvedNS = attribute.parent.namespaces[matchGroups.ns];
+ // Can't use resolveXMLNSFromPrefix here because attributes don't use the default namespace
+ const resolvedNS = attribute.parent.namespaces[prefix];
return {
ns: resolvedNS,
- name: matchGroups.name,
+ name: localName,
};
}
diff --git a/packages/xml-views-validation/src/validators/elements/unknown-tag-name.ts b/packages/xml-views-validation/src/validators/elements/unknown-tag-name.ts
index c7f30b71f..e7ab45ea0 100644
--- a/packages/xml-views-validation/src/validators/elements/unknown-tag-name.ts
+++ b/packages/xml-views-validation/src/validators/elements/unknown-tag-name.ts
@@ -1,18 +1,27 @@
import { UnknownTagNameIssue } from "../../../api";
-import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types";
+import {
+ UI5SemanticModel,
+ BaseUI5Node,
+ UI5Class,
+} from "@ui5-language-assistant/semantic-model-types";
import { XMLElement, XMLToken } from "@xml-tools/ast";
import {
getUI5ClassByXMLElement,
ui5NodeToFQN,
getUI5NodeFromXMLElementNamespace,
getUI5AggregationByXMLElement,
+ isSameXMLNS,
} from "@ui5-language-assistant/logic-utils";
import {
getMessage,
UNKNOWN_CLASS_IN_NS,
UNKNOWN_CLASS_WITHOUT_NS,
UNKNOWN_AGGREGATION_IN_CLASS,
+ UNKNOWN_AGGREGATION_IN_CLASS_DIFF_NAMESPACE,
UNKNOWN_TAG_NAME_IN_CLASS,
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
+ UNKNOWN_TAG_NAME_IN_NS,
+ UNKNOWN_TAG_NAME_NO_NS,
} from "../../utils/messages";
export function validateUnknownTagName(
@@ -59,7 +68,7 @@ function validateTagWithNamespace(
// To avoid false positives, we assume unrecognized namespaces are user-defined so we don't validate tags in them
// (they could be user-defined class tags).
- // They could also be valid special namespaces like xhtml.
+ // They could also be valid special namespaces like xhtml or template.
// TODO There should be an error in xml-tools if the namespace is not defined in an xmlns attribute
const { namespace: ui5Namespace } = getUI5NodeFromXMLElementNamespace(
xmlElement,
@@ -69,20 +78,43 @@ function validateTagWithNamespace(
return [];
}
- // Check if it's a known class (aggregations don't have namespaces)
- const ui5Class = getUI5ClassByXMLElement(xmlElement, model);
- if (ui5Class !== undefined) {
+ // Check if it's a known class or aggregaion
+ if (
+ getUI5ClassByXMLElement(xmlElement, model) !== undefined ||
+ getUI5AggregationByXMLElement(xmlElement, model) !== undefined
+ ) {
return [];
}
+ // This is an unrecognized element on a non-custom namespace.
+ // Try to find out what this element was supposed to be to give the most accurate error message.
+
+ // Check if it could be an aggregation:
+ // Aggregations cannot be the root element and cannot be under another aggregation.
+ // Aggregations are always in the parent element (class) namespace.
+ //
+ if (
+ xmlElement.parent.type === "XMLElement" &&
+ isSameXMLNS(xmlElement.parent, xmlElement) &&
+ getUI5AggregationByXMLElement(xmlElement.parent, model) === undefined
+ ) {
+ return [
+ {
+ ...issueDefaults,
+ message: getUnknownTagNameMessage(
+ xmlElement.name,
+ ui5Namespace,
+ getUI5ClassByXMLElement(xmlElement.parent, model)
+ ),
+ },
+ ];
+ }
+
+ // If it's not an aggregation it can only be a class
return [
{
...issueDefaults,
- message: getMessage(
- UNKNOWN_CLASS_IN_NS,
- xmlElement.name,
- ui5NodeToFQN(ui5Namespace)
- ),
+ message: getUnknownClassMessage(xmlElement.name, ui5Namespace),
},
];
}
@@ -119,23 +151,14 @@ function validateTagWithoutNamespace(
}
// This is an unrecognized element on a non-custom (or undefined) default namespace.
- // Try to find out what this element can be.
+ // Try to find out what this element was supposed to be to give the most accurate error message.
+
+ // If it's the root tag, it can only be a class
if (xmlElement.parent.type === "XMLDocument") {
- // If it's the root tag, it can only be a class
- let message: string;
- if (ui5Namespace !== undefined) {
- message = getMessage(
- UNKNOWN_CLASS_IN_NS,
- xmlElement.name,
- ui5NodeToFQN(ui5Namespace)
- );
- } else {
- message = getMessage(UNKNOWN_CLASS_WITHOUT_NS, xmlElement.name);
- }
return [
{
...issueDefaults,
- message,
+ message: getUnknownClassMessage(xmlElement.name, ui5Namespace),
},
];
}
@@ -143,26 +166,28 @@ function validateTagWithoutNamespace(
// Check if the parent is a recognized class
const parentUI5Class = getUI5ClassByXMLElement(xmlElement.parent, model);
if (parentUI5Class !== undefined) {
- // If the parent class has a default aggregation, this could be a class. Otherwise it must be an aggregation.
+ // If the parent class doesn't have a default aggregation, it can only contain aggregations under it
if (parentUI5Class.defaultAggregation === undefined) {
return [
{
...issueDefaults,
- message: getMessage(
- UNKNOWN_AGGREGATION_IN_CLASS,
+ message: getUnknownAggregationMessage(
xmlElement.name,
- ui5NodeToFQN(parentUI5Class)
+ xmlElement.ns,
+ parentUI5Class,
+ xmlElement.parent.ns
),
},
];
} else {
+ // It could be a class or an aggregation
return [
{
...issueDefaults,
- message: getMessage(
- UNKNOWN_TAG_NAME_IN_CLASS,
+ message: getUnknownTagNameMessage(
xmlElement.name,
- ui5NodeToFQN(parentUI5Class)
+ ui5Namespace,
+ parentUI5Class
),
},
];
@@ -176,24 +201,78 @@ function validateTagWithoutNamespace(
);
if (parentUI5Aggregation !== undefined) {
// Only classes can appear under aggregations
- let message: string;
- if (ui5Namespace !== undefined) {
- message = getMessage(
- UNKNOWN_CLASS_IN_NS,
- xmlElement.name,
- ui5NodeToFQN(ui5Namespace)
- );
- } else {
- message = getMessage(UNKNOWN_CLASS_WITHOUT_NS, xmlElement.name);
- }
return [
{
...issueDefaults,
- message,
+ message: getUnknownClassMessage(xmlElement.name, ui5Namespace),
},
];
}
- // It might be an aggregation name under a custom class, so we don't give an error to avoid false positives
- return [];
+ // We don't know what the parent tag is.
+ // Since the tag doesn't have a namespace, and there is either a recognized default namespace or no default namespace,
+ // we can conclude that this tag is not a custom class, and that it's not an aggregation of a custom class
+ // (since aggregations must have the same namespace as their parent tag, and we know that the namespace is not custom).
+ return [
+ {
+ ...issueDefaults,
+ message: getUnknownTagNameMessage(
+ xmlElement.name,
+ ui5Namespace,
+ undefined
+ ),
+ },
+ ];
+}
+
+function getUnknownClassMessage(
+ name: string,
+ ui5Namespace: BaseUI5Node | undefined
+): string {
+ if (ui5Namespace !== undefined) {
+ return getMessage(UNKNOWN_CLASS_IN_NS, name, ui5NodeToFQN(ui5Namespace));
+ }
+ return getMessage(UNKNOWN_CLASS_WITHOUT_NS, name);
+}
+
+function getUnknownAggregationMessage(
+ name: string,
+ ns: string | undefined,
+ ui5Class: UI5Class,
+ classNS: string | undefined
+): string {
+ // Aggregations must be in the same namespace as the class
+ if (ns !== classNS) {
+ return getMessage(
+ UNKNOWN_AGGREGATION_IN_CLASS_DIFF_NAMESPACE,
+ name,
+ ui5NodeToFQN(ui5Class)
+ );
+ }
+ return getMessage(UNKNOWN_AGGREGATION_IN_CLASS, name, ui5NodeToFQN(ui5Class));
+}
+
+function getUnknownTagNameMessage(
+ name: string,
+ ui5Namespace: BaseUI5Node | undefined,
+ parentUI5Class: UI5Class | undefined
+): string {
+ if (parentUI5Class !== undefined && ui5Namespace !== undefined) {
+ return getMessage(
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
+ name,
+ ui5NodeToFQN(ui5Namespace),
+ ui5NodeToFQN(parentUI5Class)
+ );
+ } else if (parentUI5Class !== undefined && ui5Namespace === undefined) {
+ return getMessage(
+ UNKNOWN_TAG_NAME_IN_CLASS,
+ name,
+ ui5NodeToFQN(parentUI5Class)
+ );
+ } else if (parentUI5Class === undefined && ui5Namespace !== undefined) {
+ return getMessage(UNKNOWN_TAG_NAME_IN_NS, name, ui5NodeToFQN(ui5Namespace));
+ } else {
+ return getMessage(UNKNOWN_TAG_NAME_NO_NS, name);
+ }
}
diff --git a/packages/xml-views-validation/test/validators/attributes/unknown-attribute-key-spec.ts b/packages/xml-views-validation/test/validators/attributes/unknown-attribute-key-spec.ts
index 5071aa8f1..76932273a 100644
--- a/packages/xml-views-validation/test/validators/attributes/unknown-attribute-key-spec.ts
+++ b/packages/xml-views-validation/test/validators/attributes/unknown-attribute-key-spec.ts
@@ -72,9 +72,9 @@ describe("the unknown attribute name validation", () => {
`
-
+
-
+
`,
"Unknown attribute key: TYPO"
);
@@ -96,9 +96,9 @@ describe("the unknown attribute name validation", () => {
`
-
+
-
+
`,
"Unknown attribute key: TYPO"
);
@@ -130,9 +130,9 @@ describe("the unknown attribute name validation", () => {
`
-
+
-
+
`,
"Unknown attribute key: stashed"
);
@@ -143,9 +143,9 @@ describe("the unknown attribute name validation", () => {
`
-
+
-
+
`,
"Unknown attribute key: binding"
);
@@ -156,9 +156,9 @@ describe("the unknown attribute name validation", () => {
`
-
+
-
+
`,
"Unknown attribute key: class"
);
@@ -186,9 +186,9 @@ describe("the unknown attribute name validation", () => {
`
-
+
-
+
`,
"Unknown attribute key: _select"
);
@@ -227,9 +227,9 @@ describe("the unknown attribute name validation", () => {
-
+
-
+
`);
});
@@ -300,9 +300,9 @@ describe("the unknown attribute name validation", () => {
-
-
-
+
+
+
`);
});
@@ -310,9 +310,9 @@ describe("the unknown attribute name validation", () => {
assertNoIssues(`
-
+
-
+
`);
});
@@ -320,9 +320,9 @@ describe("the unknown attribute name validation", () => {
assertNoIssues(`
-
+
-
+
`);
});
});
@@ -334,8 +334,8 @@ describe("the unknown attribute name validation", () => {
-
-
+
+
`);
});
@@ -344,7 +344,7 @@ describe("the unknown attribute name validation", () => {
-
+
`);
});
@@ -353,7 +353,7 @@ describe("the unknown attribute name validation", () => {
-
+
`);
});
@@ -362,7 +362,7 @@ describe("the unknown attribute name validation", () => {
-
+
`);
});
@@ -370,8 +370,8 @@ describe("the unknown attribute name validation", () => {
assertNoIssues(`
-
-
+
+
`);
});
@@ -379,8 +379,8 @@ describe("the unknown attribute name validation", () => {
assertNoIssues(`
-
-
+
+
`);
});
});
@@ -391,8 +391,8 @@ describe("the unknown attribute name validation", () => {
-
-
+
+
`);
});
@@ -400,9 +400,9 @@ describe("the unknown attribute name validation", () => {
assertNoIssues(`
-
+
-
+
`);
});
diff --git a/packages/xml-views-validation/test/validators/element/unknown-tag-name-spec.ts b/packages/xml-views-validation/test/validators/element/unknown-tag-name-spec.ts
index 98d17d584..5e16fc97b 100644
--- a/packages/xml-views-validation/test/validators/element/unknown-tag-name-spec.ts
+++ b/packages/xml-views-validation/test/validators/element/unknown-tag-name-spec.ts
@@ -1,17 +1,24 @@
+import { expect } from "chai";
import { partial } from "lodash";
import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types";
import { generateModel } from "@ui5-language-assistant/test-utils";
import {
assertNoIssues as assertNoIssuesBase,
assertSingleIssue as assertSingleIssueBase,
+ testValidationsScenario,
+ computeExpectedRanges,
} from "../../test-utils";
import { validateUnknownTagName } from "../../../src/validators/elements/unknown-tag-name";
import {
getMessage,
UNKNOWN_CLASS_IN_NS,
UNKNOWN_CLASS_WITHOUT_NS,
- UNKNOWN_TAG_NAME_IN_CLASS,
UNKNOWN_AGGREGATION_IN_CLASS,
+ UNKNOWN_AGGREGATION_IN_CLASS_DIFF_NAMESPACE,
+ UNKNOWN_TAG_NAME_IN_CLASS,
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
+ UNKNOWN_TAG_NAME_IN_NS,
+ UNKNOWN_TAG_NAME_NO_NS,
} from "../../../src/utils/messages";
describe("the unknown tag name validation", () => {
@@ -68,7 +75,12 @@ describe("the unknown tag name validation", () => {
`,
- getMessage(UNKNOWN_CLASS_IN_NS, "Button_TYPO", "sap.m")
+ getMessage(
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
+ "Button_TYPO",
+ "sap.m",
+ "sap.m.SplitApp"
+ )
);
});
@@ -77,14 +89,101 @@ describe("the unknown tag name validation", () => {
`
-
+
<🢂m:Button_TYPO🢀>
-
+
`,
getMessage(UNKNOWN_CLASS_IN_NS, "Button_TYPO", "sap.m")
);
});
+
+ it("will detect an invalid aggregation when it's in the wrong namespace", () => {
+ assertSingleIssue(
+ `
+ <🢂m:content🢀>
+
+ `,
+ getMessage(UNKNOWN_CLASS_IN_NS, "content", "sap.m")
+ );
+ });
+
+ it("will detect an invalid class name under aggregation in the same namespace", () => {
+ assertSingleIssue(
+ `
+
+ <🢂mvc:Button_TYPO🢀>
+
+
+ `,
+ getMessage(UNKNOWN_CLASS_IN_NS, "Button_TYPO", "sap.ui.core.mvc")
+ );
+ });
+
+ it("will detect an invalid aggregation name under known class tag without default aggregation", () => {
+ assertSingleIssue(
+ `
+
+ <🢂m:content_TYPO🢀>
+
+
+ `,
+ getMessage(
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
+ "content_TYPO",
+ "sap.m",
+ "sap.m.SplitApp"
+ )
+ );
+ });
+
+ it("will detect an issue for unknown name under unknown class in a known namespace", () => {
+ const xmlSnippet = `
+
+ <🢂m:SplitApp_TYPO🢀>
+ <🢂m:Button_TYPO🢀>
+
+
+ `;
+ const expectedRanges = computeExpectedRanges(xmlSnippet);
+
+ testValidationsScenario({
+ model: ui5SemanticModel,
+ xmlText: xmlSnippet,
+ validators: { element: [validateUnknownTagName] },
+ assertion: (issues) => {
+ expect(issues).to.deep.equalInAnyOrder([
+ {
+ kind: "UnknownTagName",
+ message: getMessage(
+ UNKNOWN_CLASS_IN_NS,
+ "SplitApp_TYPO",
+ "sap.m"
+ ),
+ offsetRange: expectedRanges[0],
+ severity: "error",
+ },
+ {
+ kind: "UnknownTagName",
+ message: getMessage(
+ UNKNOWN_TAG_NAME_IN_NS,
+ "Button_TYPO",
+ "sap.m"
+ ),
+ offsetRange: expectedRanges[1],
+ severity: "error",
+ },
+ ]);
+ },
+ });
+ });
});
context("tag without namespace", () => {
@@ -101,9 +200,9 @@ describe("the unknown tag name validation", () => {
assertSingleIssue(
`
-
+
<🢂List🢀>
-
+
`,
getMessage(UNKNOWN_CLASS_WITHOUT_NS, "List")
);
@@ -123,23 +222,83 @@ describe("the unknown tag name validation", () => {
);
});
- it("will detect an invalid aggregation name under known class tag without default aggregation", () => {
+ it("will detect an invalid aggregation namespace under known class tag without default aggregation", () => {
assertSingleIssue(
`
- <🢂content_TYPO🢀>
-
+ <🢂content🢀>
+
`,
getMessage(
- UNKNOWN_AGGREGATION_IN_CLASS,
- "content_TYPO",
+ UNKNOWN_AGGREGATION_IN_CLASS_DIFF_NAMESPACE,
+ "content",
"sap.m.SplitApp"
)
);
});
+
+ it("will detect an issue for unknown name under unknown class in non-default non-ui5 namespace when name starts with uppercase", () => {
+ assertSingleIssue(
+ `
+
+ <🢂Button_TYPO🢀>
+
+
+ `,
+ getMessage(UNKNOWN_TAG_NAME_NO_NS, "Button_TYPO")
+ );
+ });
+ });
+
+ context("when default namespace is a ui5 namespace", () => {
+ it("will detect an issue for unknown name under unknown class in the default namespace", () => {
+ const xmlSnippet = `
+
+ <🢂SplitApp_TYPO🢀>
+ <🢂Button_TYPO🢀>
+
+
+ `;
+ const expectedRanges = computeExpectedRanges(xmlSnippet);
+
+ testValidationsScenario({
+ model: ui5SemanticModel,
+ xmlText: xmlSnippet,
+ validators: { element: [validateUnknownTagName] },
+ assertion: (issues) => {
+ expect(issues).to.deep.equalInAnyOrder([
+ {
+ kind: "UnknownTagName",
+ message: getMessage(
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
+ "SplitApp_TYPO",
+ "sap.m",
+ "sap.ui.core.mvc.View"
+ ),
+ offsetRange: expectedRanges[0],
+ severity: "error",
+ },
+ {
+ kind: "UnknownTagName",
+ message: getMessage(
+ UNKNOWN_TAG_NAME_IN_NS,
+ "Button_TYPO",
+ "sap.m"
+ ),
+ offsetRange: expectedRanges[1],
+ severity: "error",
+ },
+ ]);
+ },
+ });
+ });
});
});
@@ -172,8 +331,9 @@ describe("the unknown tag name validation", () => {
<🢂List_TYPO🢀>
`,
getMessage(
- UNKNOWN_TAG_NAME_IN_CLASS,
+ UNKNOWN_TAG_NAME_IN_NS_UNDER_CLASS,
"List_TYPO",
+ "sap.ui.core.mvc",
"sap.ui.core.mvc.View"
)
);
@@ -181,14 +341,14 @@ describe("the unknown tag name validation", () => {
it("will detect an invalid aggregation name under known class tag without default aggregation", () => {
assertSingleIssue(
- `
-
+ `
+
<🢂content_TYPO🢀>
-
- `,
+
+ `,
getMessage(
UNKNOWN_AGGREGATION_IN_CLASS,
"content_TYPO",
@@ -246,6 +406,17 @@ describe("the unknown tag name validation", () => {
);
});
+ it("will not detect an issue for known aggregation in a different namespace prefix that references the same namespace", () => {
+ assertNoIssues(
+ `
+
+
+ `
+ );
+ });
+
it("will not detect an issue for known class under class that has default aggregation", () => {
assertNoIssues(
` {
`
-
+
-
+
`
);
});
@@ -328,10 +499,10 @@ describe("the unknown tag name validation", () => {
`
-
+
-
+
`
);
});
@@ -366,8 +537,8 @@ describe("the unknown tag name validation", () => {
`
-
-
+
+
`
);
});
@@ -403,8 +574,8 @@ describe("the unknown tag name validation", () => {
xmlns:typo="sap.m_TYPO"
xmlns="sap.m">
-
-
+
+
`
);
@@ -425,10 +596,10 @@ describe("the unknown tag name validation", () => {
`
-
+
-
+
`
);
});
@@ -500,21 +671,8 @@ describe("the unknown tag name validation", () => {
xmlns:mvc="sap.ui.core.mvc"
xmlns:typo="sap.m_TYPO">
-
-
-
- `
- );
- });
-
- it("will not detect an issue for unknown name under unknown class in non-default non-ui5 namespace when name starts with uppercase", () => {
- assertNoIssues(
- `
-
-
-
+
+
`
);
diff --git a/packages/xml-views-validation/test/validators/element/use-of-deprecated-class-spec.ts b/packages/xml-views-validation/test/validators/element/use-of-deprecated-class-spec.ts
index daebe6437..d33edf956 100644
--- a/packages/xml-views-validation/test/validators/element/use-of-deprecated-class-spec.ts
+++ b/packages/xml-views-validation/test/validators/element/use-of-deprecated-class-spec.ts
@@ -144,8 +144,8 @@ describe("the use of deprecated class validation", () => {
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.ui.commons">
-
-
+
+
`;
testValidationsScenario({