Skip to content

Commit f109686

Browse files
authored
feat: add validation for unknown namespace name in xmlns attribute value (#103)
1 parent 66f78a5 commit f109686

File tree

21 files changed

+454
-38
lines changed

21 files changed

+454
-38
lines changed

packages/language-server/src/xml-view-diagnostics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function validationIssuesToLspDiagnostics(
4949
const issueKind = currIssue.kind;
5050
switch (issueKind) {
5151
case "UnknownEnumValue":
52+
case "UnknownNamespaceInXmlnsAttributeValue":
5253
return {
5354
...commonDiagnosticPros,
5455
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<mvc:View
2+
xmlns:mvc="sap.ui.core.mvc"
3+
xmlns=🢂"sap.m.abcde"🢀>
4+
</List>
5+
</mvc:View>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[
2+
{
3+
"range": {
4+
"start": { "line": 2, "character": 14 },
5+
"end": { "line": 2, "character": 27 }
6+
},
7+
"severity": 2,
8+
"source": "UI5 Language Assistant",
9+
"message": "Unknown namespace: \"sap.m.abcde\""
10+
}
11+
]

packages/logic-utils/api.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,15 @@ export function getRootSymbolParent(node: BaseUI5Node): BaseUI5Node | undefined;
8383
* Return a human-readable string representation of a UI5 type
8484
*/
8585
export function typeToString(type: UI5Type | undefined): string;
86+
87+
/**
88+
* Check if an xml attribute key is an xmlns key
89+
* @param key
90+
*/
91+
export function isXMLNamespaceKey(key: string): boolean;
92+
93+
/**
94+
* Get the attribute name, without its "xmlns:" prefix, from an xmlns attribute key
95+
* @param key
96+
*/
97+
export function getXMLNamespaceKeyPrefix(key: string): string;

packages/logic-utils/src/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ export {
1111
export { findClassesMatchingType } from "./utils/find-classes-matching-type";
1212
export { isRootSymbol, getRootSymbolParent } from "./utils/root-symbols";
1313
export { typeToString } from "./utils/type-to-string";
14+
export {
15+
getXMLNamespaceKeyPrefix,
16+
isXMLNamespaceKey,
17+
} from "./utils/xml-ns-key";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// The xml parser takes care of validating the attribute name.
2+
// If the user started the attribute name with "xmlns:" we can assume that
3+
// they meant for it to be an xml namespace attribute.
4+
// xmlns attributes explicitly can't contain ":" after the "xmlns:" part.
5+
const namespaceRegex = /^xmlns(:(?<prefix>[^:=]*))?$/;
6+
7+
export function isXMLNamespaceKey(key: string): boolean {
8+
return key.match(namespaceRegex) !== null;
9+
}
10+
11+
export function getXMLNamespaceKeyPrefix(key: string): string {
12+
const matchArr = key.match(namespaceRegex);
13+
return matchArr?.groups?.prefix ?? "";
14+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect } from "chai";
2+
import { isXMLNamespaceKey, getXMLNamespaceKeyPrefix } from "../../src/api";
3+
4+
describe("The @ui5-language-assistant/logic-utils <isXMLNamespaceKey> function", () => {
5+
it("will return true for attribute name starting with xmlns:", () => {
6+
expect(isXMLNamespaceKey("xmlns:a")).to.be.true;
7+
});
8+
9+
it("will return true for attribute name starting with xmlns: that contains dots", () => {
10+
expect(isXMLNamespaceKey("xmlns:a.b.c")).to.be.true;
11+
});
12+
13+
it("will return true for the default namespace attribute", () => {
14+
expect(isXMLNamespaceKey("xmlns")).to.be.true;
15+
});
16+
17+
it("will return true for xmlns attribute without a name", () => {
18+
// Note: this is supported for the case of code completion
19+
expect(isXMLNamespaceKey("xmlns:")).to.be.true;
20+
});
21+
22+
it("will return false for the non-xmlns attribute", () => {
23+
expect(isXMLNamespaceKey("abc")).to.be.false;
24+
});
25+
26+
it("will return false for the non-xmlns attribute that starts with xmlns", () => {
27+
expect(isXMLNamespaceKey("xmlnst")).to.be.false;
28+
});
29+
});
30+
31+
describe("The @ui5-language-assistant/logic-utils <getNamespaceKeyPrefix> function", () => {
32+
it("will return the name without xmlns prefix for xmlns attribute with name", () => {
33+
expect(getXMLNamespaceKeyPrefix("xmlns:abc")).to.eql("abc");
34+
});
35+
36+
it("will return the name without xmlns prefix for xmlns attribute with name that contains dots", () => {
37+
expect(getXMLNamespaceKeyPrefix("xmlns:abc.efg")).to.eql("abc.efg");
38+
});
39+
40+
it("will return empty string when key does not start with xmlns", () => {
41+
expect(getXMLNamespaceKeyPrefix("abc")).to.be.empty;
42+
});
43+
44+
it("will return empty string when symbol '*' goes after xmlns", () => {
45+
expect(getXMLNamespaceKeyPrefix("xmlns*")).to.be.empty;
46+
});
47+
48+
it("will return empty string when prefix is undefined", () => {
49+
expect(getXMLNamespaceKeyPrefix("xmlns:")).to.be.empty;
50+
});
51+
});

packages/semantic-model-types/api.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface UI5SemanticModel {
22
version?: string;
3+
includedLibraries: string[];
34
classes: Record<string, UI5Class>;
45
enums: Record<string, UI5Enum>;
56
namespaces: Record<string, UI5Namespace>;

packages/semantic-model/api.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,13 @@ export function forEachSymbol(
3232
model: UI5SemanticModel,
3333
iteratee: (symbol: BaseUI5Node, fqn: string) => boolean | void
3434
): void;
35+
36+
/**
37+
* Return a root symbol according to its fully qualified name, or undefined if not found
38+
* @param model
39+
* @param fqn
40+
*/
41+
export function findSymbol(
42+
model: UI5SemanticModel,
43+
fqn: string
44+
): BaseUI5Node | undefined;

packages/semantic-model/src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ export function generate({
3838
return deepFreezeStrict(model);
3939
}
4040

41-
export { forEachSymbol } from "./utils";
41+
export { forEachSymbol, findSymbol } from "./utils";

packages/semantic-model/src/convert.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function convertToSemanticModel(
2323
): model.UI5SemanticModel {
2424
const model: model.UI5SemanticModel = {
2525
version: "",
26+
includedLibraries: [],
2627
classes: newMap(),
2728
enums: newMap(),
2829
functions: newMap(),
@@ -37,6 +38,7 @@ export function convertToSemanticModel(
3738
fileContent,
3839
}));
3940
const sortedLibs = sortBy(libsArray, "libraryName");
41+
model.includedLibraries = map(sortedLibs, (_) => _.libraryName);
4042

4143
reduce(
4244
sortedLibs,
@@ -77,6 +79,7 @@ function convertLibraryToSemanticModel(
7779
): model.UI5SemanticModel {
7880
const model: model.UI5SemanticModel = {
7981
version: lib.version,
82+
includedLibraries: [],
8083
classes: newMap(),
8184
interfaces: newMap(),
8285
enums: newMap(),

packages/semantic-model/test/unit-spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,69 @@ context("The ui5-language-assistant semantic model package unit tests", () => {
253253
});
254254
});
255255
});
256+
257+
context("includedLibraries", () => {
258+
function generateFromLibraries(
259+
libraries: Record<string, unknown>
260+
): UI5SemanticModel {
261+
const result = generate({
262+
version: "1.74.0",
263+
strict: false,
264+
typeNameFix: {},
265+
libraries: libraries,
266+
printValidationErrors: false,
267+
});
268+
return result;
269+
}
270+
271+
const libWithSymbol = {
272+
"$schema-ref": "http://schemas.sap.com/sapui5/designtime/api.json/1.0",
273+
symbols: [
274+
{
275+
kind: "class",
276+
basename: "rootSymbol",
277+
name: "rootSymbol",
278+
},
279+
],
280+
};
281+
282+
it("contains a valid library", () => {
283+
const result = generateFromLibraries({
284+
testLib: libWithSymbol,
285+
});
286+
expect(result.includedLibraries, "includedLibraries").to.deep.equal([
287+
"testLib",
288+
]);
289+
});
290+
291+
it("contains an invalid library object", () => {
292+
const result = generateFromLibraries({
293+
testLib: {},
294+
});
295+
expect(result.includedLibraries, "includedLibraries").to.deep.equal([
296+
"testLib",
297+
]);
298+
});
299+
300+
it("contains an empty library", () => {
301+
const result = generateFromLibraries({
302+
testLib: "",
303+
});
304+
expect(result.includedLibraries, "includedLibraries").to.deep.equal([
305+
"testLib",
306+
]);
307+
});
308+
309+
it("contains all sent libraries", () => {
310+
const result = generateFromLibraries({
311+
testLib: libWithSymbol,
312+
lib2: libWithSymbol,
313+
emptyLib: {},
314+
});
315+
expect(
316+
result.includedLibraries,
317+
"includedLibraries"
318+
).to.deep.equalInAnyOrder(["testLib", "lib2", "emptyLib"]);
319+
});
320+
});
256321
});

packages/xml-views-completion/src/providers/attributeName/namespace.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ import { UI5Namespace } from "@ui5-language-assistant/semantic-model-types";
33
import {
44
isElementSubClass,
55
ui5NodeToFQN,
6+
getXMLNamespaceKeyPrefix,
7+
isXMLNamespaceKey,
68
} from "@ui5-language-assistant/logic-utils";
79
import { XMLAttribute } from "@xml-tools/ast";
810
import { UI5AttributeNameCompletionOptions } from "./index";
911
import { UI5NamespacesInXMLAttributeKeyCompletion } from "../../../api";
10-
import {
11-
getXMLNamespaceKeyPrefix,
12-
isXMLNamespaceKey,
13-
} from "../utils/xml-ns-key";
1412

1513
/**
1614
* Suggests Namespaces inside Element

packages/xml-views-completion/src/providers/attributeValue/namespace.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ import { XMLAttribute } from "@xml-tools/ast";
33
import {
44
isElementSubClass,
55
ui5NodeToFQN,
6+
getXMLNamespaceKeyPrefix,
7+
isXMLNamespaceKey,
68
} from "@ui5-language-assistant/logic-utils";
79
import { UI5AttributeValueCompletionOptions } from "./index";
810
import { UI5NamespacesInXMLAttributeValueCompletion } from "../../../api";
9-
import {
10-
getXMLNamespaceKeyPrefix,
11-
isXMLNamespaceKey,
12-
} from "../utils/xml-ns-key";
1311

1412
/**
1513
* Suggests namespace value for namespace attribute

packages/xml-views-completion/src/providers/utils/xml-ns-key.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

packages/xml-views-completion/test/providers/attributeName/namespace-spec.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
namespaceKeysSuggestions,
2323
} from "../../../src/providers/attributeName/namespace";
2424
import { ui5NodeToFQN } from "@ui5-language-assistant/logic-utils";
25-
import { getXMLNamespaceKeyPrefix } from "../../../src/providers/utils/xml-ns-key";
2625
import { UI5NamespacesInXMLAttributeKeyCompletion } from "../../../api";
2726

2827
const allExpectedNamespaces = [
@@ -300,20 +299,6 @@ describe("The ui5-language-assistant xml-views-completion", () => {
300299
});
301300

302301
context("not reproducible scenario", () => {
303-
context("getNamespaceKeyPrefix", () => {
304-
it("no match is found, because key does not start with xmlns", () => {
305-
expect(getXMLNamespaceKeyPrefix("abc")).to.be.empty;
306-
});
307-
308-
it("no match is found because symbol '*' goes after xmlns", () => {
309-
expect(getXMLNamespaceKeyPrefix("xmlns*")).to.be.empty;
310-
});
311-
312-
it("prefix is undefined, empty string returns", () => {
313-
expect(getXMLNamespaceKeyPrefix("xmlns:")).to.be.empty;
314-
});
315-
});
316-
317302
//TODO check with Shachar if this case can be received from xml
318303
context("isExistingNamespaceAttribute", () => {
319304
it("invalid attribute key", () => {

packages/xml-views-validation/api.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ export interface OffsetRange {
2020
end: number;
2121
}
2222

23-
export type UI5XMLViewIssue = UnknownEnumValueIssue | UseOfDeprecatedClassIssue;
23+
export type UI5XMLViewIssue =
24+
| UnknownEnumValueIssue
25+
| UseOfDeprecatedClassIssue
26+
| UnknownNamespaceInXmlnsAttributeValueIssue;
2427

2528
// A sub-interface per issue type may seem redundant, but this allows
2629
// a sub-issue type to have additional properties (if needed) in the future.
@@ -31,3 +34,8 @@ export interface UnknownEnumValueIssue extends BaseUI5XMLViewIssue {
3134
export interface UseOfDeprecatedClassIssue extends BaseUI5XMLViewIssue {
3235
kind: "UseOfDeprecatedClass";
3336
}
37+
38+
export interface UnknownNamespaceInXmlnsAttributeValueIssue
39+
extends BaseUI5XMLViewIssue {
40+
kind: "UnknownNamespaceInXmlnsAttributeValue";
41+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { XMLAttribute } from "@xml-tools/ast";
2+
import { UI5SemanticModel } from "@ui5-language-assistant/semantic-model-types";
3+
import { findSymbol } from "@ui5-language-assistant/semantic-model";
4+
import { isXMLNamespaceKey } from "@ui5-language-assistant/logic-utils";
5+
import { UnknownNamespaceInXmlnsAttributeValueIssue } from "../../../api";
6+
import { find } from "lodash";
7+
8+
export function validateUnknownXmlnsNamespace(
9+
attribute: XMLAttribute,
10+
model: UI5SemanticModel
11+
): UnknownNamespaceInXmlnsAttributeValueIssue[] {
12+
const attributeName = attribute.key;
13+
if (attributeName === null || !isXMLNamespaceKey(attributeName)) {
14+
return [];
15+
}
16+
17+
const attributeValue = attribute.value;
18+
const attributeValueToken = attribute.syntax.value;
19+
20+
// TODO empty namespaces aren't valid but this should be handled in xml-tools because it's a general xml issue.
21+
if (attributeValueToken === undefined || attributeValue === null) {
22+
return [];
23+
}
24+
25+
// Only check namespaces from libraries.
26+
// There are valid namespaces like some that start with "http" that should not return an error.
27+
// Additionally, customers can develop in custom namespaces, even those that start with "sap", and we don't want to give false positives.
28+
// But sap library namespaces can be considered reserved.
29+
if (
30+
find(
31+
model.includedLibraries,
32+
(_) => attributeValue === _ || attributeValue.startsWith(_ + ".")
33+
) === undefined
34+
) {
35+
return [];
36+
}
37+
38+
// Find the namespace. In most cases it would actually be a namespace but some classes are defined inside other things
39+
// (e.g. sap.gantt.legend which is an Enum in 1.71.*)
40+
if (findSymbol(model, attributeValue) === undefined) {
41+
return [
42+
{
43+
kind: "UnknownNamespaceInXmlnsAttributeValue",
44+
message: `Unknown namespace: ${attributeValueToken.image}`,
45+
offsetRange: {
46+
start: attributeValueToken.startOffset,
47+
end: attributeValueToken.endOffset,
48+
},
49+
severity: "warn",
50+
},
51+
];
52+
}
53+
54+
return [];
55+
}

0 commit comments

Comments
 (0)