Skip to content

Incorrect Transformation of Ternary Expressions Containing Function Calls #208

@CodySchaaf

Description

@CodySchaaf

When applying the add-conversions plugin on TypeScript code involving ternary expressions that return function expressions in one branch and property accesses in the other, the output code is malformed. Specifically, the transformation incorrectly duplicates part of the code and introduces syntax errors.

Input Code

export function Complaints({ currentUserCanEdit, updateComplaint }) {
  const complaint = {};

  return currentUserCanEdit
    ? () => {
        updateComplaint(complaint.id);
      }
    : complaint.type;
}

Expected Output

export function Complaints({ currentUserCanEdit, updateComplaint }) {
  const complaint = {};

  return currentUserCanEdit
    ? () => {
        updateComplaint((complaint as any).id);
      }
    : (complaint as any).type;
}

Actual Output

export function Complaints({ currentUserCanEdit, updateComplaint }) {
  const complaint = {};

  return currentUserCanEdit
    ? () => {
        updateComplaint(complaint.id);
    }
    : (complaint as any).type;
        updateComplaint((complaint as any).id);
      }
    : complaint.type;
}

Steps to Reproduce

  1. Create a TypeScript file with the provided input code snippet.
  2. Run the add-conversions plugin from the ts-migrate tool on this file.
  3. Observe the malformed output as described above.

Additional Information

  • The problem seems to arise from the transformation logic not correctly handling the scoping or branching of ternary expressions, especially when mixed with function expressions, which is a simplified version of a problem I ran into when converting a react component that contained a component within a ternary operation.
{currentUserCanEdit ? (
                      <Input
                        widthOptions="s"
                        onChange={(e: any) => {
                          updateComplaint(e.currentTarget.value, 'caseNumber', complaint.id, index)
                        }}
                        value={complaint.caseNumber}
                      />
                    ) : (
                      complaint.caseNumber
                    )}

Request for Guidance
I've attempted to address this issue by adding a new failing test case to highlight the problem, but I'm unsure where within the add-conversions plugin's transformation logic this case should be specifically handled. Any insights or suggestions on how to properly transform these expressions without introducing syntax errors would be greatly appreciated.

New Failing Test

it("handles ternary expressions containing a function call", async () => {
    const text = `export function Complaints({ currentUserCanEdit, updateComplaint }) {
  const complaint = {};

  return currentUserCanEdit
    ? () => {
        updateComplaint(complaint.id);
      }
    : complaint.type;
}
`;
    const result = addConversionsPlugin.run(await realPluginParams({ text }));

    expect(result)
      .toBe(`export function Complaints({ currentUserCanEdit, updateComplaint }) {
  const complaint = {};

  return currentUserCanEdit
    ? () => {
        updateComplaint((complaint as any).id);
      }
    : (complaint as any).type;
}
`);
  });

Possible Solution
It does seem like this will solve it

const ancestorChecks = [
        ts.SyntaxKind.ExpressionStatement,
        ts.SyntaxKind.ConditionalExpression,
      ];
      ancestorReplaceMap.set(
        origNode,
        ancestorShouldBeReplaced === undefined
          ? ancestorChecks.includes(origNode.kind)
          : ancestorChecks.includes(origNode.kind) || ancestorShouldBeReplaced
      );

But I'm not comfortable enough with the package to know if this could have any negative consequences.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions