Skip to content

Potential Issue: Modified ImportSpecifier or ObjectProperty not printed correctly #1421

@shixianqin

Description

@shixianqin

This issue contains two reproducible cases where recast fails to generate the correct output after modifying AST nodes.

Environment & Versions:

  • recast: v0.23.11
  • @babel/traverse: v7.27.0
  • Node.js: v20.18.1
  • macOS: 15.4 (Intel Chip)

Steps:

  • recast.parse(...) with the babel-ts parser
  • AST traversal and mutation with @babel/traverse
  • Code regeneration with recast.print(...)

If this usage pattern is incorrect or unsupported, please advise. Otherwise, the behavior below seems to be a bug.


Case 1: ImportSpecifier alias not printed

Input:

import { foo } from 'my-package';

Transformation:

I update the local.name of the ImportSpecifier to _fooLocalName, without changing imported.name.
They are not the same object, and their names are different.
importKind remains "value".

Expected output:

import { foo as _fooLocalName } from 'my-package';

Actual output:

import { _fooLocalName } from 'my-package'; // ❌ incorrect

Case 2: ObjectProperty shorthand not updated

Input:

const foo = 100;
console.log({ foo });

Transformation:

I rename the reference foo to _fooValueName, but the parent ObjectProperty still has shorthand: true.

Expected output:

console.log({ foo: _fooValueName });

Actual output:

console.log({ _fooValueName }); // ❌ incorrect

Reproduction Code

import * as assert from 'node:assert';
import { parse, print } from 'recast';
import * as babelTsParser from 'recast/parsers/babel-ts';
import traverse from '@babel/traverse';

function reproduceCase1() {
  const source = `import { foo } from 'my-package'`;
  const ast = parse(source, { parser: babelTsParser });

  traverse(ast, {
    ImportDeclaration(path) {
      const fooSpecifier = path.node.specifiers[0];
      fooSpecifier.local.name = '_fooLocalName';

      assert.notEqual(fooSpecifier.imported.name, fooSpecifier.local.name);
      assert.equal(fooSpecifier.importKind, 'value');
    },
  });

  const result = print(ast).code;
  assert.equal(result, `import { foo as _fooLocalName } from 'my-package'`);
}

function reproduceCase2() {
  const source = `const foo = 100; console.log({ foo })`;
  const ast = parse(source, { parser: babelTsParser });

  traverse(ast, {
    Program(path) {
      const fooRef = path.scope.getBinding('foo')!.referencePaths[0];
      const objProp = fooRef.parent;

      fooRef.node.name = '_fooValueName';

      assert.notEqual(objProp.key.name, objProp.value.name);
      assert.equal(objProp.shorthand, true);
    },
  });

  const result = print(ast).code;
  assert.equal(result, `const foo = 100; console.log({ foo: _fooValueName })`);
}

reproduceCase1();
reproduceCase2();

Here is how to fix this problem

function removeRange (path) {
  delete path.node.start;
  delete path.node.end;
}

traverse(ast, {
  ImportSpecifier: removeRange,
  ObjectProperty: removeRange,
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions