Working on the compiler pipeline for a JavaScript framework I am building the workflow requires parsing JavaScript code from pages and components into an Abstract Syntax Tree (AST), then transforming the code back to regular code after conducting static analysis to implement specific transformations. In my case, I was using Acorn to generate AST from JS Code and Escodegen to generate code from the transformed AST. In that process, I was consistently getting this error from Escodegen:
Error importing module: TypeError: Cannot read properties of null (reading 'length')
at CodeGenerator.generateFunctionParams (.../node_modules/escodegen/escodegen.js:912:42)
at CodeGenerator.generateFunctionBody (.../node_modules/escodegen/escodegen.js:941:23)
at CodeGenerator.MethodDefinition (.../node_modules/escodegen/escodegen.js:2158:26)
at CodeGenerator.generateExpression (.../node_modules/escodegen/escodegen.js:2516:28)
at .../node_modules/escodegen/escodegen.js:1133:38
at withIndent (.../node_modules/escodegen/escodegen.js:581:9)
at CodeGenerator.ClassBody (.../node_modules/escodegen/escodegen.js:1128:13)
at CodeGenerator.generateStatement (.../node_modules/escodegen/escodegen.js:2529:33)
at CodeGenerator.ClassDeclaration (.../node_modules/escodegen/escodegen.js:1159:30)
at CodeGenerator.generateStatement (.../node_modules/escodegen/escodegen.js:2529:33)
The error message is not very helpful because it doesn’t indicate where in the processed file Escodegen is encountering issues. However, what we can glean from the message is that the problem relates to how the AST is formatted.
So, the first thing I did was scan through the AST to figure out whether the file might contain incomplete JSON or if the AST itself was malformed. Without banking much on it, the quick scan satisfied me that the AST content and structure were complete and correct. I then headed over to an online JSON validator (https://jsonlint.com) pasted the AST there—and voilà! I got the message: "JSON is Valid."
But this did little to ease my woes, as I now had valid JSON that still couldn’t be processed as an AST.
Hesitant to dive into Escodegen’s scripts, I went back to the error message and paid attention to the part that says:
Cannot read properties of null (reading 'length')
at CodeGenerator.generateFunctionParams.
I figured the issue had to do with the structure Escodegen expects in the AST, specifically regarding function parameters. The "Cannot read properties of null (reading 'length')" part strongly suggested an array-related problem.
To provide context, let’s look at the raw JavaScript code that triggered this error:
Real-World Example: The RegistrationForm
Class
In my file I had a RegistrationForm
class that creates a simple registration form in the DOM. Here’s the raw JavaScript code:
class RegistrationForm {
constructor(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error(`Container with ID '${containerId}' not found.`);
return;
}
this.createForm();
}
createForm() {
const form = document.createElement('form');
form.id = 'registrationForm';
form.addEventListener('submit', this.handleSubmit.bind(this));
const nameLabel = this.createLabel('Name:', 'nameInput');
const nameInput = this.createInput('text', 'nameInput', 'name');
const surnameLabel = this.createLabel('Surname:', 'surnameInput');
const surnameInput = this.createInput('text', 'surnameInput', 'surname');
const submitBtn = document.createElement('button');
submitBtn.type = 'submit';
submitBtn.textContent = 'Register';
form.appendChild(nameLabel);
form.appendChild(nameInput);
form.appendChild(document.createElement('br'));
form.appendChild(surnameLabel);
form.appendChild(surnameInput);
form.appendChild(document.createElement('br'));
form.appendChild(submitBtn);
this.container.appendChild(form);
}
createLabel(text, forId) {
const label = document.createElement('label');
label.htmlFor = forId;
label.textContent = text;
return label;
}
createInput(type, id, name) {
const input = document.createElement('input');
input.type = type;
input.id = id;
input.name = name;
input.required = true;
return input;
}
handleSubmit(event) {
event.preventDefault();
const name = document.getElementById('nameInput').value;
const surname = document.getElementById('surnameInput').value;
alert(`Registered: ${name} ${surname}`);
}
}
document.addEventListener('DOMContentLoaded', () => {
new RegistrationForm('regForm');
});
Parsing the Code with Acorn
Let's deal with installing and setting up Acorn first.
🔧 1. Install Acorn
Install Acorn via npm or yarn:
npm install acorn
or
yarn add acorn
📥 2. Import Acorn
In your JavaScript file:
import as acorn from 'acorn';
⚙️ 3. Parse JavaScript Code Use acorn.parse()
to convert code into an Abstract Syntax Tree (AST):
// code is the raw js code we referred to above.
const ast = acorn.parse(code, { ecmaVersion: 2023 });
console.log(ast);
When we parse this code with Acorn, we get an AST. However, if the AST is incomplete or malformed (at least according to the format expected by Escodegen, we will encounter errors when using Escodegen to regenerate the code.
For example, consider the handleSubmit
method in the AST:
{
"type": "MethodDefinition",
"start": 1361,
"end": 1569,
"static": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 1361,
"end": 1373,
"name": "handleSubmit"
},
"kind": "method",
"value": {
"type": "FunctionExpression",
"start": 1373,
"end": 1569,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 1374,
"end": 1379,
"name": "event"
}
],
"body": {
"type": "BlockStatement",
"start": 1381,
"end": 1569,
"body": [
{
"type": "ExpressionStatement",
"start": 1383,
"end": 1406,
"expression": {
"type": "CallExpression",
"start": 1383,
"end": 1405,
"callee": {
"type": "MemberExpression",
"start": 1383,
"end": 1403,
"object": {
"type": "Identifier",
"start": 1383,
"end": 1388,
"name": "event"
},
"property": {
"type": "Identifier",
"start": 1389,
"end": 1403,
"name": "preventDefault"
},
"computed": false,
"optional": false
},
"arguments": null, // Right here is the bug! Problem: This should be an empty array []
"optional": false
}
}
]
}
}
}
The Issue: arguments: null
In the CallExpression
node, the arguments
property is null
instead of an empty array ([]
). This causes Escodegen to throw the error:
TypeError: Cannot read properties of null (reading 'length')
The Solution: Ensure Proper AST Structure
To fix this issue, we need to ensure that all required properties in the AST are properly defined. Specifically:
-
Check for
null
orundefined
Values:- Replace
null
values with their expected defaults (e.g.,arguments: []
forCallExpression
nodes). - Validate the AST:
- Use a validation function to traverse the AST and fix malformed nodes.
- Replace
Here’s how we can fix the arguments
issue:
function fixAST(ast) {
if (ast.type === 'CallExpression' && ast.arguments === null) {
ast.arguments = [];
}
for (const key in ast) {
if (ast[key] && typeof ast[key] === 'object') {
fixAST(ast[key]);
}
}
return ast;
}
Applying the Fix
Before passing the AST to Escodegen, we apply the fixAST
function:
const fixedAST = fixAST(originalAST);
const code = escodegen.generate(fixedAST);
In my case I had "null" value (object) coming up in MethodDefinition
nodes having a FunctionExpression
with params: null
as well. So I needed to extend my ast fixer function to handle these as well.
- Null
params
for FunctionExpression and ArrowFunctionExpression. - Null
params
in MethodDefinition nodes. - Null
arguments
in CallExpression nodes.
Here’s the complete code:
function fixAST(node) {
if (!node || typeof node !== 'object') return node;
// Handle CallExpression argument null issue
if (node.type === 'CallExpression' && node.arguments === null) {
node.arguments = [];
}
// Handle null params in FunctionExpression, ArrowFunctionExpression, and MethodDefinition
if ((node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && node.params === null) {
node.params = [];
}
if (node.type === 'MethodDefinition' && node.value && node.value.type === 'FunctionExpression' && node.value.params === null) {
node.value.params = [];
}
// Recursively clean child nodes
for (const key in node) {
if (node.hasOwnProperty(key)) {
const value = node[key];
if (Array.isArray(value)) {
node[key] = value.filter(Boolean).map(fixAST);
} else if (typeof value === 'object' && value !== null) {
fixAST(value);
}
}
}
return node;
}
🔍 Key Changes:
- Function Expressions: If
params
isnull
, it gets replaced with an empty array[]
. - Method Definitions: Ensures
params
exists insidenode.value
for method definitions. - Call Expressions: Ensures
arguments
is nevernull
.
✅ Example AST Before and After:
Before:
{
"type": "MethodDefinition",
"key": { "type": "Identifier", "name": "createForm" },
"value": {
"type": "FunctionExpression",
"params": null,
"body": { "type": "BlockStatement", "body": [] }
}
}
After Cleaning:
{
"type": "MethodDefinition",
"key": { "type": "Identifier", "name": "createForm" },
"value": {
"type": "FunctionExpression",
"params": [],
"body": { "type": "BlockStatement", "body": [] }
}
}
Conclusion
When working with ASTs, it’s crucial to ensure that the structure is complete and consistent. Errors like Unknown node type: undefined
and Cannot read properties of null
often stem from malformed nodes, such as arguments: null
in CallExpression
nodes. By validating and fixing the AST before passing it to Escodegen, we can avoid these issues and generate valid JavaScript code.
Key Takeaways
- Validate the AST: Always check for
null
orundefined
values in critical properties. - Use Defaults: Replace
null
values with their expected defaults (e.g.,arguments: []
). - Test Thoroughly: After modifying the AST, test it with Escodegen to ensure it generates valid code.
- A more sustainable long-term solution would be to fix how Acorn handles empty array items. Alternatively, we could consider using other parsers, such as Esprima, that may handle these cases more effectively, ensuring the final AST is compatible with ESTree-compliant code generators.
Thank you for reading this far!!
P.S. I headed over to AST Explorer and pasted the same JavaScript code to get the Acorn-generated AST. The AST was well-formatted with the expected empty arrays, as we have explored in the article. So, I need to figure out why the Acorn implementation in my framework is producing a different AST output. It could be a version issue or some config that I am missing. I will come back to this post with feedback.
Top comments (0)