DEV Community

Cover image for Fixing Escodegen 'Cannot read properties of null (reading 'length')' Error In Acorn Generated JS Abstract Syntax
Gugulethu Nyoni
Gugulethu Nyoni

Posted on

Fixing Escodegen 'Cannot read properties of null (reading 'length')' Error In Acorn Generated JS Abstract Syntax

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)
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

📥 2. Import Acorn
In your JavaScript file:

import  as acorn from 'acorn';
Enter fullscreen mode Exit fullscreen mode

⚙️ 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);
Enter fullscreen mode Exit fullscreen mode

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
                    }
                }
            ]
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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:

  1. Check for null or undefined Values:

    • Replace null values with their expected defaults (e.g., arguments: [] for CallExpression nodes).
    • Validate the AST:
    • Use a validation function to traverse the AST and fix malformed nodes.

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;
}
Enter fullscreen mode Exit fullscreen mode

Applying the Fix

Before passing the AST to Escodegen, we apply the fixAST function:

const fixedAST = fixAST(originalAST);
const code = escodegen.generate(fixedAST);
Enter fullscreen mode Exit fullscreen mode

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.

  1. Null params for FunctionExpression and ArrowFunctionExpression.
  2. Null params in MethodDefinition nodes.
  3. 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;
}
Enter fullscreen mode Exit fullscreen mode

🔍 Key Changes:

  1. Function Expressions: If params is null, it gets replaced with an empty array [].
  2. Method Definitions: Ensures params exists inside node.value for method definitions.
  3. Call Expressions: Ensures arguments is never null.

✅ Example AST Before and After:

Before:

{
    "type": "MethodDefinition",
    "key": { "type": "Identifier", "name": "createForm" },
    "value": {
        "type": "FunctionExpression",
        "params": null,
        "body": { "type": "BlockStatement", "body": [] }
    }
}
Enter fullscreen mode Exit fullscreen mode

After Cleaning:

{
    "type": "MethodDefinition",
    "key": { "type": "Identifier", "name": "createForm" },
    "value": {
        "type": "FunctionExpression",
        "params": [],
        "body": { "type": "BlockStatement", "body": [] }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Validate the AST: Always check for null or undefined values in critical properties.
  2. Use Defaults: Replace null values with their expected defaults (e.g., arguments: []).
  3. Test Thoroughly: After modifying the AST, test it with Escodegen to ensure it generates valid code.
  4. 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)