Skip to content

refactor(js-x-ray)!: migrate to TypeScript #364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"@openally/config.typescript": "^1.0.3",
"@types/node": "^24.0.2",
"c8": "^10.1.2",
"glob": "^11.0.0",
"iterator-matcher": "^2.1.0",
"tsx": "^4.20.3",
"typescript": "^5.8.3"
Expand Down
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"files": [],
"references": [
{
"path": "./workspaces/js-x-ray"
},
{
"path": "./workspaces/sec-literal"
},
Expand Down
16 changes: 12 additions & 4 deletions workspaces/js-x-ray/docs/AstAnalyser.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface AstAnalyserOptions {
/**
* @default false
*/
optionalWarnings?: boolean | Iterable<string>;
optionalWarnings?: boolean | Iterable<OptionalWarningName>;
}
```

Expand All @@ -47,13 +47,14 @@ class AstAnalyser {
options?: RuntimeOptions
) => Report;
analyseFile(
pathToFile: string,
pathToFile: string | URL,
options?: RuntimeFileOptions
): Promise<ReportOnFile>;
analyseFileSync(
pathToFile: string,
pathToFile: string | URL,
options?: RuntimeFileOptions
): ReportOnFile;
prepareSource(source: string, options?: PrepareSourceOptions): string
}
```

Expand Down Expand Up @@ -148,7 +149,14 @@ export const customProbes = [
main: (node, options) => {
const { sourceFile, data: calleeName } = options;
if (node.declarations[0].init.value === "danger") {
sourceFile.addWarning("unsafe-danger", calleeName, node.loc);
sourceFile.warnings.push({
kind: "unsafe-danger",
value: calleeName,
location: node.loc,
source: "JS-X-Ray Custom Probe",
i18n: "sast_warnings.unsafe-danger",
severity: "Warning"
});

return ProbeSignals.Skip;
}
Expand Down
12 changes: 7 additions & 5 deletions workspaces/js-x-ray/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@
"type": "module",
"exports": {
".": {
"import": "./src/index.js",
"types": "./src/index.d.ts"
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./warnings": {
"import": "./src/warnings.js",
"types": "./types/warnings.d.ts"
"import": "./dist/warnings.js",
"types": "./dist/warnings.d.ts"
},
"./package.json": "./package.json"
},
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"test-only": "glob -c \"node --test-reporter=spec --test\" \"./test/**/*.spec.js\"",
"prepublishOnly": "npm run build",
"build": "tsc",
"test-only": "tsx --test-reporter=spec --test \"./test/**/*.spec.ts\"",
"test": "c8 --all --src ./src -r html npm run test-only"
},
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,80 @@ import path from "node:path";

// Import Third-party Dependencies
import { walk } from "estree-walker";
import type { ESTree } from "meriyah";
import isMinified from "is-minified-code";

// Import Internal Dependencies
import { SourceFile } from "./SourceFile.js";
import { generateWarning, type Warning } from "./warnings.js";
import {
SourceFile,
type ProbesOptions,
type SourceFlags
} from "./SourceFile.js";
import { isOneLineExpressionExport } from "./utils/index.js";
import { JsSourceParser } from "./JsSourceParser.js";
import { JsSourceParser, type SourceParser } from "./JsSourceParser.js";

export class AstAnalyser {
export interface Dependency {
unsafe: boolean;
inTry: boolean;
location?: null | ESTree.SourceLocation;
}

export interface RuntimeOptions {
/**
* @default true
*/
module?: boolean;
/**
* @default false
*/
removeHTMLComments?: boolean;
/**
* @default false
*/
isMinified?: boolean;
initialize?: (sourceFile: SourceFile) => void;
finalize?: (sourceFile: SourceFile) => void;
}

export interface RuntimeFileOptions extends Omit<RuntimeOptions, "isMinified"> {
packageName?: string;
}

export interface Report {
dependencies: Map<string, Dependency>;
warnings: Warning[];
flags: Set<SourceFlags>;
idsLengthAvg: number;
stringScore: number;
}

export type ReportOnFile = {
ok: true;
warnings: Warning[];
dependencies: Map<string, Dependency>;
flags: Set<SourceFlags>;
} | {
ok: false;
warnings: Warning[];
};

export interface AstAnalyserOptions extends ProbesOptions {
/**
* @constructor
* @param {object} [options={}]
* @param {SourceParser} [options.customParser]
* @param {Array<object>} [options.customProbes]
* @param {boolean} [options.skipDefaultProbes=false]
* @param {boolean | Iterable<string>} [options.optionalWarnings=false]
* @default JsSourceParser
*/
constructor(options = {}) {
customParser?: SourceParser;
}

export interface PrepareSourceOptions {
removeHTMLComments?: boolean;
}

export class AstAnalyser {
parser: SourceParser;
probesOptions: ProbesOptions;

constructor(options: AstAnalyserOptions = {}) {
this.parser = options.customParser ?? new JsSourceParser();
this.probesOptions = {
customProbes: options.customProbes ?? [],
Expand All @@ -30,7 +87,10 @@ export class AstAnalyser {
};
}

analyse(str, options = Object.create(null)) {
analyse(
str: string,
options: RuntimeOptions = {}
): Report {
const {
isMinified = false,
module = true,
Expand All @@ -54,14 +114,15 @@ export class AstAnalyser {
}

// we walk each AST Nodes, this is a purely synchronous I/O
// @ts-expect-error
walk(body, {
enter(node) {
// Skip the root of the AST.
if (Array.isArray(node)) {
return;
}

const action = source.walk(node);
const action = source.walk(node as ESTree.Node);
if (action === "skip") {
this.skip();
}
Expand Down Expand Up @@ -90,9 +151,9 @@ export class AstAnalyser {
}

async analyseFile(
pathToFile,
options = {}
) {
pathToFile: string | URL,
options: RuntimeFileOptions = {}
): Promise<ReportOnFile> {
try {
const {
packageName = null,
Expand Down Expand Up @@ -130,20 +191,22 @@ export class AstAnalyser {
flags: data.flags
};
}
catch (error) {
catch (error: any) {
return {
ok: false,
warnings: [
{ kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
generateWarning("parsing-error", {
value: error.message
})
]
};
}
}

analyseFileSync(
pathToFile,
options = {}
) {
pathToFile: string | URL,
options: RuntimeFileOptions = {}
): ReportOnFile {
try {
const {
packageName = null,
Expand Down Expand Up @@ -181,22 +244,22 @@ export class AstAnalyser {
flags: data.flags
};
}
catch (error) {
catch (error: any) {
return {
ok: false,
warnings: [
{ kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
generateWarning("parsing-error", {
value: error.message
})
]
};
}
}

/**
* @param {!string} source
* @param {object} options
* @param {boolean} [options.removeHTMLComments=false]
*/
prepareSource(source, options = {}) {
prepareSource(
source: string,
options: PrepareSourceOptions = {}
): string {
if (typeof source !== "string") {
throw new TypeError("source must be a string");
}
Expand All @@ -214,11 +277,7 @@ export class AstAnalyser {
this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
}

/**
* @param {!string} str
* @returns {string}
*/
#removeHTMLComment(str) {
#removeHTMLComment(str: string): string {
return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
}
}
Loading