Skip to content

Commit

Permalink
refactor: runASTAnalysis functions to use class AstAnalyser (#216)
Browse files Browse the repository at this point in the history
* refactor: create and use AstAnalyser class to handle ast analysis

* refactor: remove useless JSDoc

* fix: ESTree type from meriyah

* fix: add JSDoc again

* fix: optimize regular expression by removing non-capturing group

* fix: revert regex to the original pattern

* fix: declare class AstAnalyser instead of interface

* fix: replace Parser type by SourceParser type

* fix: parsers should return body directly
  • Loading branch information
jean-michelet authored Jan 28, 2024
1 parent 83aa3d5 commit e59a1a5
Show file tree
Hide file tree
Showing 12 changed files with 472 additions and 352 deletions.
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
AstAnalyser,
SourceParser,
runASTAnalysis,
runASTAnalysisOnFile,
Report,
Expand All @@ -20,6 +22,8 @@ declare const warnings: Record<WarningName, Pick<WarningDefault, "experimental"

export {
warnings,
AstAnalyser,
SourceParser,
runASTAnalysis,
runASTAnalysisOnFile,
Report,
Expand Down
94 changes: 19 additions & 75 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,95 +1,39 @@
// Import Node.js Dependencies
import fs from "fs/promises";
import path from "path";

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

// Import Internal Dependencies
import { SourceFile } from "./src/SourceFile.js";
import { warnings } from "./src/warnings.js";
import { isOneLineExpressionExport } from "./src/utils/index.js";
import { JsSourceParser } from "./src/JsSourceParser.js";
import { AstAnalyser } from "./src/AstAnalyser.js";

export function runASTAnalysis(
function runASTAnalysis(
str,
options = Object.create(null)
) {
const {
module = true,
isMinified = false,
removeHTMLComments = false
customParser = new JsSourceParser(),
...opts
} = options;

const parser = new JsSourceParser(str, { removeHTMLComments });
const body = parser.parseScript({
isEcmaScriptModule: Boolean(module)
});

const source = new SourceFile(parser.raw);

// we walk each AST Nodes, this is a purely synchronous I/O
walk(body, {
enter(node) {
// Skip the root of the AST.
if (Array.isArray(node)) {
return;
}
const analyser = new AstAnalyser(customParser);

const action = source.walk(node);
if (action === "skip") {
this.skip();
}
}
});

return {
...source.getResult(isMinified),
dependencies: source.dependencies,
isOneLineRequire: isOneLineExpressionExport(body)
};
return analyser.analyse(str, opts);
}

export async function runASTAnalysisOnFile(
async function runASTAnalysisOnFile(
pathToFile,
options = {}
) {
try {
const {
packageName = null,
module = true,
removeHTMLComments = false
} = options;

const str = await fs.readFile(pathToFile, "utf-8");
const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;
const {
customParser = new JsSourceParser(),
...opts
} = options;

const isMin = filePathString.includes(".min") || isMinified(str);
const data = runASTAnalysis(str, {
isMinified: isMin,
module: path.extname(filePathString) === ".mjs" ? true : module,
removeHTMLComments
});
if (packageName !== null) {
data.dependencies.delete(packageName);
}
const analyser = new AstAnalyser(customParser);

return {
ok: true,
dependencies: data.dependencies,
warnings: data.warnings,
isMinified: !data.isOneLineRequire && isMin
};
}
catch (error) {
return {
ok: false,
warnings: [
{ kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
]
};
}
return analyser.analyseFile(pathToFile, opts);
}

export { warnings };
export {
warnings,
AstAnalyser,
runASTAnalysis,
runASTAnalysisOnFile
};
129 changes: 129 additions & 0 deletions src/AstAnalyser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Import Node.js Dependencies
import fs from "node:fs/promises";
import path from "node:path";

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

// Import Internal Dependencies
import { SourceFile } from "./SourceFile.js";
import { isOneLineExpressionExport } from "./utils/index.js";

export class AstAnalyser {
/**
* @constructor
* @param { SourceParser } parser
*/
constructor(parser) {
this.parser = parser;
}

analyse(str, options = Object.create(null)) {
const {
isMinified = false,
module = true,
removeHTMLComments = false
} = options;

const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
isEcmaScriptModule: Boolean(module)
});

const source = new SourceFile(str);

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

const action = source.walk(node);
if (action === "skip") {
this.skip();
}
}
});

return {
...source.getResult(isMinified),
dependencies: source.dependencies,
isOneLineRequire: isOneLineExpressionExport(body)
};
}

async analyseFile(
pathToFile,
options = {}
) {
try {
const {
packageName = null,
module = true,
removeHTMLComments = false
} = options;

const str = await fs.readFile(pathToFile, "utf-8");
const filePathString = pathToFile instanceof URL ? pathToFile.href : pathToFile;

const isMin = filePathString.includes(".min") || isMinified(str);
const data = this.analyse(str, {
isMinified: isMin,
module: path.extname(filePathString) === ".mjs" ? true : module,
removeHTMLComments
});

if (packageName !== null) {
data.dependencies.delete(packageName);
}

return {
ok: true,
dependencies: data.dependencies,
warnings: data.warnings,
isMinified: !data.isOneLineRequire && isMin
};
}
catch (error) {
return {
ok: false,
warnings: [
{ kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] }
]
};
}
}

/**
* @param {!string} source
* @param {object} options
* @param {boolean} [options.removeHTMLComments=false]
*/
prepareSource(source, options = {}) {
if (typeof source !== "string") {
throw new TypeError("source must be a string");
}
const { removeHTMLComments = false } = options;

/**
* if the file start with a shebang then we remove it because meriyah.parseScript fail to parse it.
* @example
* #!/usr/bin/env node
*/
const rawNoShebang = source.startsWith("#") ?
source.slice(source.indexOf("\n") + 1) : source;

return removeHTMLComments ?
this.#removeHTMLComment(rawNoShebang) : rawNoShebang;
}

/**
* @param {!string} str
* @returns {string}
*/
#removeHTMLComment(str) {
return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
}
}
12 changes: 4 additions & 8 deletions src/JsSourceParser.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Import Third-party Dependencies
import * as meriyah from "meriyah";

// Import Internal Dependencies
import { SourceParser } from "./SourceParser.js";

// CONSTANTS
const kParsingOptions = {
next: true,
Expand All @@ -12,19 +9,19 @@ const kParsingOptions = {
jsx: true
};

export class JsSourceParser extends SourceParser {
export class JsSourceParser {
/**
* @param {object} options
* @param {boolean} options.isEcmaScriptModule
*/
parseScript(options = {}) {
parse(source, options = {}) {
const {
isEcmaScriptModule
} = options;

try {
const { body } = meriyah.parseScript(
this.source,
source,
{
...kParsingOptions,
module: isEcmaScriptModule,
Expand All @@ -43,7 +40,7 @@ export class JsSourceParser extends SourceParser {
isIllegalReturn
)) {
const { body } = meriyah.parseScript(
this.source,
source,
{
...kParsingOptions,
module: true,
Expand All @@ -58,4 +55,3 @@ export class JsSourceParser extends SourceParser {
}
}
}

34 changes: 0 additions & 34 deletions src/SourceParser.js

This file was deleted.

Loading

0 comments on commit e59a1a5

Please sign in to comment.