From 481e631c0b7bc75c8a78649927057782f1810643 Mon Sep 17 00:00:00 2001 From: Tony Gorez Date: Wed, 15 May 2024 19:33:59 +0200 Subject: [PATCH 1/5] doc: add intialize and finalize for AstAnalyser.analyze API Signed-off-by: Tony Gorez --- README.md | 2 + test/AstAnalyser.spec.js | 80 ++++++++++++++++++++++++++++++++++++++-- types/api.d.ts | 2 + 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4cf9754..7b87711 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,8 @@ interface RuntimeOptions { module?: boolean; removeHTMLComments?: boolean; isMinified?: boolean; + initialize?: (sourceFile: SourceFile) => void; + finalize?: (sourceFile: SourceFile) => void; } ``` diff --git a/test/AstAnalyser.spec.js b/test/AstAnalyser.spec.js index 0c611e2..bf43205 100644 --- a/test/AstAnalyser.spec.js +++ b/test/AstAnalyser.spec.js @@ -174,9 +174,81 @@ describe("AstAnalyser", (t) => { assert.equal(result.warnings[0].kind, kWarningUnsafeDanger); assert.equal(result.warnings.length, 1); }); + + describe("intialize", () => { + const analyser = new AstAnalyser(); + it("should throw if initialize is not a function", () => { + assert.throws(() => { + analyser.analyse("const foo = 'bar';", { + initialize: "foo" + }); + }); + }); + + it("should call the initialize function", () => { + let hasBeenCalled = false; + const initialize = () => { + assert.strictEqual(hasBeenCalled, false); + hasBeenCalled = true; + }; + + analyser.analyse("const foo = 'bar';", { + initialize + }); + assert.strictEqual(hasBeenCalled, true); + }); + + it("should pass the source file as first argument", () => { + let hasBeenCalled = false; + const initialize = (source) => { + assert.strictEqual(source instanceof SourceFile, true); + }; + + analyser.analyse("const foo = 'bar';", { + initialize + }); + assert.strictEqual(hasBeenCalled, true); + }); + }); + + describe("finalize", () => { + const analyser = new AstAnalyser(); + it("should throw if finalize is not a function", () => { + assert.throws(() => { + analyser.analyse("const foo = 'bar';", { + finalize: "foo" + }); + }); + }); + + it("should call the finalize function", () => { + let hasBeenCalled = false; + const finalize = () => { + assert.strictEqual(hasBeenCalled, false); + hasBeenCalled = true; + }; + + analyser.analyse("const foo = 'bar';", { + finalize + }); + assert.strictEqual(hasBeenCalled, true); + }); + + it("should pass the source file as first argument", () => { + let hasBeenCalled = false; + const finalize = (source) => { + assert.strictEqual(source instanceof SourceFile, true); + }; + + analyser.analyse("const foo = 'bar';", { + finalize + }); + assert.strictEqual(hasBeenCalled, true); + }); + }); }); - it("remove the packageName from the dependencies list", async() => { + it("remove the packageName from the dependencies list", async () => { const result = await getAnalyser().analyseFile( new URL("depName.js", FIXTURE_URL), { module: false, packageName: "foobar" } @@ -189,7 +261,7 @@ describe("AstAnalyser", (t) => { ); }); - it("should fail with a parsing error", async() => { + it("should fail with a parsing error", async () => { const result = await getAnalyser().analyseFile( new URL("parsingError.js", FIXTURE_URL), { module: false, packageName: "foobar" } @@ -248,8 +320,8 @@ describe("AstAnalyser", (t) => { it("should remove multiple HTML comments", () => { const preparedSource = getAnalyser().prepareSource( "\nconst yo = 'foo'\n", { - removeHTMLComments: true - }); + removeHTMLComments: true + }); assert.strictEqual(preparedSource, "\nconst yo = 'foo'\n"); }); diff --git a/types/api.d.ts b/types/api.d.ts index 255ce7b..51d372f 100644 --- a/types/api.d.ts +++ b/types/api.d.ts @@ -53,6 +53,8 @@ interface RuntimeOptions { * @default false */ isMinified?: boolean; + initialize?: (sourceFile: SourceFile) => void; + finalize?: (sourceFile: SourceFile) => void; } interface RuntimeFileOptions extends Omit { From d610873755f6bbd3444d02bcdfbf4e8b6311de9b Mon Sep 17 00:00:00 2001 From: Tony Gorez Date: Wed, 15 May 2024 20:57:26 +0200 Subject: [PATCH 2/5] test: use nodejs api for mocks Signed-off-by: Tony Gorez --- test/AstAnalyser.spec.js | 43 ++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/test/AstAnalyser.spec.js b/test/AstAnalyser.spec.js index bf43205..3742505 100644 --- a/test/AstAnalyser.spec.js +++ b/test/AstAnalyser.spec.js @@ -177,6 +177,7 @@ describe("AstAnalyser", (t) => { describe("intialize", () => { const analyser = new AstAnalyser(); + it("should throw if initialize is not a function", () => { assert.throws(() => { analyser.analyse("const foo = 'bar';", { @@ -185,29 +186,24 @@ describe("AstAnalyser", (t) => { }); }); - it("should call the initialize function", () => { - let hasBeenCalled = false; - const initialize = () => { - assert.strictEqual(hasBeenCalled, false); - hasBeenCalled = true; - }; + it("should call the initialize function", (t) => { + const initialize = t.mock.fn(); analyser.analyse("const foo = 'bar';", { initialize }); - assert.strictEqual(hasBeenCalled, true); + + assert.strictEqual(initialize.mock.callCount(), 1); }); - it("should pass the source file as first argument", () => { - let hasBeenCalled = false; - const initialize = (source) => { - assert.strictEqual(source instanceof SourceFile, true); - }; + it("should pass the source file as first argument", (t) => { + const initialize = t.mock.fn(); analyser.analyse("const foo = 'bar';", { initialize }); - assert.strictEqual(hasBeenCalled, true); + + assert.strictEqual(initialize.mock.calls[0].arguments[0] instanceof SourceFile, true); }); }); @@ -221,29 +217,24 @@ describe("AstAnalyser", (t) => { }); }); - it("should call the finalize function", () => { - let hasBeenCalled = false; - const finalize = () => { - assert.strictEqual(hasBeenCalled, false); - hasBeenCalled = true; - }; + it("should call the finalize function", (t) => { + const finalize = t.mock.fn(); analyser.analyse("const foo = 'bar';", { finalize }); - assert.strictEqual(hasBeenCalled, true); + + assert.strictEqual(finalize.mock.callCount(), 1); }); - it("should pass the source file as first argument", () => { - let hasBeenCalled = false; - const finalize = (source) => { - assert.strictEqual(source instanceof SourceFile, true); - }; + it("should pass the source file as first argument", (t) => { + const finalize = t.mock.fn(); analyser.analyse("const foo = 'bar';", { finalize }); - assert.strictEqual(hasBeenCalled, true); + + assert.strictEqual(finalize.mock.calls[0].arguments[0] instanceof SourceFile, true); }); }); }); From ac965d1cee2558b5993ca195907eb281cde9ffaa Mon Sep 17 00:00:00 2001 From: Tony Gorez Date: Thu, 16 May 2024 11:05:20 +0200 Subject: [PATCH 3/5] feat: add initialize and finalize hooks for AstAnalyser.analyse Signed-off-by: Tony Gorez --- src/AstAnalyser.js | 19 +++++++++++++++++-- test/AstAnalyser.spec.js | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/AstAnalyser.js b/src/AstAnalyser.js index 15e8a72..9a497f1 100644 --- a/src/AstAnalyser.js +++ b/src/AstAnalyser.js @@ -31,15 +31,23 @@ export class AstAnalyser { const { isMinified = false, module = true, - removeHTMLComments = false + removeHTMLComments = false, + initialize, + finalize } = options; const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), { isEcmaScriptModule: Boolean(module) }); - const source = new SourceFile(str, this.probesOptions); + if (initialize) { + if (typeof initialize !== "function") { + throw new TypeError("options.initialize must be a function"); + } + initialize(source); + } + // we walk each AST Nodes, this is a purely synchronous I/O walk(body, { enter(node) { @@ -55,6 +63,13 @@ export class AstAnalyser { } }); + if (finalize) { + if (typeof finalize !== "function") { + throw new TypeError("options.initialize must be a function"); + } + finalize(source); + } + return { ...source.getResult(isMinified), dependencies: source.dependencies, diff --git a/test/AstAnalyser.spec.js b/test/AstAnalyser.spec.js index 3742505..e72de72 100644 --- a/test/AstAnalyser.spec.js +++ b/test/AstAnalyser.spec.js @@ -6,6 +6,7 @@ import { readFileSync } from "node:fs"; // Import Internal Dependencies import { AstAnalyser } from "../src/AstAnalyser.js"; import { JsSourceParser } from "../src/JsSourceParser.js"; +import { SourceFile } from "../src/SourceFile.js"; import { customProbes, getWarningKind, From 7e40185caa39584259c2a6d79095537ae75be72b Mon Sep 17 00:00:00 2001 From: Tony Gorez Date: Thu, 16 May 2024 11:25:03 +0200 Subject: [PATCH 4/5] doc: add example using hooks Signed-off-by: Tony Gorez --- docs/api/AstAnalyser.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/api/AstAnalyser.md b/docs/api/AstAnalyser.md index 557325f..a4a7dfe 100644 --- a/docs/api/AstAnalyser.md +++ b/docs/api/AstAnalyser.md @@ -64,3 +64,29 @@ type ReportOnFile = { warnings: Warning[]; } ``` + +## Examples + +### `initialize`/`finalize` Hooks + +The `analyse` method allows for the integration of two hooks: `initialize` and `finalize`. +These hooks are triggered before and after the analysis process, respectively. + +Below is an example of how to use these hooks within the `AstAnalyser` class: + +```js +import { AstAnalyser } from "@nodesecure/js-x-ray"; + +const scanner = new AstAnalyser(); + +scanner.analyse("const foo = 'bar';", { + initialize(sourceFile) { + // Code to execute before analysis starts + sourceFile.tracer.trace("Starting analysis..."); + }, + finalize(sourceFile) { + // Code to execute after analysis completes + console.log("Analysis complete."); + } +}); +``` From 40272f2975253436072d74693ccfccde4cbb6cd9 Mon Sep 17 00:00:00 2001 From: Tony Gorez Date: Thu, 16 May 2024 11:38:42 +0200 Subject: [PATCH 5/5] test: guarantee call order for hooks + reorder tests Signed-off-by: Tony Gorez --- test/AstAnalyser.spec.js | 86 +++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/test/AstAnalyser.spec.js b/test/AstAnalyser.spec.js index e72de72..f41b411 100644 --- a/test/AstAnalyser.spec.js +++ b/test/AstAnalyser.spec.js @@ -176,66 +176,80 @@ describe("AstAnalyser", (t) => { assert.equal(result.warnings.length, 1); }); - describe("intialize", () => { - const analyser = new AstAnalyser(); + describe("hooks", () => { + describe("initialize", () => { + const analyser = new AstAnalyser(); - it("should throw if initialize is not a function", () => { - assert.throws(() => { - analyser.analyse("const foo = 'bar';", { - initialize: "foo" + it("should throw if initialize is not a function", () => { + assert.throws(() => { + analyser.analyse("const foo = 'bar';", { + initialize: "foo" + }); }); }); - }); - it("should call the initialize function", (t) => { - const initialize = t.mock.fn(); + it("should call the initialize function", (t) => { + const initialize = t.mock.fn(); - analyser.analyse("const foo = 'bar';", { - initialize + analyser.analyse("const foo = 'bar';", { + initialize + }); + + assert.strictEqual(initialize.mock.callCount(), 1); }); - assert.strictEqual(initialize.mock.callCount(), 1); - }); + it("should pass the source file as first argument", (t) => { + const initialize = t.mock.fn(); - it("should pass the source file as first argument", (t) => { - const initialize = t.mock.fn(); + analyser.analyse("const foo = 'bar';", { + initialize + }); - analyser.analyse("const foo = 'bar';", { - initialize + assert.strictEqual(initialize.mock.calls[0].arguments[0] instanceof SourceFile, true); }); - - assert.strictEqual(initialize.mock.calls[0].arguments[0] instanceof SourceFile, true); }); - }); - describe("finalize", () => { - const analyser = new AstAnalyser(); - it("should throw if finalize is not a function", () => { - assert.throws(() => { - analyser.analyse("const foo = 'bar';", { - finalize: "foo" + describe("finalize", () => { + const analyser = new AstAnalyser(); + it("should throw if finalize is not a function", () => { + assert.throws(() => { + analyser.analyse("const foo = 'bar';", { + finalize: "foo" + }); }); }); - }); - it("should call the finalize function", (t) => { - const finalize = t.mock.fn(); + it("should call the finalize function", (t) => { + const finalize = t.mock.fn(); - analyser.analyse("const foo = 'bar';", { - finalize + analyser.analyse("const foo = 'bar';", { + finalize + }); + + assert.strictEqual(finalize.mock.callCount(), 1); }); - assert.strictEqual(finalize.mock.callCount(), 1); + it("should pass the source file as first argument", (t) => { + const finalize = t.mock.fn(); + + analyser.analyse("const foo = 'bar';", { + finalize + }); + + assert.strictEqual(finalize.mock.calls[0].arguments[0] instanceof SourceFile, true); + }); }); - it("should pass the source file as first argument", (t) => { - const finalize = t.mock.fn(); + it("intialize should be called before finalize", () => { + const calls = []; + const analyser = new AstAnalyser(); analyser.analyse("const foo = 'bar';", { - finalize + initialize: () => calls.push("initialize"), + finalize: () => calls.push("finalize") }); - assert.strictEqual(finalize.mock.calls[0].arguments[0] instanceof SourceFile, true); + assert.deepEqual(calls, ["initialize", "finalize"]); }); }); });