Skip to content

Commit d02cf1b

Browse files
authored
feat: add intialize and finalize for AstAnalyser.analyze API (#269)
Signed-off-by: Tony Gorez <[email protected]>
1 parent 12baaaf commit d02cf1b

File tree

5 files changed

+129
-6
lines changed

5 files changed

+129
-6
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ interface RuntimeOptions {
223223
module?: boolean;
224224
removeHTMLComments?: boolean;
225225
isMinified?: boolean;
226+
initialize?: (sourceFile: SourceFile) => void;
227+
finalize?: (sourceFile: SourceFile) => void;
226228
}
227229
```
228230

docs/api/AstAnalyser.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,29 @@ type ReportOnFile = {
6464
warnings: Warning[];
6565
}
6666
```
67+
68+
## Examples
69+
70+
### `initialize`/`finalize` Hooks
71+
72+
The `analyse` method allows for the integration of two hooks: `initialize` and `finalize`.
73+
These hooks are triggered before and after the analysis process, respectively.
74+
75+
Below is an example of how to use these hooks within the `AstAnalyser` class:
76+
77+
```js
78+
import { AstAnalyser } from "@nodesecure/js-x-ray";
79+
80+
const scanner = new AstAnalyser();
81+
82+
scanner.analyse("const foo = 'bar';", {
83+
initialize(sourceFile) {
84+
// Code to execute before analysis starts
85+
sourceFile.tracer.trace("Starting analysis...");
86+
},
87+
finalize(sourceFile) {
88+
// Code to execute after analysis completes
89+
console.log("Analysis complete.");
90+
}
91+
});
92+
```

src/AstAnalyser.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,23 @@ export class AstAnalyser {
3131
const {
3232
isMinified = false,
3333
module = true,
34-
removeHTMLComments = false
34+
removeHTMLComments = false,
35+
initialize,
36+
finalize
3537
} = options;
3638

3739
const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
3840
isEcmaScriptModule: Boolean(module)
3941
});
40-
4142
const source = new SourceFile(str, this.probesOptions);
4243

44+
if (initialize) {
45+
if (typeof initialize !== "function") {
46+
throw new TypeError("options.initialize must be a function");
47+
}
48+
initialize(source);
49+
}
50+
4351
// we walk each AST Nodes, this is a purely synchronous I/O
4452
walk(body, {
4553
enter(node) {
@@ -55,6 +63,13 @@ export class AstAnalyser {
5563
}
5664
});
5765

66+
if (finalize) {
67+
if (typeof finalize !== "function") {
68+
throw new TypeError("options.initialize must be a function");
69+
}
70+
finalize(source);
71+
}
72+
5873
return {
5974
...source.getResult(isMinified),
6075
dependencies: source.dependencies,

test/AstAnalyser.spec.js

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { readFileSync } from "node:fs";
66
// Import Internal Dependencies
77
import { AstAnalyser } from "../src/AstAnalyser.js";
88
import { JsSourceParser } from "../src/JsSourceParser.js";
9+
import { SourceFile } from "../src/SourceFile.js";
910
import {
1011
customProbes,
1112
getWarningKind,
@@ -174,9 +175,86 @@ describe("AstAnalyser", (t) => {
174175
assert.equal(result.warnings[0].kind, kWarningUnsafeDanger);
175176
assert.equal(result.warnings.length, 1);
176177
});
178+
179+
describe("hooks", () => {
180+
describe("initialize", () => {
181+
const analyser = new AstAnalyser();
182+
183+
it("should throw if initialize is not a function", () => {
184+
assert.throws(() => {
185+
analyser.analyse("const foo = 'bar';", {
186+
initialize: "foo"
187+
});
188+
});
189+
});
190+
191+
it("should call the initialize function", (t) => {
192+
const initialize = t.mock.fn();
193+
194+
analyser.analyse("const foo = 'bar';", {
195+
initialize
196+
});
197+
198+
assert.strictEqual(initialize.mock.callCount(), 1);
199+
});
200+
201+
it("should pass the source file as first argument", (t) => {
202+
const initialize = t.mock.fn();
203+
204+
analyser.analyse("const foo = 'bar';", {
205+
initialize
206+
});
207+
208+
assert.strictEqual(initialize.mock.calls[0].arguments[0] instanceof SourceFile, true);
209+
});
210+
});
211+
212+
describe("finalize", () => {
213+
const analyser = new AstAnalyser();
214+
it("should throw if finalize is not a function", () => {
215+
assert.throws(() => {
216+
analyser.analyse("const foo = 'bar';", {
217+
finalize: "foo"
218+
});
219+
});
220+
});
221+
222+
it("should call the finalize function", (t) => {
223+
const finalize = t.mock.fn();
224+
225+
analyser.analyse("const foo = 'bar';", {
226+
finalize
227+
});
228+
229+
assert.strictEqual(finalize.mock.callCount(), 1);
230+
});
231+
232+
it("should pass the source file as first argument", (t) => {
233+
const finalize = t.mock.fn();
234+
235+
analyser.analyse("const foo = 'bar';", {
236+
finalize
237+
});
238+
239+
assert.strictEqual(finalize.mock.calls[0].arguments[0] instanceof SourceFile, true);
240+
});
241+
});
242+
243+
it("intialize should be called before finalize", () => {
244+
const calls = [];
245+
const analyser = new AstAnalyser();
246+
247+
analyser.analyse("const foo = 'bar';", {
248+
initialize: () => calls.push("initialize"),
249+
finalize: () => calls.push("finalize")
250+
});
251+
252+
assert.deepEqual(calls, ["initialize", "finalize"]);
253+
});
254+
});
177255
});
178256

179-
it("remove the packageName from the dependencies list", async() => {
257+
it("remove the packageName from the dependencies list", async () => {
180258
const result = await getAnalyser().analyseFile(
181259
new URL("depName.js", FIXTURE_URL),
182260
{ module: false, packageName: "foobar" }
@@ -189,7 +267,7 @@ describe("AstAnalyser", (t) => {
189267
);
190268
});
191269

192-
it("should fail with a parsing error", async() => {
270+
it("should fail with a parsing error", async () => {
193271
const result = await getAnalyser().analyseFile(
194272
new URL("parsingError.js", FIXTURE_URL),
195273
{ module: false, packageName: "foobar" }
@@ -248,8 +326,8 @@ describe("AstAnalyser", (t) => {
248326
it("should remove multiple HTML comments", () => {
249327
const preparedSource = getAnalyser().prepareSource(
250328
"<!-- const yo = 5; -->\nconst yo = 'foo'\n<!-- const yo = 5; -->", {
251-
removeHTMLComments: true
252-
});
329+
removeHTMLComments: true
330+
});
253331

254332
assert.strictEqual(preparedSource, "\nconst yo = 'foo'\n");
255333
});

types/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ interface RuntimeOptions {
5353
* @default false
5454
*/
5555
isMinified?: boolean;
56+
initialize?: (sourceFile: SourceFile) => void;
57+
finalize?: (sourceFile: SourceFile) => void;
5658
}
5759

5860
interface RuntimeFileOptions extends Omit<RuntimeOptions, "isMinified"> {

0 commit comments

Comments
 (0)