Skip to content

Commit

Permalink
feat: Add detection for deprecated dependencies in .library (#104)
Browse files Browse the repository at this point in the history
JIRA: CPOUI5FOUNDATION-825

+refactor: Add common `xmlParser`
+refactor: Remove `htmlParser`

The `Lib.init()` call detection is addressed in the following PR:
#197

---------

Co-authored-by: Yavor Ivanov <yavor.ivanov@sap.com>
  • Loading branch information
maxreichmann and d3xter666 authored Jul 30, 2024
1 parent 5ac0984 commit 161f157
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 46 deletions.
83 changes: 83 additions & 0 deletions src/linter/dotLibrary/DotLibraryLinter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {LintMessageSeverity} from "../LinterContext.js";
import LinterContext from "../LinterContext.js";
import {deprecatedLibraries} from "../../utils/deprecations.js";
import {SaxEventType, Tag as SaxTag} from "sax-wasm";
import {parseXML} from "../../utils/xmlParser.js";
import {ReadStream} from "node:fs";
import {RULES, MESSAGES, formatMessage} from "../linterReporting.js";

export default class DotLibraryLinter {
#contentStream;
#resourcePath;
#context: LinterContext;

constructor(resourcePath: string, contentStream: ReadStream, context: LinterContext) {
this.#contentStream = contentStream;
this.#resourcePath = resourcePath;
this.#context = context;
}

async lint() {
try {
const dotLibraryDependencyTags = await this.#parseDotLibrary(this.#contentStream);
this.#analyzeDeprecatedLibs(dotLibraryDependencyTags);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.#context.addLintingMessage(this.#resourcePath, {
severity: LintMessageSeverity.Error,
message,
ruleId: RULES["ui5-linter-parsing-error"],
fatal: true,
});
}
}

async #parseDotLibrary(contentStream: ReadStream): Promise<SaxTag[]> {
const libs = new Set();
const tagsStack: string[] = [];
const libNamePath = ["library", "dependencies", "dependency"];
await parseXML(contentStream, (event, tag) => {
if (!(tag instanceof SaxTag)) {
return;
}

if (event === SaxEventType.OpenTag && !tag.selfClosing) {
tagsStack.push(tag.value);
} else if (event === SaxEventType.CloseTag && !tag.selfClosing) {
tagsStack.pop();
}

if (event === SaxEventType.CloseTag &&
tag.value === "libraryName") {
const isMatchingPath = libNamePath.length === tagsStack.length &&
libNamePath.every((lib, index) => lib === tagsStack[index]);

if (isMatchingPath) {
libs.add(tag);
}
}
});

return Array.from(libs) as SaxTag[];
}

#analyzeDeprecatedLibs(libs: SaxTag[]) {
// Check for deprecated libraries
libs.forEach((lib) => {
const {line, character: column} = lib.openStart;
// textNodes is always an array, but it might be empty
const libName = lib.textNodes[0]?.value;

if (deprecatedLibraries.includes(libName)) {
this.#context.addLintingMessage(this.#resourcePath, {
ruleId: RULES["ui5-linter-no-deprecated-library"],
severity: LintMessageSeverity.Error,
fatal: undefined,
line: line + 1,
column: column + 1,
message: formatMessage(MESSAGES.SHORT__DEPRECATED_LIBRARY, libName),
});
}
});
}
}
28 changes: 28 additions & 0 deletions src/linter/dotLibrary/linter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {LinterParameters} from "../LinterContext.js";
import DotLibraryLinter from "./DotLibraryLinter.js";
import {Resource} from "@ui5/fs";

export default async function lintDotLibrary({context, workspace}: LinterParameters) {
let dotLibraryResources: Resource[];
const pathsToLint = context.getPathsToLint();
if (pathsToLint?.length) {
dotLibraryResources = [];
await Promise.all(pathsToLint.map(async (resourcePath) => {
if (!resourcePath.endsWith(".library")) {
return;
}
const resource = await workspace.byPath(resourcePath);
if (!resource) {
throw new Error(`Resource not found: ${resourcePath}`);
}
dotLibraryResources.push(resource);
}));
} else {
dotLibraryResources = await workspace.byGlob("**/.library");
}

await Promise.all(dotLibraryResources.map(async (resource: Resource) => {
const linter = new DotLibraryLinter(resource.getPath(), resource.getStream(), context);
await linter.lint();
}));
}
49 changes: 3 additions & 46 deletions src/linter/html/parser.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,11 @@
import type {ReadStream} from "node:fs";
import {Detail, SaxEventType, SAXParser, Tag as SaxTag} from "sax-wasm";
import {finished} from "node:stream/promises";
import fs from "node:fs/promises";
import {createRequire} from "node:module";
const require = createRequire(import.meta.url);

let saxWasmBuffer: Buffer;
async function initSaxWasm() {
if (!saxWasmBuffer) {
const saxPath = require.resolve("sax-wasm/lib/sax-wasm.wasm");
saxWasmBuffer = await fs.readFile(saxPath);
}

return saxWasmBuffer;
}

async function parseHtml(contentStream: ReadStream, parseHandler: (type: SaxEventType, tag: Detail) => void) {
const options = {highWaterMark: 32 * 1024}; // 32k chunks
const saxWasmBuffer = await initSaxWasm();
const saxParser = new SAXParser(SaxEventType.CloseTag, options);

saxParser.eventHandler = parseHandler;

// Instantiate and prepare the wasm for parsing
if (!await saxParser.prepareWasm(saxWasmBuffer)) {
throw new Error("Unknown error during WASM Initialization");
}

// stream from a file in the current directory
contentStream.on("data", (chunk: Uint8Array) => {
try {
saxParser.write(chunk);
} catch (err) {
if (err instanceof Error) {
// In case of an error, destroy the content stream to make the
// error bubble up to our callers
contentStream.destroy(err);
} else {
throw err;
}
}
});
await finished(contentStream);
saxParser.end();
}
import {SaxEventType, Tag as SaxTag} from "sax-wasm";
import {parseXML} from "../../utils/xmlParser.js";

export async function extractJSScriptTags(contentStream: ReadStream) {
const scriptTags: SaxTag[] = [];

await parseHtml(contentStream, (event, tag) => {
await parseXML(contentStream, (event, tag) => {
if (tag instanceof SaxTag &&
event === SaxEventType.CloseTag &&
tag.value === "script") {
Expand Down
2 changes: 2 additions & 0 deletions src/linter/lintWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import lintXml from "./xmlTemplate/linter.js";
import lintJson from "./manifestJson/linter.js";
import lintHtml from "./html/linter.js";
import lintUI5Yaml from "./yaml/linter.js";
import lintDotLibrary from "./dotLibrary/linter.js";
import {taskStart} from "../utils/perf.js";
import TypeLinter from "./ui5Types/TypeLinter.js";
import LinterContext, {LintResult, LinterParameters, LinterOptions} from "./LinterContext.js";
Expand All @@ -22,6 +23,7 @@ export default async function lintWorkspace(
lintJson(params),
lintHtml(params),
lintUI5Yaml(params),
lintDotLibrary(params),
]);

const typeLinter = new TypeLinter(params);
Expand Down
46 changes: 46 additions & 0 deletions src/utils/xmlParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type {ReadStream} from "node:fs";
import {Detail, SaxEventType, SAXParser} from "sax-wasm";
import {finished} from "node:stream/promises";
import fs from "node:fs/promises";
import {createRequire} from "node:module";
const require = createRequire(import.meta.url);

let saxWasmBuffer: Buffer;
async function initSaxWasm() {
if (!saxWasmBuffer) {
const saxPath = require.resolve("sax-wasm/lib/sax-wasm.wasm");
saxWasmBuffer = await fs.readFile(saxPath);
}

return saxWasmBuffer;
}

export async function parseXML(contentStream: ReadStream, parseHandler: (type: SaxEventType, tag: Detail) => void) {
const options = {highWaterMark: 32 * 1024}; // 32k chunks
const saxWasmBuffer = await initSaxWasm();
const saxParser = new SAXParser(SaxEventType.CloseTag + SaxEventType.OpenTag, options);

saxParser.eventHandler = parseHandler;

// Instantiate and prepare the wasm for parsing
if (!await saxParser.prepareWasm(saxWasmBuffer)) {
throw new Error("Unknown error during WASM Initialization");
}

// stream from a file in the current directory
contentStream.on("data", (chunk: Uint8Array) => {
try {
saxParser.write(chunk);
} catch (err) {
if (err instanceof Error) {
// In case of an error, destroy the content stream to make the
// error bubble up to our callers
contentStream.destroy(err);
} else {
throw err;
}
}
});
await finished(contentStream);
saxParser.end();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,31 @@
<vendor>SAP SE</vendor>
<version>${version}</version>
<copyright>${copyright}</copyright>

<libraryName>sap.ui.ca</libraryName> <!-- Invalid one. Not nested properly -->
<dependency>
<libraryName>sap.ui.ca</libraryName> <!-- Invalid one. Not nested properly -->
</dependency>

<dependencies>
<libraryName>sap.ui.ca</libraryName> <!-- Invalid one. Not nested properly -->
<dependency>
<libraryName></libraryName>
</dependency>
<dependency>
<libraryName> </libraryName>
</dependency>
<dependency>
<libraryName>sap.ui.core</libraryName>
</dependency>
<dependency>
<libraryName>sap.ca.scfld.md</libraryName>
</dependency>
<dependency>
<libraryName>sap.ca.scfld.md</libraryName>
</dependency>
<dependency>
<libraryName>sap.ca.ui</libraryName>
</dependency>
</dependencies>
</library>
33 changes: 33 additions & 0 deletions test/lib/linter/snapshots/linter.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,39 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1
[
{
coverageInfo: [],
errorCount: 3,
fatalErrorCount: 0,
filePath: 'src/main/js/.library',
messages: [
{
column: 4,
fatal: undefined,
line: 25,
message: 'Use of deprecated library \'sap.ca.scfld.md\'',
ruleId: 'ui5-linter-no-deprecated-library',
severity: 2,
},
{
column: 4,
fatal: undefined,
line: 28,
message: 'Use of deprecated library \'sap.ca.scfld.md\'',
ruleId: 'ui5-linter-no-deprecated-library',
severity: 2,
},
{
column: 4,
fatal: undefined,
line: 31,
message: 'Use of deprecated library \'sap.ca.ui\'',
ruleId: 'ui5-linter-no-deprecated-library',
severity: 2,
},
],
warningCount: 0,
},
{
coverageInfo: [],
errorCount: 0,
Expand Down
Binary file modified test/lib/linter/snapshots/linter.ts.snap
Binary file not shown.
50 changes: 50 additions & 0 deletions test/lib/utils/xmlParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import test from "ava";
import {parseXML} from "../../../src/utils/xmlParser.js";
import {ReadStream} from "node:fs";
import {Readable} from "node:stream";
import {SaxEventType, Tag as SaxTag} from "sax-wasm";

test("Test xmlParser with .library", async (t) => {
const sampleDotLibrary = `<?xml version="1.0" ?>
<library xmlns="http://www.sap.com/sap.ui.library.xsd">
<name>library.with.custom.paths</name>
<vendor>SAP SE</vendor>
<version>1.0</version>
<copyright>any</copyright>
<dependencies>
<dependency>
<libraryName>sap.ui.core</libraryName>
</dependency>
<dependency>
<libraryName>sap.ca.scfld.md</libraryName>
</dependency>
<dependency>
<libraryName>sap.ca.scfld.md</libraryName>
</dependency>
<dependency>
<libraryName>sap.ca.ui</libraryName>
</dependency>
</dependencies>
</library>`;

// Convert raw .library content into stream
const contentStream = new Readable() as ReadStream;
// eslint-disable-next-line @typescript-eslint/no-empty-function
contentStream._read = () => {};
contentStream.push(sampleDotLibrary);
contentStream.push(null);

// Call SAXParser with the contentStream
const libs: SaxTag[] = [];
await parseXML(contentStream, (event, tag) => {
if (tag instanceof SaxTag &&
event === SaxEventType.CloseTag &&
tag.value === "libraryName") {
libs.push(tag);
}
});

// Test parsed results
t.is(libs.length, 4, "Parsed .library XML should contain 4 libraries");
t.is(libs[0].textNodes[0].value, "sap.ui.core", "First library should be 'sap.ui.core'");
});

0 comments on commit 161f157

Please sign in to comment.